@namzu/sdk 6.2.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +677 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +5 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +172 -158
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
- package/dist/agents/runAgent.d.ts +19 -1
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +2 -0
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +4 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +24 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js +142 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js.map +1 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +123 -8
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/constants/agent/index.d.ts +5 -0
- package/dist/constants/agent/index.d.ts.map +1 -1
- package/dist/constants/agent/index.js +5 -0
- package/dist/constants/agent/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +15 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +15 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
- package/dist/gateway/completion-inbox.d.ts +94 -6
- package/dist/gateway/completion-inbox.d.ts.map +1 -1
- package/dist/gateway/completion-inbox.js +235 -15
- package/dist/gateway/completion-inbox.js.map +1 -1
- package/dist/gateway/local.d.ts +11 -0
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +27 -1
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +6 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/provider/thinking-support.d.ts +2 -1
- package/dist/provider/thinking-support.d.ts.map +1 -1
- package/dist/provider/thinking-support.js +14 -0
- package/dist/provider/thinking-support.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +9 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +11 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
- package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/context.test.js +18 -0
- package/dist/runtime/query/__tests__/context.test.js.map +1 -1
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js +118 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js +34 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js.map +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +16 -1
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +11 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/guard.d.ts +28 -0
- package/dist/runtime/query/guard.d.ts.map +1 -1
- package/dist/runtime/query/guard.js +31 -0
- package/dist/runtime/query/guard.js.map +1 -1
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +92 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +818 -565
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +61 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/telemetry/__tests__/model-call-span.test.js +22 -4
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -1
- package/dist/telemetry/__tests__/span-closure.test.js +12 -5
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -1
- package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
- package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js +193 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +19 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +191 -71
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts +66 -0
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts.map +1 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js +78 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +9 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/agent/base.d.ts +16 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/gateway.d.ts +41 -0
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/agent/lifecycle-event.d.ts +9 -1
- package/dist/types/agent/lifecycle-event.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +5 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +10 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/probe/registry.d.ts +6 -0
- package/dist/types/probe/registry.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +35 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +25 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/entity.d.ts +16 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +75 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/prepare-step.d.ts +17 -2
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/verification/index.d.ts +98 -0
- package/dist/types/verification/index.d.ts.map +1 -1
- package/dist/types/verification/index.js +10 -0
- package/dist/types/verification/index.js.map +1 -1
- package/dist/utils/__tests__/abort-reason.test.d.ts +2 -0
- package/dist/utils/__tests__/abort-reason.test.d.ts.map +1 -0
- package/dist/utils/__tests__/abort-reason.test.js +48 -0
- package/dist/utils/__tests__/abort-reason.test.js.map +1 -0
- package/dist/utils/abort.d.ts +26 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +34 -0
- package/dist/utils/abort.js.map +1 -1
- package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
- package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
- package/dist/verification/__tests__/argument-pattern.test.js +122 -0
- package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.d.ts +2 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.d.ts.map +1 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js +126 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js.map +1 -0
- package/dist/verification/gate.d.ts +17 -1
- package/dist/verification/gate.d.ts.map +1 -1
- package/dist/verification/gate.js +102 -2
- package/dist/verification/gate.js.map +1 -1
- package/dist/verification/index.d.ts +1 -1
- package/dist/verification/index.d.ts.map +1 -1
- package/dist/verification/index.js +1 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/verification/rules.d.ts.map +1 -1
- package/dist/verification/rules.js +27 -0
- package/dist/verification/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +175 -162
- package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
- package/src/agents/runAgent.ts +22 -1
- package/src/bridge/a2a/mapper.ts +4 -0
- package/src/bridge/sse/mapper.ts +25 -0
- package/src/connector/mcp/__tests__/positional-arrays.test.ts +183 -0
- package/src/connector/mcp/adapter.ts +131 -7
- package/src/constants/agent/index.ts +5 -0
- package/src/constants/plugin/index.ts +15 -0
- package/src/contracts/api.ts +1 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
- package/src/gateway/completion-inbox.ts +248 -16
- package/src/gateway/local.ts +26 -1
- package/src/manager/agent/lifecycle.ts +6 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/provider/thinking-support.ts +19 -2
- package/src/public-runtime.ts +9 -0
- package/src/run/reporter.ts +12 -0
- package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
- package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
- package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
- package/src/runtime/query/__tests__/context.test.ts +24 -0
- package/src/runtime/query/__tests__/effort-reaches-the-wire.test.ts +135 -0
- package/src/runtime/query/__tests__/tool-timeout.test.ts +38 -0
- package/src/runtime/query/context.ts +16 -1
- package/src/runtime/query/executor.ts +11 -1
- package/src/runtime/query/guard.ts +32 -0
- package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
- package/src/runtime/query/iteration/index.ts +906 -635
- package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
- package/src/runtime/query/iteration/phases/compaction.ts +83 -10
- package/src/telemetry/__tests__/model-call-span.test.ts +22 -5
- package/src/telemetry/__tests__/span-closure.test.ts +12 -5
- package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
- package/src/tools/coordinator/__tests__/wait-with-idle-bound.test.ts +247 -0
- package/src/tools/coordinator/index.ts +205 -78
- package/src/tools/coordinator/wait-with-idle-bound.ts +142 -0
- package/src/tools/untrusted-envelope.ts +9 -1
- package/src/types/agent/base.ts +17 -0
- package/src/types/agent/gateway.ts +42 -0
- package/src/types/agent/lifecycle-event.ts +7 -0
- package/src/types/agent/task.ts +5 -0
- package/src/types/hitl/index.ts +10 -0
- package/src/types/probe/registry.ts +6 -0
- package/src/types/provider/interface.ts +39 -0
- package/src/types/run/config.ts +26 -0
- package/src/types/run/entity.ts +17 -0
- package/src/types/run/events.ts +75 -0
- package/src/types/run/prepare-step.ts +17 -2
- package/src/types/verification/index.ts +61 -0
- package/src/utils/__tests__/abort-reason.test.ts +56 -0
- package/src/utils/abort.ts +34 -0
- package/src/verification/__tests__/argument-pattern.test.ts +158 -0
- package/src/verification/__tests__/rule-order-and-reason.test.ts +149 -0
- package/src/verification/gate.ts +106 -3
- package/src/verification/index.ts +1 -1
- package/src/verification/rules.ts +28 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { TaskGateway, TaskHandle } from '../../../types/agent/gateway.js'
|
|
4
|
+
import type { TaskId } from '../../../types/ids/index.js'
|
|
5
|
+
import { describeWaitTimeout, waitForTaskWithBounds } from '../wait-with-idle-bound.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A wall clock cannot tell a slow worker from a stuck one.
|
|
9
|
+
*
|
|
10
|
+
* The bound before this was an hour of elapsed time, and an hour has to do
|
|
11
|
+
* two incompatible jobs: be long enough for a child doing real work, and
|
|
12
|
+
* short enough to notice one that wedged. It cannot be both, so a worker
|
|
13
|
+
* stuck in minute two held the supervisor for another fifty-eight, and one
|
|
14
|
+
* making steady progress at minute fifty-nine was killed for being slow.
|
|
15
|
+
*
|
|
16
|
+
* These tests drive both clocks with a fake one, because the real bounds are
|
|
17
|
+
* measured in minutes and a test that actually waits them out is a test
|
|
18
|
+
* nobody runs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function gatewayFor(opts: {
|
|
22
|
+
/** Resolve the wait when this is called. */
|
|
23
|
+
settle?: (resolve: (h: TaskHandle) => void) => void
|
|
24
|
+
withProgress?: boolean
|
|
25
|
+
}): { gateway: TaskGateway; progress: () => void; finish: () => void } {
|
|
26
|
+
let resolveWait: ((h: TaskHandle) => void) | undefined
|
|
27
|
+
const listeners = new Set<(id: TaskId) => void>()
|
|
28
|
+
|
|
29
|
+
const handle: TaskHandle = {
|
|
30
|
+
taskId: 'tsk_1' as TaskId,
|
|
31
|
+
agentId: 'worker',
|
|
32
|
+
state: 'completed',
|
|
33
|
+
createdAt: 0,
|
|
34
|
+
completedAt: 1,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const gateway = {
|
|
38
|
+
waitForTask: () =>
|
|
39
|
+
new Promise<TaskHandle>((resolve) => {
|
|
40
|
+
resolveWait = resolve
|
|
41
|
+
opts.settle?.(resolve)
|
|
42
|
+
}),
|
|
43
|
+
...(opts.withProgress === false
|
|
44
|
+
? {}
|
|
45
|
+
: {
|
|
46
|
+
onTaskProgress: (cb: (id: TaskId) => void) => {
|
|
47
|
+
listeners.add(cb)
|
|
48
|
+
return () => listeners.delete(cb)
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
} as unknown as TaskGateway
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
gateway,
|
|
55
|
+
progress: () => {
|
|
56
|
+
for (const cb of listeners) cb('tsk_1' as TaskId)
|
|
57
|
+
},
|
|
58
|
+
finish: () => resolveWait?.(handle),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A clock the test moves by hand. */
|
|
63
|
+
function fakeClock(): { now: () => number; advance: (ms: number) => void } {
|
|
64
|
+
let t = 1_000_000
|
|
65
|
+
return {
|
|
66
|
+
now: () => t,
|
|
67
|
+
advance: (ms) => {
|
|
68
|
+
t += ms
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('a worker that is still working is not killed for being slow', () => {
|
|
74
|
+
it('keeps waiting past the idle bound while progress keeps arriving', async () => {
|
|
75
|
+
vi.useFakeTimers()
|
|
76
|
+
try {
|
|
77
|
+
const clock = fakeClock()
|
|
78
|
+
const { gateway, progress, finish } = gatewayFor({})
|
|
79
|
+
|
|
80
|
+
const waiting = waitForTaskWithBounds(
|
|
81
|
+
gateway,
|
|
82
|
+
'tsk_1' as TaskId,
|
|
83
|
+
{ runMs: 60_000, idleMs: 5_000 },
|
|
84
|
+
clock.now,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Four times the idle bound in elapsed time, but never quiet for
|
|
88
|
+
// more than half of it.
|
|
89
|
+
for (let i = 0; i < 8; i += 1) {
|
|
90
|
+
clock.advance(2_500)
|
|
91
|
+
progress()
|
|
92
|
+
await vi.advanceTimersByTimeAsync(1_000)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
finish()
|
|
96
|
+
await vi.advanceTimersByTimeAsync(1_000)
|
|
97
|
+
|
|
98
|
+
expect((await waiting).kind, 'a working worker was cut off').toBe('completed')
|
|
99
|
+
} finally {
|
|
100
|
+
vi.useRealTimers()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('a worker that has gone quiet is reported as quiet', () => {
|
|
106
|
+
it('fires the idle bound and says which clock it was', async () => {
|
|
107
|
+
vi.useFakeTimers()
|
|
108
|
+
try {
|
|
109
|
+
const clock = fakeClock()
|
|
110
|
+
const { gateway } = gatewayFor({})
|
|
111
|
+
|
|
112
|
+
const waiting = waitForTaskWithBounds(
|
|
113
|
+
gateway,
|
|
114
|
+
'tsk_1' as TaskId,
|
|
115
|
+
{ runMs: 600_000, idleMs: 5_000 },
|
|
116
|
+
clock.now,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
clock.advance(6_000)
|
|
120
|
+
await vi.advanceTimersByTimeAsync(1_100)
|
|
121
|
+
|
|
122
|
+
const outcome = await waiting
|
|
123
|
+
expect(outcome.kind).toBe('timeout')
|
|
124
|
+
if (outcome.kind !== 'timeout') return
|
|
125
|
+
// The distinction is the point: "went quiet" and "ran too long"
|
|
126
|
+
// are different diagnoses and the caller acts on the message.
|
|
127
|
+
expect(outcome.cause).toBe('idle')
|
|
128
|
+
expect(outcome.idleBoundArmed).toBe(true)
|
|
129
|
+
} finally {
|
|
130
|
+
vi.useRealTimers()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('says it went quiet, and that the worker was not cancelled', () => {
|
|
135
|
+
const text = describeWaitTimeout({
|
|
136
|
+
kind: 'timeout',
|
|
137
|
+
cause: 'idle',
|
|
138
|
+
elapsedMs: 30_000,
|
|
139
|
+
idleBoundArmed: true,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(text).toContain('went quiet')
|
|
143
|
+
// A wait that ran out is a statement about the WAITER. Losing an
|
|
144
|
+
// eight-minute worker's output because a short clock expired is the
|
|
145
|
+
// bug this whole area has been unpicking.
|
|
146
|
+
expect(text).toContain('not been cancelled')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('the run bound still catches a worker that never stops', () => {
|
|
151
|
+
it('fires on elapsed time even while progress keeps arriving', async () => {
|
|
152
|
+
vi.useFakeTimers()
|
|
153
|
+
try {
|
|
154
|
+
const clock = fakeClock()
|
|
155
|
+
const { gateway, progress } = gatewayFor({})
|
|
156
|
+
|
|
157
|
+
const waiting = waitForTaskWithBounds(
|
|
158
|
+
gateway,
|
|
159
|
+
'tsk_1' as TaskId,
|
|
160
|
+
{ runMs: 10_000, idleMs: 5_000 },
|
|
161
|
+
clock.now,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < 6; i += 1) {
|
|
165
|
+
clock.advance(2_000)
|
|
166
|
+
progress()
|
|
167
|
+
await vi.advanceTimersByTimeAsync(1_000)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const outcome = await waiting
|
|
171
|
+
expect(outcome.kind).toBe('timeout')
|
|
172
|
+
if (outcome.kind !== 'timeout') return
|
|
173
|
+
expect(outcome.cause).toBe('run')
|
|
174
|
+
} finally {
|
|
175
|
+
vi.useRealTimers()
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('a gateway that cannot see its children', () => {
|
|
181
|
+
it('is bounded by the wall clock alone, as it was before', async () => {
|
|
182
|
+
vi.useFakeTimers()
|
|
183
|
+
try {
|
|
184
|
+
const clock = fakeClock()
|
|
185
|
+
const { gateway } = gatewayFor({ withProgress: false })
|
|
186
|
+
|
|
187
|
+
const waiting = waitForTaskWithBounds(
|
|
188
|
+
gateway,
|
|
189
|
+
'tsk_1' as TaskId,
|
|
190
|
+
{ runMs: 10_000, idleMs: 1_000 },
|
|
191
|
+
clock.now,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Far past the idle bound, and it must NOT fire — there is no
|
|
195
|
+
// signal, so silence carries no information.
|
|
196
|
+
clock.advance(5_000)
|
|
197
|
+
await vi.advanceTimersByTimeAsync(1_100)
|
|
198
|
+
clock.advance(6_000)
|
|
199
|
+
await vi.advanceTimersByTimeAsync(1_100)
|
|
200
|
+
|
|
201
|
+
const outcome = await waiting
|
|
202
|
+
expect(outcome.kind).toBe('timeout')
|
|
203
|
+
if (outcome.kind !== 'timeout') return
|
|
204
|
+
expect(outcome.cause).toBe('run')
|
|
205
|
+
// And the degradation is visible rather than silent.
|
|
206
|
+
expect(outcome.idleBoundArmed).toBe(false)
|
|
207
|
+
} finally {
|
|
208
|
+
vi.useRealTimers()
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('says so, instead of implying the worker was stuck', () => {
|
|
213
|
+
const text = describeWaitTimeout({
|
|
214
|
+
kind: 'timeout',
|
|
215
|
+
cause: 'run',
|
|
216
|
+
elapsedMs: 3_600_000,
|
|
217
|
+
idleBoundArmed: false,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(text).toContain('cannot report progress')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('a completion always wins', () => {
|
|
225
|
+
it('returns the handle rather than a timeout when the task finishes first', async () => {
|
|
226
|
+
vi.useFakeTimers()
|
|
227
|
+
try {
|
|
228
|
+
const clock = fakeClock()
|
|
229
|
+
const { gateway, finish } = gatewayFor({})
|
|
230
|
+
|
|
231
|
+
const waiting = waitForTaskWithBounds(
|
|
232
|
+
gateway,
|
|
233
|
+
'tsk_1' as TaskId,
|
|
234
|
+
{ runMs: 10_000, idleMs: 5_000 },
|
|
235
|
+
clock.now,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
finish()
|
|
239
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
240
|
+
|
|
241
|
+
const outcome = await waiting
|
|
242
|
+
expect(outcome.kind).toBe('completed')
|
|
243
|
+
} finally {
|
|
244
|
+
vi.useRealTimers()
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -11,6 +11,7 @@ import type { ToolDefinition } from '../../types/tool/index.js'
|
|
|
11
11
|
import { defineTool } from '../defineTool.js'
|
|
12
12
|
import { wrapUntrusted } from '../untrusted-envelope.js'
|
|
13
13
|
import { resolvePlanDependencies } from './plan-dependencies.js'
|
|
14
|
+
import { describeWaitTimeout, waitForTaskWithBounds } from './wait-with-idle-bound.js'
|
|
14
15
|
|
|
15
16
|
export type TaskLaunchedCallback = (
|
|
16
17
|
agentTaskId: TaskId,
|
|
@@ -259,6 +260,33 @@ const LISTED_RESULT_LIMIT = 2_000
|
|
|
259
260
|
*/
|
|
260
261
|
export const DELEGATION_TIMEOUT_MS = 60 * 60 * 1000
|
|
261
262
|
|
|
263
|
+
/**
|
|
264
|
+
* How long a delegated worker may say nothing before the wait gives up.
|
|
265
|
+
*
|
|
266
|
+
* The hour above answers "how long is too long". It cannot also answer
|
|
267
|
+
* "how quiet is too quiet", because it has to be generous enough for a
|
|
268
|
+
* child doing real work — which makes it useless as a stall detector. A
|
|
269
|
+
* worker wedged in its second minute held the supervisor for another
|
|
270
|
+
* fifty-eight under that number alone.
|
|
271
|
+
*
|
|
272
|
+
* Five minutes of silence, because a worker between tool calls can be
|
|
273
|
+
* quiet for a while legitimately — a long model turn emits nothing until
|
|
274
|
+
* it starts streaming — and the cost of guessing low is killing a wait on
|
|
275
|
+
* a worker that was fine. Guessing high only delays a diagnosis. Set
|
|
276
|
+
* `NAMZU_DELEGATION_IDLE_MS` to change it.
|
|
277
|
+
*
|
|
278
|
+
* Only armed when the gateway can report progress at all; see
|
|
279
|
+
* `TaskGateway.onTaskProgress`.
|
|
280
|
+
*/
|
|
281
|
+
export const DELEGATION_IDLE_MS = readPositiveIntEnv('NAMZU_DELEGATION_IDLE_MS', 5 * 60 * 1000)
|
|
282
|
+
|
|
283
|
+
function readPositiveIntEnv(key: string, fallback: number): number {
|
|
284
|
+
const value = process.env[key]?.trim()
|
|
285
|
+
if (!value) return fallback
|
|
286
|
+
const parsed = Number(value)
|
|
287
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
288
|
+
}
|
|
289
|
+
|
|
262
290
|
export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
|
|
263
291
|
const {
|
|
264
292
|
gateway,
|
|
@@ -283,9 +311,66 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
283
311
|
|
|
284
312
|
const agentIdEnum = delegateSchema(agentIds)
|
|
285
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Whether a launch can be made with nothing waiting on it.
|
|
316
|
+
*
|
|
317
|
+
* A background launch returns a task id and promises the result "later, as
|
|
318
|
+
* a task notification". The only thing that keeps that promise is the
|
|
319
|
+
* inbox: it is what holds the run open for an outstanding worker and what
|
|
320
|
+
* puts the completion into the transcript. With no inbox the tool told the
|
|
321
|
+
* model to expect a message on a channel that does not exist — measured,
|
|
322
|
+
* and the launch itself succeeded, so nothing failed loudly either.
|
|
323
|
+
*
|
|
324
|
+
* Withheld rather than refused per call, and rather than thrown at
|
|
325
|
+
* construction. Least functionality (NIST SP 800-53 Rev. 5 CM-7: provide
|
|
326
|
+
* only mission-essential capabilities): a parameter the model is never
|
|
327
|
+
* shown costs it nothing, where a parameter it is shown and then denied
|
|
328
|
+
* costs prompt-prefix tokens plus an iteration per attempt. And a throw
|
|
329
|
+
* would break a legitimate caller — an inbox-less coordinator surface is a
|
|
330
|
+
* supported configuration whose blocking path is unaffected, pinned by a
|
|
331
|
+
* test ("runs unchanged with no inbox at all"). That is the same reasoning
|
|
332
|
+
* that made an empty roster WITHHOLD `create_task` rather than refuse to
|
|
333
|
+
* build, and it is one parameter wide here for the same reason it was one
|
|
334
|
+
* tool wide there.
|
|
335
|
+
*/
|
|
336
|
+
const canLaunchInBackground = completionInbox !== undefined
|
|
337
|
+
|
|
338
|
+
const backgroundClause = canLaunchInBackground
|
|
339
|
+
? " By default this BLOCKS and returns the agent's final output as this call's tool_result; pass background: true to get a task_id back immediately and receive the result later as a task notification."
|
|
340
|
+
: " This BLOCKS and returns the agent's final output as this call's tool_result."
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* What to tell the model when a wait was cut short.
|
|
344
|
+
*
|
|
345
|
+
* The worker keeps going either way — giving up on a wait is a statement
|
|
346
|
+
* about the waiter, not about the work. Where the result then turns up is
|
|
347
|
+
* NOT the same either way, and the tool said it was: it promised a task
|
|
348
|
+
* notification unconditionally, which without an inbox is a message on a
|
|
349
|
+
* channel that does not exist. A model told to expect one waits for it,
|
|
350
|
+
* and the one tool that could still reach the output is the one it was
|
|
351
|
+
* told not to use for this.
|
|
352
|
+
*/
|
|
353
|
+
const whereTheResultWillTurnUp = (taskId: TaskId): string =>
|
|
354
|
+
completionInbox
|
|
355
|
+
? `its result will arrive separately as a task notification (task ${taskId}).`
|
|
356
|
+
: `it is still running as task ${taskId} — call wait_for_task with that id, or find it in agent_task_list once it finishes. Nothing will announce it on its own.`
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The listing's standing advice, which depends on there being an inbox.
|
|
360
|
+
*
|
|
361
|
+
* "Do not call this to find out whether work finished" is right when a
|
|
362
|
+
* notification is coming. With no inbox an abandoned blocking launch has
|
|
363
|
+
* no announcer at all, and this listing is the only way left to reach the
|
|
364
|
+
* output — so the same sentence would send the model away from the one
|
|
365
|
+
* tool that could help it.
|
|
366
|
+
*/
|
|
367
|
+
const listingAdvice = completionInbox
|
|
368
|
+
? "Do NOT call this to find out whether work finished: a blocking create_task has already returned each worker's output, and a backgrounded one arrives as a task notification. Use it when you need to see what is still running, or to re-read the output of a task whose launch you stopped waiting for."
|
|
369
|
+
: "A blocking create_task already returns each worker's output, so do not call this in a loop to find out whether work finished. Nothing announces a completion on this configuration, so this listing and wait_for_task are how you reach the output of a task whose launch you stopped waiting for."
|
|
370
|
+
|
|
286
371
|
const createTask = defineTool({
|
|
287
372
|
name: 'create_task',
|
|
288
|
-
description: `Launch a task on a specialized agent
|
|
373
|
+
description: `Launch a task on a specialized agent.${backgroundClause} Available agents: ${agentIds.join(', ')}. Prefer compact assignments; for large context, write/read shared workspace files and pass filenames or references. To launch multiple tasks in parallel, call this tool multiple times in a single assistant turn — the runtime executes every tool_use block from one response concurrently and delivers all tool_results together, so 'fan out 8 specialists' is one assistant message with 8 create_task blocks. Do not race: until a worker's result reaches you, you know nothing about it — never fabricate, summarise or predict what it will say, in any form.`,
|
|
289
374
|
inputSchema: z.object({
|
|
290
375
|
agent_id: agentIdEnum.describe('Which agent to run'),
|
|
291
376
|
prompt: z
|
|
@@ -300,12 +385,16 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
300
385
|
.describe(
|
|
301
386
|
'Existing planning task ID to link. If omitted, a planning task is auto-created.',
|
|
302
387
|
),
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
388
|
+
...(canLaunchInBackground
|
|
389
|
+
? {
|
|
390
|
+
background: z
|
|
391
|
+
.boolean()
|
|
392
|
+
.optional()
|
|
393
|
+
.describe(
|
|
394
|
+
'Return immediately with a task_id instead of waiting. The result arrives later as a task notification. Use this when you have other work to do meanwhile; leave it off when the next thing you do depends on this answer.',
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
309
398
|
}),
|
|
310
399
|
category: 'custom',
|
|
311
400
|
permissions: [],
|
|
@@ -348,6 +437,34 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
348
437
|
...(_context.parentSpan ? { parentSpan: _context.parentSpan } : {}),
|
|
349
438
|
})
|
|
350
439
|
|
|
440
|
+
// Whose task this is. The inbox ignores completions for anything it
|
|
441
|
+
// was not told about, because `onTaskCompleted` is a broadcast and a
|
|
442
|
+
// gateway shared between two supervisors would otherwise hand each
|
|
443
|
+
// of them the other's worker output. Said on BOTH paths: the
|
|
444
|
+
// blocking one needs it too, because the case the inbox exists for
|
|
445
|
+
// is exactly the blocking launch whose wait was abandoned.
|
|
446
|
+
completionInbox?.launched(handle.taskId)
|
|
447
|
+
|
|
448
|
+
// A background launch asked for with nowhere to deliver it is
|
|
449
|
+
// REFUSED, not quietly turned into a blocking one.
|
|
450
|
+
//
|
|
451
|
+
// The schema withholds the parameter and Zod strips what it does
|
|
452
|
+
// not declare, so this is unreachable through the model; it exists
|
|
453
|
+
// for a directly-constructed definition. Falling back to blocking
|
|
454
|
+
// would have been the tempting answer — the caller does get the
|
|
455
|
+
// output — but it is accepting work whose stated terms cannot be
|
|
456
|
+
// met, and the caller asked for a call that returns immediately.
|
|
457
|
+
// Naming the missing piece is the only response that tells them
|
|
458
|
+
// what to change.
|
|
459
|
+
if (background && !canLaunchInBackground) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
output: '',
|
|
463
|
+
error:
|
|
464
|
+
'background: true needs a CompletionInbox — without one there is no channel for the notification this launch promises. Pass `completionInbox` to buildCoordinatorTools and the same instance to drainQuery, or omit `background` to wait for the result inline.',
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
351
468
|
if (background) {
|
|
352
469
|
// Tell the inbox to hold the run open for this. Without it the
|
|
353
470
|
// supervisor could launch a worker, answer, and settle the run
|
|
@@ -379,7 +496,23 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
379
496
|
// assistant turn, the runtime runs them together and delivers all
|
|
380
497
|
// N `tool_result`s at once. No second `tool_result` for the
|
|
381
498
|
// same `tool_use_id` — providers reject a duplicated id outright.
|
|
382
|
-
|
|
499
|
+
// Bounded by two clocks rather than one. The wait gives up on a
|
|
500
|
+
// worker that has gone quiet long before the hour is out, and says
|
|
501
|
+
// which of the two ran out — the caller acts on that difference.
|
|
502
|
+
// Giving up does NOT cancel the child: it keeps going, and its
|
|
503
|
+
// result still reaches the supervisor as a notification.
|
|
504
|
+
const outcome = await waitForTaskWithBounds(gateway, handle.taskId, {
|
|
505
|
+
runMs: DELEGATION_TIMEOUT_MS,
|
|
506
|
+
idleMs: DELEGATION_IDLE_MS,
|
|
507
|
+
})
|
|
508
|
+
if (outcome.kind === 'timeout') {
|
|
509
|
+
return {
|
|
510
|
+
success: false,
|
|
511
|
+
output: describeWaitTimeout(outcome),
|
|
512
|
+
data: { task_id: handle.taskId, agent_id, timed_out: outcome.cause },
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const completed = outcome.handle
|
|
383
516
|
|
|
384
517
|
// Whether this call is still the live path decides who delivers the
|
|
385
518
|
// result. If the executor already gave up on us — its deadline
|
|
@@ -395,7 +528,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
395
528
|
if (_context.abortSignal?.aborted) {
|
|
396
529
|
return {
|
|
397
530
|
success: false,
|
|
398
|
-
output: `This wait was abandoned before ${agent_id} finished;
|
|
531
|
+
output: `This wait was abandoned before ${agent_id} finished; ${whereTheResultWillTurnUp(handle.taskId)}`,
|
|
399
532
|
data: { task_id: handle.taskId, agent_id, abandoned: true },
|
|
400
533
|
}
|
|
401
534
|
}
|
|
@@ -450,55 +583,6 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
450
583
|
},
|
|
451
584
|
})
|
|
452
585
|
|
|
453
|
-
const continueTask = defineTool({
|
|
454
|
-
name: 'continue_task',
|
|
455
|
-
description:
|
|
456
|
-
"Send a follow-up message to a previously completed task and await the agent's next reply. BLOCKING: returns the agent's new output as this call's tool_result, the same shape as create_task. Only use this with a task_id from a previous create_task. To run multiple follow-ups in parallel, call this tool multiple times in a single assistant turn.",
|
|
457
|
-
inputSchema: z.object({
|
|
458
|
-
task_id: z.string().describe('Agent task ID from a previous create_task'),
|
|
459
|
-
message: z.string().describe('Follow-up instruction for the agent'),
|
|
460
|
-
}),
|
|
461
|
-
category: 'custom',
|
|
462
|
-
permissions: [],
|
|
463
|
-
readOnly: false,
|
|
464
|
-
destructive: false,
|
|
465
|
-
concurrencySafe: true,
|
|
466
|
-
// It waits on a child exactly as create_task does, so it inherits
|
|
467
|
-
// the same bound rather than the file-read default.
|
|
468
|
-
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
469
|
-
async execute({ task_id, message }, _context) {
|
|
470
|
-
await gateway.continueTask(task_id as TaskId, message)
|
|
471
|
-
// Mirror create_task's blocking pattern: await the new
|
|
472
|
-
// completion and return the agent's output inline. The
|
|
473
|
-
// previous non-blocking shape ('You will receive a
|
|
474
|
-
// task-notification…') relied on a global
|
|
475
|
-
// onTaskCompleted listener that the iteration loop
|
|
476
|
-
// no longer registers (envelope path is dead).
|
|
477
|
-
const completed = await gateway.waitForTask(task_id as TaskId)
|
|
478
|
-
// Same reasoning as create_task: the model already has a timeout
|
|
479
|
-
// for this call, so leaving the completion unclaimed is what sends
|
|
480
|
-
// it to the transcript as a notification.
|
|
481
|
-
if (_context.abortSignal?.aborted) {
|
|
482
|
-
return {
|
|
483
|
-
success: false,
|
|
484
|
-
output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
|
|
485
|
-
data: { task_id, abandoned: true },
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
completionInbox?.claim(task_id as TaskId)
|
|
489
|
-
const success = completed.state === 'completed'
|
|
490
|
-
const resultText =
|
|
491
|
-
completed.result?.result ??
|
|
492
|
-
completed.result?.lastError ??
|
|
493
|
-
`Task finished with state: ${completed.state}`
|
|
494
|
-
return {
|
|
495
|
-
success,
|
|
496
|
-
output: resultText,
|
|
497
|
-
data: { task_id, state: completed.state },
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
})
|
|
501
|
-
|
|
502
586
|
/**
|
|
503
587
|
* Join a task already running, without sending it anything.
|
|
504
588
|
*
|
|
@@ -533,11 +617,22 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
533
617
|
}
|
|
534
618
|
}
|
|
535
619
|
|
|
536
|
-
const
|
|
620
|
+
const outcome = await waitForTaskWithBounds(gateway, task_id as TaskId, {
|
|
621
|
+
runMs: DELEGATION_TIMEOUT_MS,
|
|
622
|
+
idleMs: DELEGATION_IDLE_MS,
|
|
623
|
+
})
|
|
624
|
+
if (outcome.kind === 'timeout') {
|
|
625
|
+
return {
|
|
626
|
+
success: false,
|
|
627
|
+
output: describeWaitTimeout(outcome),
|
|
628
|
+
data: { task_id, timed_out: outcome.cause },
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const completed = outcome.handle
|
|
537
632
|
if (_context.abortSignal?.aborted) {
|
|
538
633
|
return {
|
|
539
634
|
success: false,
|
|
540
|
-
output: `This wait was abandoned before task ${task_id} finished;
|
|
635
|
+
output: `This wait was abandoned before task ${task_id} finished; ${whereTheResultWillTurnUp(task_id as TaskId)}`,
|
|
541
636
|
data: { task_id, abandoned: true },
|
|
542
637
|
}
|
|
543
638
|
}
|
|
@@ -598,8 +693,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
598
693
|
|
|
599
694
|
const agentTaskList = defineTool({
|
|
600
695
|
name: 'agent_task_list',
|
|
601
|
-
description:
|
|
602
|
-
"Inspect the live state of every agent task launched on this gateway via create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Distinct from the plan-task store's `task_list` (which lists planning tasks): this tool lists running/completed worker invocations. Do NOT call this to find out whether work finished: a blocking create_task has already returned each worker's output, and a backgrounded one arrives as a task notification. Use it when you need to see what is still running, or to re-read the output of a task whose launch you stopped waiting for.",
|
|
696
|
+
description: `Inspect the live state of every agent task launched on this gateway via create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Distinct from the plan-task store's \`task_list\` (which lists planning tasks): this tool lists running/completed worker invocations. ${listingAdvice}`,
|
|
603
697
|
inputSchema: z.object({
|
|
604
698
|
state: z
|
|
605
699
|
.enum(['pending', 'running', 'completed', 'failed', 'canceled'])
|
|
@@ -656,10 +750,29 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
656
750
|
// model cannot see, which is how this listing came to prove a
|
|
657
751
|
// task had finished while withholding what it said.
|
|
658
752
|
if (i.result === undefined) return head
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
753
|
+
const overLimit = i.result.length > LISTED_RESULT_LIMIT
|
|
754
|
+
// Framed exactly as the blocking `create_task` and
|
|
755
|
+
// `wait_for_task` frame the same bytes. This listing was the
|
|
756
|
+
// third way to read a delegate's output and the only one that
|
|
757
|
+
// pasted it bare — so a worker's text was material on two
|
|
758
|
+
// paths and read as the parent's own reasoning on the third,
|
|
759
|
+
// and which one a run got depended on how the model chose to
|
|
760
|
+
// fetch it.
|
|
761
|
+
const framed = wrapUntrusted(
|
|
762
|
+
{
|
|
763
|
+
kind: 'agent-result',
|
|
764
|
+
attributes: { agent: i.agent_id, task: i.task_id },
|
|
765
|
+
provenance: `This is the output of the delegated agent "${i.agent_id}", not this agent's own work.`,
|
|
766
|
+
},
|
|
767
|
+
overLimit ? i.result.slice(0, LISTED_RESULT_LIMIT) : i.result,
|
|
768
|
+
)
|
|
769
|
+
// After the closing tag, not inside it: this sentence is the
|
|
770
|
+
// kernel telling the model how to get the rest, and inside
|
|
771
|
+
// the envelope it has just been told the contents are not
|
|
772
|
+
// instructions addressed to it.
|
|
773
|
+
const body = overLimit
|
|
774
|
+
? `${framed}\n… truncated; call wait_for_task with "${i.task_id}" for the whole thing.`
|
|
775
|
+
: framed
|
|
663
776
|
return `${head}\n${body
|
|
664
777
|
.split('\n')
|
|
665
778
|
.map((line) => ` ${line}`)
|
|
@@ -675,18 +788,32 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
675
788
|
},
|
|
676
789
|
})
|
|
677
790
|
|
|
678
|
-
// `continue_task`
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
|
|
791
|
+
// `continue_task` is gone, and the reasoning is worth keeping because
|
|
792
|
+
// the obvious argument for bringing it back does not survive contact.
|
|
793
|
+
//
|
|
794
|
+
// It was dropped on the grounds that a blocking `create_task` leaves every
|
|
795
|
+
// worker terminal before a later turn learns its id, and the manager
|
|
796
|
+
// refuses `continue` on a terminal task. `background: true` reinstated that
|
|
797
|
+
// precondition — a live id is reachable now — so the question was reopened.
|
|
798
|
+
//
|
|
799
|
+
// Measured rather than assumed, and it fails on the other side. On a LIVE
|
|
800
|
+
// task the manager accepts the call and pushes onto `pendingMessages`,
|
|
801
|
+
// and NOTHING drains that queue during a run. This codebase already
|
|
802
|
+
// knows: `runtime/query/steering.ts` says in as many words that
|
|
803
|
+
// `queueMessage`/`drainMessages` were never read by the iteration loop,
|
|
804
|
+
// and `SteeringChannel` exists BECAUSE of that — it delivers guidance on a
|
|
805
|
+
// tool result instead, since a `tool_use` must be answered by a
|
|
806
|
+
// `tool_result` with the same id and there is no legal slot for a user
|
|
807
|
+
// message mid-batch.
|
|
808
|
+
//
|
|
809
|
+
// So the tool had no state it worked in: terminal tasks refuse it, live
|
|
810
|
+
// tasks accept it into a queue nobody reads. Registering it would have
|
|
811
|
+
// handed the model a call that silently does nothing — worse than the
|
|
812
|
+
// unregistered definition it replaced, because a defined-but-unreachable
|
|
813
|
+
// tool at least cannot be called.
|
|
814
|
+
//
|
|
815
|
+
// If follow-ups on a live worker are wanted, the work is a consumer for
|
|
816
|
+
// the queue or a steering channel that reaches a child, not this tool.
|
|
690
817
|
// `cancel_task` is registered again, and the reasoning that dropped it is
|
|
691
818
|
// worth keeping because it was sound at the time and is not any more.
|
|
692
819
|
//
|