Merge remote-tracking branch 'origin/master' into worktree-dynamic-workflows

Beyond the mechanical conflicts (provider capability lines vs master's new
inheritsParentContext field; generated catalogs regenerated rather than
hand-merged; knip/lockfile), three master-side reworks required semantic
adaptation of this branch:

- The persona rework removed AgentOptions.systemPrompt, which was the
  structured-output instruction's channel. The instruction now rides the
  SAME final-request enforcement listener that injects the schema'd tool:
  appended per request to final.system (per-request wire state, not agent
  prompt state). Tests assert the wire request (adapter.requests) instead
  of child.options; the bare-direct-dispatch test pins the no-system arm.
- Tool guidance moved out of deployment prompts into per-tool prompt
  sections; the examples' workflow paragraph became a tool:<toolName>
  section contributed by dsh-tool-workflow (explicit-ask-only policy),
  and both example personas resolve to master's minimal identity+behavior
  form. tool-workflow gains inject: systemPrompt (+ peer dep, tsconfig
  ref); the export-shape guard updated.
- The uniform-RFC-format gate: the dynamic-workflows RFC restructured to
  the implemented/ skeleton (bare Status line; Proposal -> Decision;
  What-was-rejected -> Alternatives considered; new Consequences), and
  the overall-run-timeout deferral is now recorded in the RFC's Deferred
  list. The doc-graphs atlas classification gains the workflows seam
  (workflow-vm implementation, tool-workflow consumer).

Master's harness-identity section made "empty assembled prompt" states
unreachable through the loop, so the instruction-append is a plain
undefined-ternary and the structured tests assert append-not-replace.
All snapshot goldens (including workflow-run) replay unchanged. Full
local CI-equivalent gate sequence green on the merged tree.
This commit is contained in:
Tianyi Cui
2026-07-06 03:14:07 +08:00
244 changed files with 6025 additions and 1629 deletions

View File

@@ -187,23 +187,37 @@ describe('in-process structured output', () => {
expect(ctx.agents.get(AgentId('parent'))).toBeDefined()
})
it('appends the structured instruction to the child system prompt (caller prompt preserved)', async () => {
const { ctx, parent } = await setup([toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 })])
const run = ctx.subagents.start('spawn', structuredRequest(parent, {
agentOptions: { systemPrompt: 'You are a counter.' },
}))
it('appends the structured instruction to the child REQUEST\'s system text (base prompt preserved)', async () => {
const { ctx, parent, adapter } = await setup([toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 })])
// A context-wide section stands in for the deployment persona: the
// instruction must APPEND to whatever the prompt pipeline assembled, not
// replace it (AgentOptions has no prompt field — the instruction is
// per-request wire state added by the final-request listener).
ctx.systemPrompt.section({ name: 'test:persona', order: 10, text: 'You are a counter.' })
const run = ctx.subagents.start('spawn', structuredRequest(parent))
await run.result
const child = ctx.agents.get(run.id)!
expect(child.options.systemPrompt).toBe(`You are a counter.\n\n${STRUCTURED_OUTPUT_INSTRUCTION}`)
const childRequest = adapter.requests.at(-1)!
expect(childRequest.system).toContain('You are a counter.')
expect(childRequest.system!.endsWith(STRUCTURED_OUTPUT_INSTRUCTION)).toBe(true)
expect(childRequest.system!.indexOf(STRUCTURED_OUTPUT_INSTRUCTION)).toBeGreaterThan(0)
await run.dispose()
})
it('a structured child WITHOUT a caller prompt gets exactly the instruction', async () => {
const { ctx, parent } = await setup([toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 })])
it('the instruction rides ONLY structured requests: appended for the child, absent for a plain agent', async () => {
const { ctx, parent, adapter } = await setup([
textResponse('parent answer'),
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 }),
])
parent.send([{ type: 'text', text: 'hello' }])
await parent.whenIdle()
expect(adapter.requests[0]!.system ?? '').not.toContain(STRUCTURED_OUTPUT_INSTRUCTION)
const run = ctx.subagents.start('spawn', structuredRequest(parent))
await run.result
const child = ctx.agents.get(run.id)!
expect(child.options.systemPrompt).toBe(STRUCTURED_OUTPUT_INSTRUCTION)
// The loop always assembles a base prompt (the harness identity section),
// so the instruction APPENDS — never replaces.
const childSystem = adapter.requests.at(-1)!.system!
expect(childSystem.endsWith(STRUCTURED_OUTPUT_INSTRUCTION)).toBe(true)
expect(childSystem.length).toBeGreaterThan(STRUCTURED_OUTPUT_INSTRUCTION.length)
await run.dispose()
})
@@ -314,6 +328,8 @@ describe('in-process structured output', () => {
const bare2: GenerateOptions = { model: 'mock', messages: [] }
const shaped = await ctx.waterfall('agent/request', parent, 1, 1, bare2, () => Promise.resolve(bare2))
expect(shaped.tools!.map(tool => tool.name)).toEqual([STRUCTURED_OUTPUT_TOOL])
// A bare request carries no system text: the instruction IS the system.
expect(shaped.system).toBe(STRUCTURED_OUTPUT_INSTRUCTION)
acquisition.detach(parent)
acquisition.release()
})