mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-24 20:35:31 -04:00
8fca61dc03
Adds rubric-driven acceptance criteria to Deep Agents Code and layers a goal workflow on top of it. A rubric is the explicit definition of done. Use it when you already know the criteria the agent should satisfy before it considers the work complete. Criteria can be sticky for the thread, one-shot for the next turn, or loaded from a file. While the agent works, the TUI shows grading lifecycle messages so the user can see when the work is being checked, revised, satisfied, or stopped. Usage examples: ```text /rubric set tests pass; no unrelated files changed; help text is updated /rubric next only change the auth callback; do not refactor unrelated code /rubric file acceptance.md /rubric show /rubric clear ``` A goal is the objective-oriented workflow. Use it when you know the outcome, but want `dcode` to propose acceptance criteria before execution. `/goal <objective>` asks the model to draft criteria and displays them in an inline review prompt. The user can accept the criteria, edit them directly, reject them with feedback to regenerate, or cancel. Accepted goal criteria become the sticky rubric for the thread. Usage examples: ```text /goal add OAuth refresh handling /goal show /goal clear ``` The `--goal` CLI flag starts the same goal-review workflow when launching the TUI. It is intentionally interactive: the generated criteria must be reviewed before execution. After the user accepts the proposal, the accepted goal is sent as the first task. ```bash dcode --goal "add OAuth refresh handling" ``` `--goal` cannot be combined with `-n`, `-m`, `--skill`, or `--rubric`. For non-interactive/headless runs, users should provide criteria explicitly with `--rubric` instead: ```bash dcode -n "implement OAuth refresh handling" --rubric "tests pass; no unrelated files changed" dcode -n "implement OAuth refresh handling" --rubric @acceptance.md ``` Accepted goal/rubric state is persisted on the thread and restored on resume. For example, a user can set `/goal add OAuth refresh handling`, accept the proposed criteria, quit the TUI, and later resume the same thread with the goal and criteria still active. That matters because the acceptance criteria continue to guide future turns and remain visible in `/goal show` instead of becoming hidden context that disappears between sessions. When the agent believes the goal is done, it does not get to declare victory on its own. For example, after implementing OAuth refresh handling, the agent can ask to mark the goal complete with evidence such as "tests pass." `dcode` keeps that request pending until the rubric check finishes. If the rubric still needs revision, the goal stays active and the user sees why it was not completed. If the rubric is satisfied, auto-approve mode records the completion automatically; manual mode asks the user before changing the goal status. This keeps the user's accepted criteria as the source of truth for whether the goal is actually finished. The active criteria and goal are also visible to the agent through constrained tools. `get_rubric` lets the agent inspect the current criteria and whether they came from a goal, a sticky rubric, or the current invocation. `get_goal` lets it inspect the active objective, status, criteria, and any prior note. `update_goal` lets it report when it believes the goal is `complete` or `blocked` with evidence. The constraints matter: the agent can read criteria and update progress, but it cannot create, pause, resume, clear, or replace goals. Those lifecycle actions stay user/system controlled so the model cannot silently redefine or remove the user's objective.
110 lines
4.1 KiB
Python
110 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)
|
|
|
|
effective_model: str | None = None
|
|
|
|
auto_approve: bool = False
|
|
|
|
approval_mode_key: str | None = None
|
|
|
|
thread_id: str | None = None
|
|
|
|
blocked_goal_retry_context: 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.
|
|
"""
|
|
|
|
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.
|
|
"""
|