Files
deepagents/libs/code/deepagents_code/_cli_context.py
T
Mason Daugherty 564e5a05bc fix(code): run /offload server-side (#4696)
`/offload` now stores archived conversation history through the agent's
backend.

---

`/offload` previously summarized and saved conversation history in the
client process. In server and sandbox modes, that process does not own
the backend used by the agent: persistence could fail against a
read-only filesystem, and even a successfully written archive would not
be available to the agent through `read_file`.

This changes Deep Agents Code to run the existing `compact_conversation`
tool through the active agent instead. The command seeds a tool call
into the thread, approves the expected human-in-the-loop interrupt
because the user explicitly requested `/offload`, resumes the graph, and
reads the persisted summarization event back from server state. The
archive is therefore written through the agent's composite backend and
remains readable by the agent in local, server, and sandbox runs.

The old client-side `perform_offload` helper (and its `OffloadResult` /
`OffloadThresholdNotMet` / `OffloadModelError` result types) is removed,
as summarization and persistence now run through the agent. In local
mode, the `conversation_history` backend now roots under `~/.deepagents`
(via `_offload_fallback_root`, with a hardened private-temp fallback
when the home directory is not writable) instead of a throwaway
`tempfile.mkdtemp` directory, so offloaded history persists across
sessions.

The SDK's `compact_conversation` API is unchanged; this PR is confined
to Deep Agents Code.

Made by [Open
SWE](https://openswe.vercel.app/agents/1cbc308e-b411-2a09-bd6b-541e606ca4c5)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-07-14 15:03:45 -04:00

116 lines
4.1 KiB
Python

"""Lightweight runtime context types for the CLI agent graph.
Carries per-run overrides (model swap/params, approval mode) passed via
`context=`. Extracted from `configurable_model` so hot-path modules (`app`,
`textual_adapter`) can import `CLIContext` without pulling in the langchain
middleware stack.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, TypedDict
@dataclass
class CLIContextSchema:
"""Declared `context_schema` for the agent graph.
Registered via `context_schema=` when the graph is built, so LangGraph
coerces each run's `context=` payload into this dataclass — in-process,
`runtime.context` is a `CLIContextSchema` instance.
It exists alongside `CLIContext` (below) because the payload is shaped
differently on each side of the API boundary: in-process it is coerced to
this dataclass, but over the LangGraph API server (RemoteGraph) it is
serialized to JSON and arrives as a plain dict. Consumers
(`configurable_model._get_context`, `_should_interrupt_tool_call`)
therefore accept both shapes. `CLIContext` is the client-facing builder for
constructing that payload.
Fields mirror `CLIContext`; see its per-field docstrings for semantics.
"""
model: str | None = None
model_params: dict[str, Any] = field(default_factory=dict)
profile_overrides: dict[str, Any] = field(default_factory=dict)
model_context_limit: int | None = None
auto_approve: bool = False
approval_mode_key: str | None = None
thread_id: str | None = None
blocked_goal_retry_context: str | None = None
offload_tool_call_id: str | None = None
class CLIContext(TypedDict, total=False):
"""Client-facing builder for the per-run graph context payload.
Callers populate this and pass it via `context=` to `astream`/`ainvoke`.
`ConfigurableModelMiddleware` and the `interrupt_on` `when` predicate read
it from `request.runtime.context`. In-process LangGraph coerces it into
`CLIContextSchema` (the registered `context_schema`); over the API it stays
a plain dict — which is why consumers handle both shapes.
"""
model: str | None
"""Model spec to swap at runtime (e.g. `'provider:model'`)."""
model_params: dict[str, Any]
"""Invocation params (e.g. `temperature`, `max_tokens`) to merge
into `model_settings`."""
profile_overrides: dict[str, Any]
"""Model profile metadata supplied by `--profile-override`."""
model_context_limit: int | None
"""Effective context-window limit for profile-aware middleware."""
auto_approve: bool
"""Whether gated tool calls should skip the human-approval interrupt.
Sourced from the client session (not graph state) so the model cannot
self-approve by writing state. The `interrupt_on` `when` predicate reads
this to suppress interrupts at the source when "approve always" is on,
avoiding the interrupt-then-auto-resolve round-trip.
"""
approval_mode_key: str | None
"""Store key for the live approval-mode control record.
The TUI updates this record when the user toggles approval mode mid-run.
The server-side interrupt predicate reads it from the LangGraph Store on
each gated tool call so auto-to-manual changes can take effect before the
current stream returns.
"""
thread_id: str | None
"""LangGraph thread ID for the active conversation.
Mirrors `config.configurable.thread_id` into runtime context for model-call
middleware that needs per-request session identity, including Fireworks
session-affinity headers.
"""
blocked_goal_retry_context: str | None
"""One-turn model context for retrying a previously blocked goal.
This is intentionally carried in runtime context instead of the user
message so it is not parsed as a file mention or checkpointed as human
input.
"""
offload_tool_call_id: str | None
"""The sole tool-call ID authorized during a server-driven `/offload` run.
This is set by the client, not graph state, so model-generated calls cannot
grant themselves permission to execute during the hidden compaction turn.
"""