Files
deepagents/libs/code/deepagents_code/_cli_context.py
T
Mason Daugherty 4600365ea0 fix(code): sync approval toggles during active runs (#4239)
Approval mode now has a live control path for remote `dcode` sessions
instead of relying only on the run-context snapshot captured at stream
start. That lets mid-run toggles take effect before the next gated tool
call, while failing closed to manual approval when the live state cannot
be synced or read.

## Changes
- **Live approval state:** Added `approval_mode_key`,
`approval_mode_payload`, `read_approval_mode_from_store`, and
`awrite_approval_mode` to store per-thread auto-approve state in the
LangGraph Store under a deterministic hashed thread key.
- **Interrupt decisions:** Updated `_should_interrupt_tool_call` to
prefer the live store value over stale context, so toggling from
auto-approve back to manual can interrupt subsequent gated tool calls in
the same active run.
- **TUI synchronization:** Updated
`DeepAgentsApp.action_toggle_auto_approve` and
`_on_auto_approve_enabled` to write the live approval mode whenever the
user toggles approval behavior.
- **Fail-closed behavior:** If manual approval cannot sync while an
agent is running, the app warns the user and cancels the active run
rather than allowing stale auto-approval to continue.
- **Remote support:** Added `RemoteAgent.aput_store_item` so remote
sessions can write unindexed LangGraph Store records used by the
server-side approval predicate.
- **Context reconstruction:** Preserved `auto_approve` and
`approval_mode_key` when dict-based runtime context is reconstructed
into `CLIContextSchema`.
- **Stream propagation:** Updated `execute_task_textual` to write live
approval state before each stream iteration and await async auto-approve
callbacks triggered from approval prompts.
2026-06-24 23:12:40 -04:00

90 lines
3.4 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)
effective_model: str | None = None
auto_approve: bool = False
approval_mode_key: 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`."""
effective_model: str | None
"""Resolved `provider:model` spec actually in use for this invocation.
Unlike `model` (a swap *instruction* that is `None` when the base model is
used), this carries the model in effect — whether from a `/model` override
or the startup default — so `ResumeStateMiddleware` can record it to
checkpoint state for restore on resume. `None` when no usable spec is
resolved yet (e.g. credentials not configured), in which case nothing is
recorded rather than a malformed spec.
"""
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.
"""