|
@@ -85,7 +85,11 @@ describe('dsh-tool-subagent', () => {
|
|
|
|
|
|
|
|
it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => {
|
|
it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => {
|
|
|
const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' })
|
|
const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' })
|
|
|
- const result = await callSubagent(ctx, { description: 'do a thing', prompt: 'go research X' })
|
|
|
|
|
|
|
+ const result = await callSubagent(ctx, {
|
|
|
|
|
+ description: 'do a thing',
|
|
|
|
|
+ prompt: 'go research X',
|
|
|
|
|
+ run_in_background: false,
|
|
|
|
|
+ })
|
|
|
expect(result.isError).toBe(false)
|
|
expect(result.isError).toBe(false)
|
|
|
if (result.isError) throw new Error('expected subagent success')
|
|
if (result.isError) throw new Error('expected subagent success')
|
|
|
expect(result.value).toEqual({
|
|
expect(result.value).toEqual({
|
|
@@ -814,7 +818,7 @@ describe('dsh-tool-subagent background mode', () => {
|
|
|
agent: parent,
|
|
agent: parent,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- expect(text(started)).toBe('started background subagent task subagent-1')
|
|
|
|
|
|
|
+ expect(text(started)).toBe('started background subagent job subagent-1')
|
|
|
expect(prepareCalls).toBe(0)
|
|
expect(prepareCalls).toBe(0)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -826,7 +830,7 @@ describe('dsh-tool-subagent background mode', () => {
|
|
|
expect(start.isError).toBe(false)
|
|
expect(start.isError).toBe(false)
|
|
|
if (start.isError) throw new Error('expected background subagent success')
|
|
if (start.isError) throw new Error('expected background subagent success')
|
|
|
expect(start.value).toEqual({ kind: 'background', jobId: 'subagent-1' })
|
|
expect(start.value).toEqual({ kind: 'background', jobId: 'subagent-1' })
|
|
|
- expect(text(start)).toBe('started background subagent task subagent-1')
|
|
|
|
|
|
|
+ expect(text(start)).toBe('started background subagent job subagent-1')
|
|
|
|
|
|
|
|
const collected = await ctx.tools.execute({
|
|
const collected = await ctx.tools.execute({
|
|
|
signal: testToolSignal,
|
|
signal: testToolSignal,
|
|
@@ -887,7 +891,7 @@ describe('dsh-tool-subagent background mode', () => {
|
|
|
arguments: { description: 'broken', prompt: 'p', run_in_background: true },
|
|
arguments: { description: 'broken', prompt: 'p', run_in_background: true },
|
|
|
agent: parent,
|
|
agent: parent,
|
|
|
})
|
|
})
|
|
|
- expect(text(started)).toBe('started background subagent task subagent-1')
|
|
|
|
|
|
|
+ expect(text(started)).toBe('started background subagent job subagent-1')
|
|
|
const output = await ctx.tools.execute({
|
|
const output = await ctx.tools.execute({
|
|
|
signal: testToolSignal,
|
|
signal: testToolSignal,
|
|
|
callId: CallId('broken-output'),
|
|
callId: CallId('broken-output'),
|
|
@@ -935,6 +939,48 @@ describe('dsh-tool-subagent background mode', () => {
|
|
|
expect(text(output)).toBe('(no new output)\n[status: killed]')
|
|
expect(text(output)).toBe('(no new output)\n[status: killed]')
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ it('reports startup rollback failure after cancellation as a failed job', async () => {
|
|
|
|
|
+ const ctx = await backgroundSetup({ provider: 'mock' })
|
|
|
|
|
+ const parent = ownerAgent(ctx, 'sess-parent')
|
|
|
|
|
+ ctx.subagents.registerProvider({
|
|
|
|
|
+ name: 'broken-start-rollback',
|
|
|
|
|
+ capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
|
|
|
|
|
+ inheritsParentContext: false,
|
|
|
|
|
+ start: request => new Promise((_resolve, reject) => {
|
|
|
|
|
+ request.signal.addEventListener('abort', () => {
|
|
|
|
|
+ reject(new AggregateError(
|
|
|
|
|
+ [new Error('startup aborted'), new Error('cleanup failed')],
|
|
|
|
|
+ 'startup failed and cleanup also failed',
|
|
|
|
|
+ ))
|
|
|
|
|
+ }, { once: true })
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+ tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
|
|
|
|
|
+
|
|
|
|
|
+ await ctx.tools.execute({
|
|
|
|
|
+ signal: testToolSignal,
|
|
|
|
|
+ callId: CallId('broken-rollback-start'),
|
|
|
|
|
+ name: 'subagent_broken_rollback',
|
|
|
|
|
+ arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true },
|
|
|
|
|
+ agent: parent,
|
|
|
|
|
+ })
|
|
|
|
|
+ await ctx.tools.execute({
|
|
|
|
|
+ signal: testToolSignal,
|
|
|
|
|
+ callId: CallId('broken-rollback-kill'),
|
|
|
|
|
+ name: 'job_kill',
|
|
|
|
|
+ arguments: { job_id: 'subagent-1' },
|
|
|
|
|
+ agent: parent,
|
|
|
|
|
+ })
|
|
|
|
|
+ const output = await ctx.tools.execute({
|
|
|
|
|
+ signal: testToolSignal,
|
|
|
|
|
+ callId: CallId('broken-rollback-output'),
|
|
|
|
|
+ name: 'job_output',
|
|
|
|
|
+ arguments: { job_id: 'subagent-1', wait: true },
|
|
|
|
|
+ agent: parent,
|
|
|
|
|
+ })
|
|
|
|
|
+ expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
|
|
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
|
|
|
// Use a provider that remains live until its signal is aborted.
|
|
// Use a provider that remains live until its signal is aborted.
|
|
|
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
|
|
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
|
|
@@ -966,8 +1012,8 @@ describe('dsh-tool-subagent background mode', () => {
|
|
|
|
|
|
|
|
const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
|
|
const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
|
|
|
const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
|
|
const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
|
|
|
- expect(text(startOne)).toBe('started background subagent task subagent-1')
|
|
|
|
|
- expect(text(startTwo)).toBe('started background subagent task subagent-2')
|
|
|
|
|
|
|
+ expect(text(startOne)).toBe('started background subagent job subagent-1')
|
|
|
|
|
+ expect(text(startTwo)).toBe('started background subagent job subagent-2')
|
|
|
|
|
|
|
|
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent })
|
|
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent })
|
|
|
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent })
|
|
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent })
|