Merge latest master into subagent policy inheritance
Retarget the feature branch to the current master tip without rewriting its existing review history. Keeping this as a dedicated merge checkpoint makes the later simplification diff attributable to the stacked child rather than mixing base movement with design changes. Resolve the identified-message API drift in the feature tests by constructing complete user messages, reading the nested tool-result message shape, and adapting the prompt-submit listener signature. Preserve both sides of the user-approval conflict: master’s createUserMessage wrapper and the feature’s inherited-policy attribution. Regenerate the Cordis and persistence catalogs, re-record the session README pair, and refresh the affected ACP/headless fixtures so derived artifacts describe the merged source rather than either parent in isolation. Validated with the focused policy/session/persistence/query suites (430 tests), focused ACP/headless snapshots (3 tests), build, doc-sync (25 gates), lint, hygiene, and git diff checks.
This commit is contained in:
@@ -64,7 +64,7 @@ describe('ACP subagent cwd inheritance through a real cordis.yml', () => {
|
||||
// session's workspace, never the harness process's launch directory.
|
||||
const results = events.filter(event => event.type === 'tool/result')
|
||||
expect(results).toHaveLength(1)
|
||||
const resultText = results[0]!.data.content
|
||||
const resultText = results[0]!.data.message.content[0].content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
|
||||
@@ -173,7 +173,7 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
|
||||
if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {
|
||||
partial.push(event.data.chunk.text)
|
||||
} else if (event.type === 'assistant/message') {
|
||||
lastMessage = event.data.content
|
||||
lastMessage = event.data.message.content
|
||||
}
|
||||
}
|
||||
const collectOutput = (): ContentBlock[] => {
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('SDK subagent cwd inheritance through a real cordis.yml', () => {
|
||||
// process's launch directory.
|
||||
const results = events.filter(event => event.type === 'tool/result')
|
||||
expect(results).toHaveLength(1)
|
||||
const resultText = results[0]!.data.content
|
||||
const resultText = results[0]!.data.message.content[0].content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -64,7 +65,7 @@ describe('multi-subagent coexistence (spawn + fork on one context)', () => {
|
||||
])
|
||||
|
||||
// Parent does one real turn first, so the fork has a completed turn to seed.
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent q1' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent q1' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const parentPrefixLen = parent.session.events.length
|
||||
|
||||
@@ -93,10 +94,10 @@ describe('multi-subagent coexistence (spawn + fork on one context)', () => {
|
||||
await forkRun.dispose()
|
||||
|
||||
// The parent is unaffected and keeps working after both delegations.
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent q2' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent q2' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const lastParentMessage = parent.session.events.findLast(e => e.type === 'assistant/message')
|
||||
expect(lastParentMessage?.type === 'assistant/message' && text(lastParentMessage.data.content)).toBe('parent turn two')
|
||||
expect(lastParentMessage?.type === 'assistant/message' && text(lastParentMessage.data.message.content)).toBe('parent turn two')
|
||||
// The parent's OWN log never recorded the children's internal steps — its
|
||||
// only subagent-related entries would be tool/call+tool/result IF it had
|
||||
// used the tool, but here we called the service directly, so the parent log
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
@@ -89,9 +90,9 @@ describe('dsh-subagent-fork', () => {
|
||||
|
||||
it('seeds every completed parent turn through the last turn/end', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('first'), textResponse('second'), textResponse('child')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
parent.followup({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const parentPrefixLen = parent.session.events.length
|
||||
|
||||
@@ -108,7 +109,7 @@ describe('dsh-subagent-fork', () => {
|
||||
// Parent runs one turn, then we fork. The child's seeded log should contain
|
||||
// the parent's first turn, and the child should run its own new turn on top.
|
||||
const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const parentPrefixLen = parent.session.events.length
|
||||
|
||||
@@ -137,10 +138,10 @@ describe('dsh-subagent-fork', () => {
|
||||
// open (a hanging model call), and fork while it's in flight. The seed must stop after the
|
||||
// balanced first turn; including the open turn would fail invariant replay during start.
|
||||
const { ctx, parent } = await setup([textResponse('done'), 'hang', textResponse('child')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
// Start a second turn that hangs (open turn/start + open step, never ends).
|
||||
parent.followup({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } }))
|
||||
await new Promise(r => setTimeout(r, 20)) // let the hanging turn open
|
||||
|
||||
// Forking now must NOT throw (the open second turn is excluded from the seed).
|
||||
@@ -164,7 +165,7 @@ describe('dsh-subagent-fork', () => {
|
||||
textResponse('parent turn'),
|
||||
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 9 }),
|
||||
])
|
||||
parent.followup({ content: [{ type: 'text', text: 'warm up' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'warm up' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const run = await start(ctx, 'fork', {
|
||||
prompt: [{ type: 'text', text: 'report structured' }],
|
||||
@@ -183,7 +184,7 @@ describe('dsh-subagent-fork', () => {
|
||||
// `readResult` must scan only child-owned events after the seed. The child emits no assistant
|
||||
// message, so scanning the whole log would incorrectly return the parent's distinctive text.
|
||||
const { ctx, parent } = await setup([textResponse('parent stale'), emptyStop])
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await start(ctx, 'fork', { prompt: [{ type: 'text', text: 'child question' }], parent })
|
||||
|
||||
@@ -11,7 +11,7 @@ import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { findLastMessageTurnEnd, SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { assertSubagentMaxDepth, delegationDepthOf } from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
|
||||
// Type-only: make `ctx.get('sandboxPolicy')` / `ctx.get('approval')` resolve
|
||||
@@ -162,7 +162,7 @@ export async function startInProcessRun(
|
||||
|
||||
const result: Promise<SubagentResult> = (async () => {
|
||||
try {
|
||||
child.followup({ content: request.prompt, source: { kind: 'user' } })
|
||||
child.followup(createUserMessage({ content: request.prompt, source: { kind: 'user' } }))
|
||||
await child.whenIdle()
|
||||
return readResult(
|
||||
child,
|
||||
@@ -197,7 +197,7 @@ function readResult(
|
||||
const own = child.session.events.slice(seedLength)
|
||||
const lastMessage = own.findLast((event): event is SessionEvent<'assistant/message'> => event.type === 'assistant/message')
|
||||
const lastEnd = findLastMessageTurnEnd(own)
|
||||
const output: ContentBlock[] = lastMessage?.data.content ?? []
|
||||
const output: ContentBlock[] = lastMessage?.data.message.content ?? []
|
||||
const recorded = toStopReason(lastEnd?.data.reason)
|
||||
// Disposal can tear the owner down before the loop records its ordinary
|
||||
// `aborted` end, yielding `disposed` instead. A requested cancellation owns
|
||||
|
||||
@@ -31,7 +31,7 @@ import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
|
||||
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
|
||||
import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
@@ -150,7 +150,11 @@ function registerDelegate(ctx: Context, captured: Agent[], raceSwitch?: 'danger-
|
||||
function toolResultTexts(agent: Agent): string[] {
|
||||
return agent.session.events
|
||||
.filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
.map(e => e.data.content.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text').map(b => b.text).join(''))
|
||||
.map(e => e.data.message.content
|
||||
.flatMap(block => block.content)
|
||||
.filter((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join(''))
|
||||
}
|
||||
|
||||
/** Count the policy-override events in a session log. */
|
||||
@@ -186,7 +190,7 @@ describe('sandbox-mode inheritance against the real fs fence', () => {
|
||||
toolCallResponse('c-write', 'write', { file_path: blocked, content: 'escaped' }),
|
||||
textResponse('child done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage the session policy' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage the session policy' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const parentLogLength = parent.session.events.length
|
||||
|
||||
@@ -229,7 +233,7 @@ describe('sandbox-mode inheritance against the real fs fence', () => {
|
||||
},
|
||||
textResponse('child done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
@@ -264,9 +268,9 @@ describe('sandbox-mode inheritance against the real fs fence', () => {
|
||||
textResponse('fork child done'),
|
||||
textResponse('turn two done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'turn one' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'turn one' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
parent.followup({ content: [{ type: 'text', text: 'turn two: delegate' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'turn two: delegate' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const child = captured[0] as Agent
|
||||
@@ -297,9 +301,9 @@ describe('sandbox-mode inheritance against the real fs fence', () => {
|
||||
textResponse('race child done'),
|
||||
textResponse('turn two done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
parent.followup({ content: [{ type: 'text', text: 'delegate' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'delegate' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const child = captured[0] as Agent
|
||||
@@ -329,9 +333,9 @@ describe('sandbox-mode inheritance against the real fs fence', () => {
|
||||
textResponse('child done'),
|
||||
textResponse('parent done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
parent.followup({ content: [{ type: 'text', text: 'delegate twice' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'delegate twice' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
expect(captured).toHaveLength(2)
|
||||
@@ -350,7 +354,7 @@ describe('inheritance survives prompt vetoes', () => {
|
||||
// A veto-capable listener registered BEFORE the child exists — the
|
||||
// Claude/Codex UserPromptSubmit hook shape: it blocks the child's prompt
|
||||
// and never delegates. Inheritance must still run for the first turn.
|
||||
ctx.on('agent/prompt-submit', (agent, _content, _source, _signal, next) => {
|
||||
ctx.on('agent/prompt-submit', (agent, _message, _signal, next) => {
|
||||
if (agent.session.header.parentSession !== undefined) {
|
||||
return Promise.resolve({ kind: 'block' as const, reason: 'vetoed by test hook' })
|
||||
}
|
||||
@@ -363,7 +367,7 @@ describe('inheritance survives prompt vetoes', () => {
|
||||
},
|
||||
// No child model entries: the blocked prompt closes a zero-step turn.
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
@@ -432,7 +436,7 @@ describe('what a blocked child experiences', () => {
|
||||
},
|
||||
textResponse('child done'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
@@ -468,7 +472,7 @@ describe('what a blocked child experiences', () => {
|
||||
}),
|
||||
textResponse('child gave up'),
|
||||
)
|
||||
parent.followup({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'stage' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId, type ContentBlock, type GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import { createUserMessage, CallId, type ContentBlock, type GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
@@ -185,8 +185,8 @@ describe('in-process structured output', () => {
|
||||
expect(sideEffectRan).toBe(false)
|
||||
const child = ctx.agents.get(run.id)
|
||||
const sideEffectResult = child?.session.events.find(event =>
|
||||
event.type === 'tool/result' && event.data.callId === 'c2')
|
||||
expect(sideEffectResult?.type === 'tool/result' && sideEffectResult.data.isError).toBe(true)
|
||||
event.type === 'tool/result' && event.data.message.source.callId === 'c2')
|
||||
expect(sideEffectResult?.type === 'tool/result' && sideEffectResult.data.message.content[0].isError).toBe(true)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -230,7 +230,7 @@ describe('in-process structured output', () => {
|
||||
const child = ctx.agents.get(run.id)!
|
||||
const results = child.session.events.filter(e => e.type === 'tool/result')
|
||||
expect(results.length).toBe(2)
|
||||
expect((results[0]!.data as { isError?: boolean }).isError).toBe(true)
|
||||
expect(results[0]!.data.message.content[0].isError).toBe(true)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -312,8 +312,8 @@ describe('in-process structured output', () => {
|
||||
// ...the logged tool result is the blocked isError with the feedback...
|
||||
const child = ctx.agents.get(run.id)!
|
||||
const results = child.session.events.filter(e => e.type === 'tool/result')
|
||||
expect((results[0]!.data as { isError?: boolean }).isError).toBe(true)
|
||||
expect(JSON.stringify((results[0]!.data as { content: unknown }).content)).toContain('capture rejected by hook')
|
||||
expect(results[0]!.data.message.content[0].isError).toBe(true)
|
||||
expect(JSON.stringify(results[0]!.data.message.content)).toContain('capture rejected by hook')
|
||||
// ...and the turn CONTINUED past the blocked call (no captured veto):
|
||||
// the model got to react to the failure with a second step.
|
||||
expect(adapter.requests.length).toBe(2)
|
||||
@@ -357,8 +357,8 @@ describe('in-process structured output', () => {
|
||||
expect(result.stopReason).toBe('error')
|
||||
const child = ctx.agents.get(run.id)
|
||||
const captureResult = child?.session.events.find(event =>
|
||||
event.type === 'tool/result' && event.data.callId === 'c1')
|
||||
expect(captureResult?.type === 'tool/result' && captureResult.data.isError).toBe(true)
|
||||
event.type === 'tool/result' && event.data.message.source.callId === 'c1')
|
||||
expect(captureResult?.type === 'tool/result' && captureResult.data.message.content[0].isError).toBe(true)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -428,8 +428,8 @@ describe('in-process structured output', () => {
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
const child = ctx.agents.get(run.id)!
|
||||
const outer = child.session.events.find(event =>
|
||||
event.type === 'tool/result' && event.data.callId === CallId('c1'))
|
||||
expect(outer?.type === 'tool/result' && outer.data.isError).toBe(true)
|
||||
event.type === 'tool/result' && event.data.message.source.callId === CallId('c1'))
|
||||
expect(outer?.type === 'tool/result' && outer.data.message.content[0].isError).toBe(true)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -463,7 +463,7 @@ describe('in-process structured output', () => {
|
||||
textResponse('parent answer'),
|
||||
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 }),
|
||||
])
|
||||
parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
expect(adapter.requests[0]!.system ?? '').not.toContain(STRUCTURED_OUTPUT_INSTRUCTION)
|
||||
const run = await ctx.subagents.start('spawn', structuredRequest(parent))
|
||||
@@ -479,7 +479,7 @@ describe('in-process structured output', () => {
|
||||
describe('scoped registration (each child owns its capture tool)', () => {
|
||||
it('a plain agent never sees the tool: nothing is registered globally at all', async () => {
|
||||
const { ctx, parent, adapter } = await setup([textResponse('parent answer')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
// Scoped registration: the global view has no capture tool, ever.
|
||||
expect(ctx.tools.get(STRUCTURED_OUTPUT_TOOL)).toBeUndefined()
|
||||
@@ -493,7 +493,7 @@ describe('in-process structured output', () => {
|
||||
// Child turn: must see it, with the run's schema.
|
||||
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 42 }),
|
||||
])
|
||||
parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
expect(toolNames(adapter.requests[0]!)).not.toContain(STRUCTURED_OUTPUT_TOOL)
|
||||
|
||||
@@ -571,7 +571,7 @@ describe('in-process structured output', () => {
|
||||
|
||||
it('a non-structured agent request keeps tools ABSENT when it had none (no tools: [] materialized)', async () => {
|
||||
const { parent, adapter } = await setup([textResponse('plain')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const request = adapter.requests[0]!
|
||||
expect(request.tools).toBeUndefined()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent'
|
||||
@@ -68,10 +69,10 @@ describe('startInProcessRun', () => {
|
||||
turn,
|
||||
trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'late-metadata' } },
|
||||
})
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'late metadata' }],
|
||||
source: { kind: 'plugin', plugin: 'late-metadata' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
})
|
||||
|
||||
@@ -88,7 +89,7 @@ describe('startInProcessRun', () => {
|
||||
|
||||
it('seeds a forked child but reads only the child-owned output', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const seed = parent.session.events.slice()
|
||||
const run = await startInProcessRun(request(parent), { seed })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -24,10 +25,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('spawn backend with-key smoke', (
|
||||
ctx = await spawnHarness(workdir)
|
||||
const parent = ctx.agentLoop.create(SessionId('e2e-parent'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
parent.followup({ content: [{ type: 'text', text:
|
||||
parent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text:
|
||||
'Use the subagent tool to delegate this exact task: "Use the bash tool to write the text '
|
||||
+ 'SUBAGENT_WAS_HERE into a file named proof.txt in the current directory." '
|
||||
+ 'After the subagent finishes, tell me it is done.' }], source: { kind: 'user' } })
|
||||
+ 'After the subagent finishes, tell me it is done.' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, parent)
|
||||
|
||||
// Assert the filesystem effect independently of the model response.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context, symbols, type EffectMeta } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
@@ -106,7 +107,7 @@ describe('dsh-subagent-spawn', () => {
|
||||
it('a fresh child does NOT inherit the parent conversation (its log starts empty before the prompt)', async () => {
|
||||
// Drive the parent through one real turn so it has history, THEN spawn.
|
||||
const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('child sees nothing')])
|
||||
parent.followup({ content: [{ type: 'text', text: 'parent prompt' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent prompt' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const parentEventCount = parent.session.events.length
|
||||
expect(parentEventCount).toBeGreaterThan(0)
|
||||
@@ -383,7 +384,7 @@ describe('dsh-subagent-spawn', () => {
|
||||
textResponse('parent answer'),
|
||||
textResponse('child answer'),
|
||||
])
|
||||
parent.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } })
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
|
||||
const run = await start(ctx, 'spawn', {
|
||||
|
||||
Reference in New Issue
Block a user