feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge

The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.

run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).

The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
This commit is contained in:
Tianyi Cui
2026-07-08 12:58:23 +08:00
parent aa2a7f9a8a
commit b59d245c7c
56 changed files with 2395 additions and 102 deletions

View File

@@ -37,7 +37,7 @@ The hooks **themselves** run in the agent's session workspace: for the agent-sco
| `SessionStart` | `agent/session-start` (emit) | additionalContext → `agent.inject()` into the new session (cannot block) |
| `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `deny``PromptDecision.block`; additionalContext-only → delegate via `next()` then fold context onto the downstream decision (a later listener can still block/rewrite) |
| `PreToolUse` | `tools/pre-execute` (waterfall) | `deny``PreToolDecision.deny`; `ask``PreToolDecision.ask` |
| `PostToolUse` | `tools/post-execute` (waterfall) | `deny``block` with feedback; additionalContext-only → delegate via `next()` then fold context onto the downstream decision |
| `PostToolUse` | `tools/post-execute` (waterfall) | `deny``block` with feedback; additionalContext-only → delegate via `next()` then fold context onto the downstream decision (a Code Mode sub-calls context is dropped by the run_code bridge — see [the pipeline doc](../../../docs/tool-execution-pipeline.md)) |
| `Stop` | `agent/turn-continuation` (waterfall) | a blocking Stop hook forces `continue`, feeding its reason as next-step steering |
| `SubagentStart` | `subagent/start` (emit) | additionalContext → `agent.inject()` into the live child |
| `SubagentStop` | `subagent/end` (emit) | observe-only |

View File

@@ -43,7 +43,7 @@ The hooks themselves run in the agent's session workspace: for the agent-scoped
| `SessionStart` | `agent/session-start` (emit) | a plain-stdout hook's output → additionalContext → `agent.inject()` |
| `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `block` (exit 2) → `PromptDecision.block`; additionalContext-only → delegate via `next()` then fold context onto the downstream decision |
| `PreToolUse` | `tools/pre-execute` (waterfall) | `block``PreToolDecision.deny` (no `allow`/`ask`) |
| `PostToolUse` | `tools/post-execute` (waterfall) | `block``block` with feedback; additionalContext-only → delegate via `next()` then fold context onto the downstream decision |
| `PostToolUse` | `tools/post-execute` (waterfall) | `block``block` with feedback; additionalContext-only → delegate via `next()` then fold context onto the downstream decision (a Code Mode sub-calls context is dropped by the run_code bridge — see [the pipeline doc](../../../docs/tool-execution-pipeline.md)) |
| `Stop` | `agent/turn-continuation` (waterfall) | a blocking Stop hook forces `continue` with the reason as next-step steering |
A tool call's payload carries the real `tool_name` (the same value the matcher tests) and Codex's `tool_input: { command }` shape (the `command` arg when present, else `''`). The matcher subject is the tool name (`PreToolUse`/`PostToolUse`) or the session source (`SessionStart`); `UserPromptSubmit`/`Stop` ignore matchers.