subagent: implement structured output for in-process backends

The seam vocabulary (SubagentStartRequest.outputSchema, SubagentResult
.structured) existed but no in-process backend honored it — spawn/fork
advertised outputSchema: false. This lands the missing half:

- dsh-tools gains a structured-output JSON Schema subset (json-schema.ts):
  StructuredOutputSchema, assertSupportedOutputSchema (rejects loud outside
  the enforced subset, every violation listed), validateStructuredValue
  (path-qualified issues, total). outputSchema's seam type becomes this raw
  JSON-Schema subset instead of the author-facing SchemaSpec DSL — the schema
  travels verbatim to the model as a forced tool's parameters.
- dsh-subagent-inprocess gains the shared structured runtime: one global
  structured_output capture tool (placeholder parameters) + a prepend:true
  agent/request listener doing FINAL-REQUEST enforcement (strip for plain
  agents, per-run schema for structured children — survives downstream
  request-replacing listeners) + an agent/turn-continuation veto that stops
  a child's turn once captured (no wasted extra model step). Lifetime is
  refcounted by backends (plugin lifetime) AND live runs (start→settle).
- startInProcessRun drives the capture: subset asserted before the child
  exists, instruction appended to the child's system prompt, clean-finish
  nudge loop (structuredNudgeRetries, backend Config, default 1), captured
  value on result.structured; a clean finish without a capture settles
  'error' (never a silent success with a missing field).
- spawn + fork flip outputSchema: true and inject 'tools'.
This commit is contained in:
Tianyi Cui
2026-07-05 11:35:39 +08:00
parent 2bad139ece
commit dafb81be7b
26 changed files with 1402 additions and 65 deletions

View File

@@ -287,7 +287,7 @@ A tool was registered or unregistered (the available tool set changed).
'tools/change'(): void
```
Source: [`packages/core/tools/src/index.ts:87`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:97`](../../packages/core/tools/src/index.ts)
### `tools/post-execute` — waterfall
@@ -299,7 +299,7 @@ Waterfall AFTER a tool runs — where hook plugins inspect the result and accept
Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:82`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:92`](../../packages/core/tools/src/index.ts)
### `tools/pre-execute` — waterfall
@@ -311,7 +311,7 @@ Waterfall BEFORE a tool runs — the gate where sandbox, permission, and hook pl
Types: [ToolExecution](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:66`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:76`](../../packages/core/tools/src/index.ts)
## Inherited events (cordis core + loader/hmr/timer)

View File

@@ -203,7 +203,7 @@ async execute(exec: ToolExecution): Promise<ToolExecutionResult>
Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:268`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:278`](../../packages/core/tools/src/index.ts)
## `ctx.web` — `WebService`

View File

@@ -20,7 +20,7 @@ interface SubagentCapabilities {
## The start request
What a caller asks for when starting a subagent. The tool layer builds this from the model's `{ description, prompt }` plus its own config; the service validates the start-time capabilities against the named provider, then passes it to `provider.start`. `parent` is REQUIRED — in-process backends read `parent.session.header` for the working directory, the `parentSession` lineage, and the delegation depth. The three optional fields (`outputSchema`, `maxDepth`, `toolFilter`) each gate on the matching `SubagentCapabilities` flag.
What a caller asks for when starting a subagent. The tool layer builds this from the model's `{ description, prompt }` plus its own config; the service validates the start-time capabilities against the named provider, then passes it to `provider.start`. `parent` is REQUIRED — in-process backends read `parent.session.header` for the working directory, the `parentSession` lineage, and the delegation depth. The three optional fields (`outputSchema`, `maxDepth`, `toolFilter`) each gate on the matching `SubagentCapabilities` flag. `outputSchema` is an object-rooted JSON Schema within the subset `assertSupportedOutputSchema` (dsh-tools) enforces — a schema outside it is rejected loud at start; the in-process backends realize it with a forced `structured_output` capture tool (see the [driver README](../../packages/subagent/subagent-inprocess/README.md)).
```ts type-equiv
interface SubagentStartRequest {
@@ -28,7 +28,7 @@ interface SubagentStartRequest {
parent: Agent
signal?: AbortSignal
agentOptions?: AgentOptions
outputSchema?: SchemaSpec
outputSchema?: StructuredOutputSchema
maxDepth?: number
toolFilter?: { allow?: string[]; deny?: string[] }
}

View File

@@ -104,6 +104,7 @@ graph TD
subagent-inprocess --> llm
subagent-inprocess --> session
subagent-inprocess --> subagent
subagent-inprocess --> tools
subagent-mock --> agent
subagent-mock --> llm
subagent-mock --> subagent
@@ -169,7 +170,7 @@ graph TD
| `agent-core` | `agent`, `agent-loop`, `invariants`, `llm`, `session`, `system-prompt`, `tool-bash`, `tools` |
| `hooks-claude` | `agent`, `hook-protocol`, `llm`, `session`, `subagent`, `tools` |
| `subagent-acp` | `agent`, `llm`, `subagent` |
| `subagent-inprocess` | `agent`, `llm`, `session`, `subagent` |
| `subagent-inprocess` | `agent`, `llm`, `session`, `subagent`, `tools` |
| `subagent-mock` | `agent`, `llm`, `subagent` |
| `tool-subagent` | `agent`, `llm`, `subagent`, `tools` |
| `acp-agent` | `acp`, `agent-core`, `app-boot`, `session-persistence-jsonl` |