docs: rewrite the agent-scope RFC
This commit is contained in:
@@ -12,7 +12,7 @@ The seam this rides on: `CreateAgentOptions.seed` (added on `dsh-agent`, threade
|
||||
|
||||
## Capabilities
|
||||
|
||||
`{ outputSchema: true, depthLimit: true, toolFilter: false }` — identical to spawn (the depth/model/structured-output behavior is the shared driver's).
|
||||
`{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }` — identical to spawn because the shared driver owns depth, model, persona, tool-filter, and structured-output behavior.
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ Runs a child as a child [`Agent`](../../core/agent) on the same cordis context (
|
||||
|
||||
`attachStructuredRuntime(childCtx, schema)` registers the run's whole enforcement surface as SCOPED registrations on the child's `agent.ctx` — riding the child's fiber (a backend hot-reload mid-run cannot unregister anything; a disposed child leaves no residue) and visible to that child alone (two concurrent structured runs never interact; no placeholder schema, no strip-for-everyone-else, no refcounted global state):
|
||||
|
||||
- the `structured_output` capture tool with the run's REAL schema as its registered `parameters`, validating each call (`validateStructuredValue`) — violations become an `INVALID_ARGS` isError the model retries in-turn; a valid call STAGES the value keyed by its call id;
|
||||
- the `structured_output` capture tool with the run's REAL schema as its registered `parameters`, validating each call (`validateStructuredValue`) — violations become an `INVALID_ARGS` isError the model retries in-turn; a valid call STAGES the value in a `WeakMap` keyed by that call's `ToolExecution` object;
|
||||
- the calling instruction as an ordinary order-190 scoped prompt section (the demand travels with the tool, as prompt state of exactly one agent);
|
||||
- a scoped `system-prompt/assemble` re-assert (`prepend: true` = outermost): whatever downstream listeners mutate or replace, the child's assembly always carries its capture tool and instruction — the loop logs the rendered assembly as the step's `request/header`, so the demand is reconstructable log state;
|
||||
- a scoped `tools/post-execute` COMMIT (`prepend: true`): the staged value becomes the run's result only when the final decision accepts THE SAME CALL that staged it — call-keyed, so a stale stage orphaned by an outer short-circuiting listener is dropped, never promoted on a later call's acceptance;
|
||||
- a scoped `system-prompt/assemble` re-assert (`prepend: true`) that post-processes its downstream chain, replacing conflicting entries with the child's capture tool and instruction — the loop logs the rendered assembly as the step's `request/header`, so the demand is reconstructable log state;
|
||||
- a scoped `tools/post-execute` COMMIT (`prepend: true`): the staged value becomes the run's result when that same execution's downstream post-execute decision accepts it. Execution-object identity prevents an orphaned stage from matching a later call even when an adapter reuses the call id;
|
||||
- a scoped `tools/pre-execute` deny for any call arriving after the capture — terminal means terminal WITHIN the step;
|
||||
- a scoped `agent/turn-continuation` veto (`prepend: true`) stopping the child's turn once its output is captured, so a successful capture doesn't buy a wasted extra model step.
|
||||
|
||||
|
||||
@@ -11,22 +11,21 @@
|
||||
* enforcement listeners fire only for this child (scope-filtered dispatch).
|
||||
* Registration lifetime rides the child's fiber, so a backend hot-reload
|
||||
* mid-run cannot unregister the capture tool out from under a live child, and
|
||||
* a disposed child leaves no residue — no placeholder schema, no
|
||||
* strip-for-everyone-else, no refcounted global runtime, no `WeakMap` state.
|
||||
* a disposed child leaves no residue — no placeholder schema,
|
||||
* strip-for-everyone-else pass, or refcounted global runtime.
|
||||
*
|
||||
* Four listeners enforce the contract:
|
||||
*
|
||||
* - `system-prompt/assemble` (prepend, scoped): FINAL-ASSEMBLY re-assert —
|
||||
* whatever downstream listeners mutated or replaced, the child's assembly
|
||||
* always carries its capture tool and the trailing instruction section. The
|
||||
* registry already contributes both; this outermost wrapper preserves the
|
||||
* guarantee against a (global) listener that strips or replaces the
|
||||
* assembly — placement-preserving: tools are replaced in place, the section
|
||||
* re-inserted at its ascending-order position, so the untampered path keeps
|
||||
* the registry's ordering (identical output, up to intra-band section order
|
||||
* — which carries no contract). The loop logs the rendered assembly as the
|
||||
* request header, so the demand is reconstructable log state, never a
|
||||
* wire-only mutation.
|
||||
* - `system-prompt/assemble` (prepend, scoped): assembly re-assert — the
|
||||
* listener post-processes its downstream chain so a listener inside that
|
||||
* chain cannot leave the child's capture tool or instruction stripped or
|
||||
* replaced. Tools are replaced in place and the section is re-inserted at
|
||||
* its ascending-order position, so the untampered path keeps the registry's
|
||||
* ordering (up to intra-band section order, which carries no contract). A
|
||||
* listener prepended later can still wrap and transform this result; this is
|
||||
* an ordinary waterfall listener, not a service-level finalizer. The loop
|
||||
* logs the rendered assembly as the request header, so the demand is
|
||||
* reconstructable log state, never a wire-only mutation.
|
||||
* - `agent/turn-continuation` (prepend, scoped): stop the child's turn once
|
||||
* its output is captured — the loop's default "had tool calls ⇒ continue"
|
||||
* would buy a wasted extra model step per structured child.
|
||||
@@ -36,8 +35,9 @@
|
||||
* effects after the final answer was accepted.
|
||||
* - `tools/post-execute` (prepend, scoped): the capture COMMIT. The tool body
|
||||
* only STAGES the validated value, KEYED BY THE EXECUTION OBJECT in a
|
||||
* WeakMap; it becomes the run's captured result only when the final
|
||||
* post-execute decision accepts THAT SAME pipeline trip. Execution-keyed
|
||||
* WeakMap; it becomes the run's captured result when this listener's
|
||||
* downstream post-execute decision accepts THAT SAME pipeline trip. A
|
||||
* later-prepended wrapper remains outside that decision. Execution-keyed
|
||||
* staging makes the stale-stage class structurally impossible: a value
|
||||
* orphaned by an outer short-circuiting listener (a post-execute block, or
|
||||
* a pre-execute deny whose call never dispatched) can never match another
|
||||
@@ -125,7 +125,7 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
|
||||
if (violations.length > 0) throw new ToolArgsError(violations)
|
||||
// Two-phase commit, KEYED BY THIS EXECUTION: the body only stages; the
|
||||
// post-execute listener promotes exactly this pipeline trip's entry
|
||||
// when the final decision accepts it.
|
||||
// when its downstream decision accepts it.
|
||||
staged.set(exec, { value: args })
|
||||
return Promise.resolve([{ type: 'text', text: 'Structured output recorded.' }])
|
||||
},
|
||||
@@ -137,10 +137,12 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
|
||||
text: STRUCTURED_OUTPUT_INSTRUCTION,
|
||||
})
|
||||
|
||||
// FINAL-ASSEMBLY re-assert (prepend = outermost): scoped dispatch means this
|
||||
// fires only for the child's assemblies; `await next()` returns whatever the
|
||||
// downstream chain (and any replacement assembly) produced, and the capture
|
||||
// tool + instruction are re-asserted onto it if anything stripped them.
|
||||
// PREPENDED assembly re-assert: scoped dispatch means this fires only for the
|
||||
// child's assemblies; `await next()` returns whatever this listener's
|
||||
// downstream chain produced, and the capture tool + instruction are
|
||||
// re-asserted onto it if anything stripped them. A listener prepended later
|
||||
// can still wrap and transform the returned assembly; this is not a
|
||||
// service-level finalizer.
|
||||
childCtx.on('system-prompt/assemble', async function (
|
||||
this: unknown, _assembly: PromptAssembly, _context: AssembleContext, next: () => Promise<PromptAssembly>,
|
||||
): Promise<PromptAssembly> {
|
||||
|
||||
@@ -10,7 +10,7 @@ The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../
|
||||
|
||||
## Capabilities
|
||||
|
||||
`{ outputSchema: true, depthLimit: true, toolFilter: false }`. It constructs the child, so it enforces a recursion cap, and it supports structured output via the driver's [structured runtime](../subagent-inprocess/README.md) (acquired per structured run inside the driver — this backend registers nothing at apply). Tool-scoping is deferred (the service rejects a request needing it before `start` runs).
|
||||
`{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`. It constructs the child, so it enforces a recursion cap and composes the child's persona, global-tool restriction, and [structured runtime](../subagent-inprocess/README.md) inside the agent-creation setup window. This backend registers nothing at apply.
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
* ({@link startInProcessRun}); this backend just passes NO seed (a fresh
|
||||
* child). The fork backend is an independent peer over the same driver.
|
||||
*
|
||||
* Structured output (`outputSchema`) is supported via the driver's shared
|
||||
* structured runtime: the backend acquires it for its plugin lifetime (so the
|
||||
* capture tool and request-shaping listeners exist before any run), and each
|
||||
* structured run holds its own acquisition until it settles.
|
||||
* Structured output (`outputSchema`) is supported through the driver's
|
||||
* per-child scoped runtime: the child registers its real-schema capture tool,
|
||||
* prompt instruction, and enforcement listeners inside the creation setup
|
||||
* window, and its scope owns their lifetime.
|
||||
*
|
||||
* Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default.
|
||||
*
|
||||
@@ -25,11 +25,10 @@ import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } fro
|
||||
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
|
||||
|
||||
export const name = 'subagent-spawn'
|
||||
// `tools` is deliberately NOT injected: the shared driver's structured runtime
|
||||
// (acquired per structured RUN, not at apply) gates its own capture-tool
|
||||
// registration on `tools` availability, so this backend's apply timing — and
|
||||
// with it the provider-mirroring delegation tool's position in the
|
||||
// model-visible tool list — stays what it was before structured output existed.
|
||||
// `tools` is deliberately NOT injected: the shared driver registers structured
|
||||
// output through the child's creation context, whose factory already requires
|
||||
// the tool service. Keeping it out of this backend's inject list preserves the
|
||||
// provider's independent apply timing.
|
||||
export const inject = ['subagents', 'agents']
|
||||
|
||||
/** Config: the registry name to register the provider under. */
|
||||
|
||||
@@ -14,7 +14,10 @@ The tool description and the `prompt` parameter description are DERIVED from the
|
||||
|---|---|
|
||||
| `provider` (required) | The `ctx.subagents` provider name to start runs on (`spawn`, `fork`, `acp`, …). |
|
||||
| `toolName` | The model-facing tool name to register (default `subagent`). Set a distinct value per load when exposing multiple providers, e.g. `subagent` + `subagent_acp`. |
|
||||
| `agentOptions` | Default per-child `{ model? }` applied to every spawned child. (No per-child persona: the deployment persona is a context-wide section every agent shares.) |
|
||||
| `agentOptions` | Default per-child `{ model? }` applied to every spawned child. |
|
||||
| `persona` | Per-child persona that shadows the deployment persona; requires the provider's `persona` capability. |
|
||||
| `toolFilter` | Per-child `{ allow?, deny? }` restriction over global tools; requires the provider's `toolFilter` capability. |
|
||||
| `maxDepth` | Maximum delegation depth; requires the provider's `depthLimit` capability. |
|
||||
|
||||
## Lifecycle (synchronous collect)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user