Files
deepagents/libs/code/deepagents_code/_cli_context.py
T
Mason Daugherty 8fca61dc03 feat(code): add rubric-backed goal workflow (#4365)
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.
2026-06-29 16:20:18 -04:00

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.
"""