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:
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user