From 4641f1a851b4579adb3cd7ccd50ba53b4102ce04 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:26:47 +0800 Subject: [PATCH] chore: absorb the no-nudge seam into the workflow tree The parent branch dropped structuredNudgeRetries; the two workflow-side test setups stop passing it, and the RFC's foundation paragraph now describes the current design (final-ASSEMBLY enforcement logged via request/header, the post-capture pre-execute deny, the start() schema snapshot, no re-prompt) instead of the retired agent/request + nudge shape. --- docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md | 2 +- packages/workflow/workflow-vm/tests/integration.spec.ts | 2 +- packages/workflow/workflow-vm/tests/workflow.e2e.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md index 17c26760be..7bf09f742c 100644 --- a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md +++ b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md @@ -36,7 +36,7 @@ A `workflow` tool mirroring `dsh-tool-subagent`'s synchronous shape: start, awai ### The foundation: structured output on the subagent seam -`agent({schema})` needs `SubagentStartRequest.outputSchema` to actually work; it was vocabulary without an implementation (`outputSchema: false` everywhere). Implemented in `dsh-subagent-inprocess` for both in-process backends: a globally registered `structured_output` capture tool whose per-child schema is enforced by a `prepend: true` `agent/request` listener doing FINAL-REQUEST enforcement (post-processing `await next()` — cooperative mutation would not survive a downstream listener returning a replacement request; the listener also appends the calling instruction to the request's `system` text, since `AgentOptions` carries no per-agent prompt field), a `prepend: true` `agent/turn-continuation` veto after capture (no wasted extra model step, and an earlier-registered force-continue listener cannot short-circuit it), validation-retry in-turn via `ToolArgsError`, and a clean-finish nudge loop (`structuredNudgeRetries`). Lifetime is refcounted by backends (plugin lifetime) AND live runs (start → settle). The seam's `outputSchema` type became the raw JSON-Schema SUBSET (`StructuredOutputSchema` in dsh-tools: single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`; anything unenforced is rejected loud) — the schema travels verbatim to the model as the forced tool's parameters, so the wire format, not the author DSL, is the right vocabulary. +`agent({schema})` needs `SubagentStartRequest.outputSchema` to actually work; it was vocabulary without an implementation (`outputSchema: false` everywhere). Implemented in `dsh-subagent-inprocess` for both in-process backends: a globally registered `structured_output` capture tool whose per-child schema is enforced by a `prepend: true` `system-prompt/assemble` listener doing FINAL-ASSEMBLY enforcement (post-processing `await next()` — cooperative mutation would not survive a downstream listener returning a replacement assembly; the calling instruction rides as a trailing prompt section, since `AgentOptions` carries no per-agent prompt field, and the loop logs the result as the step's `request/header`, keeping the injection reconstructable), a `prepend: true` `agent/turn-continuation` veto after capture (no wasted extra model step) plus a `tools/pre-execute` deny for calls arriving after the capture (terminal within the step, not only at its end), and validation-retry in-turn via `ToolArgsError`. The schema is `structuredClone`d at `start()` (caller mutation cannot drift enforcement). Deliberately NO re-prompt: a child that finishes cleanly without calling the tool settles `error` to the parent. Lifetime is refcounted by backends (plugin lifetime) AND live runs (start → settle). The seam's `outputSchema` type became the raw JSON-Schema SUBSET (`StructuredOutputSchema` in dsh-tools: single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`; anything unenforced is rejected loud) — the schema travels verbatim to the model as the forced tool's parameters, so the wire format, not the author DSL, is the right vocabulary. ## Deferred (documented non-goals of this cut) diff --git a/packages/workflow/workflow-vm/tests/integration.spec.ts b/packages/workflow/workflow-vm/tests/integration.spec.ts index 48b308a986..3572131a8a 100644 --- a/packages/workflow/workflow-vm/tests/integration.spec.ts +++ b/packages/workflow/workflow-vm/tests/integration.spec.ts @@ -32,7 +32,7 @@ async function setup(script: Script) { await ctx.plugin(Invariants) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) - await ctx.plugin(spawn, { providerName: 'spawn', structuredNudgeRetries: 1 }) + await ctx.plugin(spawn, { providerName: 'spawn' }) await ctx.plugin(VmWorkflowEngine, {}) ctx.llm.registerAdapter(['mock'], adapter) const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' }) diff --git a/packages/workflow/workflow-vm/tests/workflow.e2e.ts b/packages/workflow/workflow-vm/tests/workflow.e2e.ts index 6ddfdb7251..c3f959d072 100644 --- a/packages/workflow/workflow-vm/tests/workflow.e2e.ts +++ b/packages/workflow/workflow-vm/tests/workflow.e2e.ts @@ -39,7 +39,7 @@ async function harness(): Promise { await built.plugin(AgentLoop, { agents: [] }) await built.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] }) await built.plugin(SubagentService) - await built.plugin(Spawn, { providerName: 'spawn', structuredNudgeRetries: 1 }) + await built.plugin(Spawn, { providerName: 'spawn' }) await built.plugin(VmWorkflowEngine, { provider: 'spawn' }) await built.plugin(ToolWorkflow, {}) return built