Lee-W commented on code in PR #73559:
URL: https://github.com/apache/airflow/pull/73559#discussion_r4091927936


##########
providers/common/ai/src/airflow/providers/common/ai/toolsets/sandbox.py:
##########
@@ -230,26 +311,146 @@ async def for_run(self, ctx: RunContext[Any]) -> 
AbstractToolset[Any]:
         # not silently degrade to this class on every run.
         return type(self)(
             self._backend,
-            spec=self._spec,
+            # Attach mode refuses a spec, and the default one filled in above 
is
+            # not the author's, so it is not handed back.
+            spec=None if self._attach_mode else self._spec,
             default_command_timeout=self._default_command_timeout,
             max_command_timeout=self._max_command_timeout,
             max_output_lines=self._max_output_lines,
             max_output_bytes=self._max_output_bytes,
             max_read_bytes=self._max_read_bytes,
             tool_prefix=self._tool_prefix,
+            attach_to=self._attached_handle() if self._attach_mode else None,
+            owner=self._owner,
         )
 
+    def _attached_handle(self) -> str:
+        """
+        Return the rendered handle, checked, because the templater bypasses 
the constructor.
+
+        ``attach_to`` is templated so the handle can come from an upstream 
XCom, and
+        an XCom that was never pushed renders to ``None`` under native 
rendering or to
+        the string ``"None"`` otherwise. Neither is a sandbox, and silently 
falling back
+        to provisioning one would run the agent in an empty workspace with no 
error.
+        """
+        handle = self.attach_to
+        if not is_sandbox_handle(handle):
+            raise SandboxTerminalError(
+                f"attach_to rendered to {handle!r}, which is not a sandbox 
handle. The task that "
+                "provisions the sandbox pushed nothing, or this task does not 
depend on it and ran "
+                "first; check the upstream task and the XCom it returns."
+            )
+        return handle
+
     async def __aenter__(self) -> Self:
-        # The sandbox is provisioned lazily on first use, not here: a durable
+        # An owned sandbox is provisioned lazily on first use, not here: a 
durable
         # replay that only serves cached tool results must not provision one, 
and
-        # nothing leaks if the run fails before any tool executes.
+        # nothing leaks if the run fails before any tool executes. An attached 
one is
+        # claimed now, so a wrong handle or a held sandbox fails the run 
before the
+        # model has spent anything, and the tool descriptions can state the 
lifetime.
+        if self._attach_mode:
+            await self._attach(self._attached_handle())
         return self
 
+    async def _attach(self, handle: str) -> None:
+        owner, holder = self._identity()
+        backend = self._attachable_backend()
+        try:
+            attached = await asyncio.to_thread(backend.attach, handle, 
owner=owner, holder=holder)
+        except SandboxTerminalError:
+            raise
+        except SandboxError as e:
+            # Nothing the model does can change whether this sandbox can be 
attached
+            # to, so a recoverable label here is one it could not act on.
+            raise SandboxTerminalError(
+                f"Could not attach to sandbox {handle!r} on backend 
{backend.name!r}: {e}"
+            ) from e
+        self._sandbox = handle
+        self._holder = holder
+        remaining = attached.remaining_lifetime
+        self._expires_at = None if remaining is None else time.monotonic() + 
remaining
+        self._attach_note = self._describe_attached(attached.network, 
remaining)
+        log.info(
+            "Attached to sandbox %s on backend %s as %s; %s of its lifetime 
remain",
+            handle,
+            backend.name,
+            holder,
+            "an unknown number of seconds" if remaining is None else 
f"{remaining:.0f}s",
+        )
+
+    @classmethod
+    def _describe_attached(cls, network: SandboxSpec | None, 
remaining_lifetime: float | None) -> str:
+        whose = (
+            "This sandbox was set up by an earlier task, and your files stay 
in it after this run "
+            "for a later task to collect."
+        )
+        policy = (
+            cls._describe_network(network)
+            if network is not None
+            else "Its network access is whatever the task that set it up 
allowed; test before relying on it."
+        )
+        if remaining_lifetime is None:
+            clock = "How long it has left is not known."
+        elif remaining_lifetime < 60:
+            clock = "Under a minute of its lifetime remained when this run 
began, so finish up."
+        else:
+            minutes = round(remaining_lifetime / 60)
+            unit = "minute" if minutes == 1 else "minutes"
+            clock = f"About {minutes} {unit} of its lifetime remained when 
this run began."
+        return f"{whose} {policy} {clock}"
+
+    def _attachable_backend(self) -> AttachableSandboxBackend:
+        if self._attachable is None:
+            # The constructor refuses attach_to on any other backend, so this 
is a
+            # programming error, not a run-time condition.
+            raise RuntimeError("attach mode on a backend that cannot attach")
+        return self._attachable
+
+    def _identity(self) -> tuple[str, str]:

Review Comment:
   should we make the return type a named tuple?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to