@namzu/sdk 6.1.0 → 7.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 +361 -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 +9 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-coordinator-registration.test.js +31 -0
- package/dist/agents/__tests__/supervisor-coordinator-registration.test.js.map +1 -1
- 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/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/gateway/__tests__/completion-inbox.test.js +17 -0
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
- package/dist/gateway/completion-inbox.d.ts.map +1 -1
- package/dist/gateway/completion-inbox.js +17 -3
- 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/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/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__/run-survives-its-own-park.proc-test.d.ts +2 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js +122 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-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/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +5 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +43 -13
- package/dist/runtime/query/iteration/phases/context.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/builtins/__tests__/bash.proc-test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.js +115 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.js.map +1 -0
- package/dist/tools/builtins/__tests__/bash.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/bash.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/bash.test.js +65 -0
- package/dist/tools/builtins/__tests__/bash.test.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +89 -17
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.js +94 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.js.map +1 -0
- 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 +28 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +101 -62
- 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/types/agent/base.d.ts +16 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/gateway.d.ts +23 -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/supervisor.d.ts +27 -0
- package/dist/types/agent/supervisor.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/run/config.d.ts +25 -0
- package/dist/types/run/config.d.ts.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__/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 +58 -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/package.json +3 -2
- package/src/agents/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +9 -0
- package/src/agents/__tests__/supervisor-coordinator-registration.test.ts +37 -0
- package/src/agents/runAgent.ts +22 -1
- 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/gateway/__tests__/completion-inbox.test.ts +23 -0
- package/src/gateway/completion-inbox.ts +17 -3
- package/src/gateway/local.ts +26 -1
- package/src/manager/agent/lifecycle.ts +6 -0
- package/src/provider/thinking-support.ts +19 -2
- package/src/public-runtime.ts +9 -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__/run-survives-its-own-park.proc-test.ts +127 -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/iteration/index.ts +5 -0
- package/src/runtime/query/iteration/phases/context.ts +41 -15
- package/src/telemetry/__tests__/model-call-span.test.ts +22 -5
- package/src/telemetry/__tests__/span-closure.test.ts +12 -5
- package/src/tools/builtins/__tests__/bash.proc-test.ts +142 -0
- package/src/tools/builtins/__tests__/bash.test.ts +79 -0
- package/src/tools/builtins/bash.ts +100 -18
- package/src/tools/coordinator/__tests__/allow-delegation.test.ts +120 -0
- package/src/tools/coordinator/__tests__/wait-with-idle-bound.test.ts +247 -0
- package/src/tools/coordinator/index.ts +112 -65
- package/src/tools/coordinator/wait-with-idle-bound.ts +142 -0
- package/src/types/agent/base.ts +17 -0
- package/src/types/agent/gateway.ts +24 -0
- package/src/types/agent/lifecycle-event.ts +7 -0
- package/src/types/agent/supervisor.ts +28 -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/run/config.ts +26 -0
- package/src/utils/__tests__/abort-reason.test.ts +56 -0
- package/src/utils/abort.ts +34 -0
- package/src/verification/__tests__/rule-order-and-reason.test.ts +149 -0
- package/src/verification/gate.ts +61 -3
- package/src/verification/index.ts +1 -1
|
@@ -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,
|
|
@@ -34,6 +35,16 @@ export interface CoordinatorToolsOptions {
|
|
|
34
35
|
runtimeContext?: AgentRuntimeContext
|
|
35
36
|
allowedAgentIds: string[]
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* May this run delegate at all? Defaults to true.
|
|
40
|
+
*
|
|
41
|
+
* Same field, same name, as SupervisorAgentConfig.allowDelegation — the
|
|
42
|
+
* name is kept identical deliberately. This options bag already renames
|
|
43
|
+
* agentIds to allowedAgentIds, and a second rename on the road between
|
|
44
|
+
* the config and the decision would make the road untraceable.
|
|
45
|
+
*/
|
|
46
|
+
allowDelegation?: boolean
|
|
47
|
+
|
|
37
48
|
taskStore?: TaskStore
|
|
38
49
|
|
|
39
50
|
runId?: RunId
|
|
@@ -249,10 +260,38 @@ const LISTED_RESULT_LIMIT = 2_000
|
|
|
249
260
|
*/
|
|
250
261
|
export const DELEGATION_TIMEOUT_MS = 60 * 60 * 1000
|
|
251
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
|
+
|
|
252
290
|
export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
|
|
253
291
|
const {
|
|
254
292
|
gateway,
|
|
255
293
|
allowedAgentIds: agentIds,
|
|
294
|
+
allowDelegation,
|
|
256
295
|
taskStore,
|
|
257
296
|
runId,
|
|
258
297
|
getPlanManager,
|
|
@@ -368,7 +407,23 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
368
407
|
// assistant turn, the runtime runs them together and delivers all
|
|
369
408
|
// N `tool_result`s at once. No second `tool_result` for the
|
|
370
409
|
// same `tool_use_id` — providers reject a duplicated id outright.
|
|
371
|
-
|
|
410
|
+
// Bounded by two clocks rather than one. The wait gives up on a
|
|
411
|
+
// worker that has gone quiet long before the hour is out, and says
|
|
412
|
+
// which of the two ran out — the caller acts on that difference.
|
|
413
|
+
// Giving up does NOT cancel the child: it keeps going, and its
|
|
414
|
+
// result still reaches the supervisor as a notification.
|
|
415
|
+
const outcome = await waitForTaskWithBounds(gateway, handle.taskId, {
|
|
416
|
+
runMs: DELEGATION_TIMEOUT_MS,
|
|
417
|
+
idleMs: DELEGATION_IDLE_MS,
|
|
418
|
+
})
|
|
419
|
+
if (outcome.kind === 'timeout') {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
output: describeWaitTimeout(outcome),
|
|
423
|
+
data: { task_id: handle.taskId, agent_id, timed_out: outcome.cause },
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const completed = outcome.handle
|
|
372
427
|
|
|
373
428
|
// Whether this call is still the live path decides who delivers the
|
|
374
429
|
// result. If the executor already gave up on us — its deadline
|
|
@@ -439,55 +494,6 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
439
494
|
},
|
|
440
495
|
})
|
|
441
496
|
|
|
442
|
-
const continueTask = defineTool({
|
|
443
|
-
name: 'continue_task',
|
|
444
|
-
description:
|
|
445
|
-
"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.",
|
|
446
|
-
inputSchema: z.object({
|
|
447
|
-
task_id: z.string().describe('Agent task ID from a previous create_task'),
|
|
448
|
-
message: z.string().describe('Follow-up instruction for the agent'),
|
|
449
|
-
}),
|
|
450
|
-
category: 'custom',
|
|
451
|
-
permissions: [],
|
|
452
|
-
readOnly: false,
|
|
453
|
-
destructive: false,
|
|
454
|
-
concurrencySafe: true,
|
|
455
|
-
// It waits on a child exactly as create_task does, so it inherits
|
|
456
|
-
// the same bound rather than the file-read default.
|
|
457
|
-
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
458
|
-
async execute({ task_id, message }, _context) {
|
|
459
|
-
await gateway.continueTask(task_id as TaskId, message)
|
|
460
|
-
// Mirror create_task's blocking pattern: await the new
|
|
461
|
-
// completion and return the agent's output inline. The
|
|
462
|
-
// previous non-blocking shape ('You will receive a
|
|
463
|
-
// task-notification…') relied on a global
|
|
464
|
-
// onTaskCompleted listener that the iteration loop
|
|
465
|
-
// no longer registers (envelope path is dead).
|
|
466
|
-
const completed = await gateway.waitForTask(task_id as TaskId)
|
|
467
|
-
// Same reasoning as create_task: the model already has a timeout
|
|
468
|
-
// for this call, so leaving the completion unclaimed is what sends
|
|
469
|
-
// it to the transcript as a notification.
|
|
470
|
-
if (_context.abortSignal?.aborted) {
|
|
471
|
-
return {
|
|
472
|
-
success: false,
|
|
473
|
-
output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
|
|
474
|
-
data: { task_id, abandoned: true },
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
completionInbox?.claim(task_id as TaskId)
|
|
478
|
-
const success = completed.state === 'completed'
|
|
479
|
-
const resultText =
|
|
480
|
-
completed.result?.result ??
|
|
481
|
-
completed.result?.lastError ??
|
|
482
|
-
`Task finished with state: ${completed.state}`
|
|
483
|
-
return {
|
|
484
|
-
success,
|
|
485
|
-
output: resultText,
|
|
486
|
-
data: { task_id, state: completed.state },
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
})
|
|
490
|
-
|
|
491
497
|
/**
|
|
492
498
|
* Join a task already running, without sending it anything.
|
|
493
499
|
*
|
|
@@ -522,7 +528,18 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
522
528
|
}
|
|
523
529
|
}
|
|
524
530
|
|
|
525
|
-
const
|
|
531
|
+
const outcome = await waitForTaskWithBounds(gateway, task_id as TaskId, {
|
|
532
|
+
runMs: DELEGATION_TIMEOUT_MS,
|
|
533
|
+
idleMs: DELEGATION_IDLE_MS,
|
|
534
|
+
})
|
|
535
|
+
if (outcome.kind === 'timeout') {
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
output: describeWaitTimeout(outcome),
|
|
539
|
+
data: { task_id, timed_out: outcome.cause },
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const completed = outcome.handle
|
|
526
543
|
if (_context.abortSignal?.aborted) {
|
|
527
544
|
return {
|
|
528
545
|
success: false,
|
|
@@ -664,18 +681,32 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
664
681
|
},
|
|
665
682
|
})
|
|
666
683
|
|
|
667
|
-
// `continue_task`
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
|
|
684
|
+
// `continue_task` is gone, and the reasoning is worth keeping because
|
|
685
|
+
// the obvious argument for bringing it back does not survive contact.
|
|
686
|
+
//
|
|
687
|
+
// It was dropped on the grounds that a blocking `create_task` leaves every
|
|
688
|
+
// worker terminal before a later turn learns its id, and the manager
|
|
689
|
+
// refuses `continue` on a terminal task. `background: true` reinstated that
|
|
690
|
+
// precondition — a live id is reachable now — so the question was reopened.
|
|
691
|
+
//
|
|
692
|
+
// Measured rather than assumed, and it fails on the other side. On a LIVE
|
|
693
|
+
// task the manager accepts the call and pushes onto `pendingMessages`,
|
|
694
|
+
// and NOTHING drains that queue during a run. This codebase already
|
|
695
|
+
// knows: `runtime/query/steering.ts` says in as many words that
|
|
696
|
+
// `queueMessage`/`drainMessages` were never read by the iteration loop,
|
|
697
|
+
// and `SteeringChannel` exists BECAUSE of that — it delivers guidance on a
|
|
698
|
+
// tool result instead, since a `tool_use` must be answered by a
|
|
699
|
+
// `tool_result` with the same id and there is no legal slot for a user
|
|
700
|
+
// message mid-batch.
|
|
701
|
+
//
|
|
702
|
+
// So the tool had no state it worked in: terminal tasks refuse it, live
|
|
703
|
+
// tasks accept it into a queue nobody reads. Registering it would have
|
|
704
|
+
// handed the model a call that silently does nothing — worse than the
|
|
705
|
+
// unregistered definition it replaced, because a defined-but-unreachable
|
|
706
|
+
// tool at least cannot be called.
|
|
707
|
+
//
|
|
708
|
+
// If follow-ups on a live worker are wanted, the work is a consumer for
|
|
709
|
+
// the queue or a steering channel that reaches a child, not this tool.
|
|
679
710
|
// `cancel_task` is registered again, and the reasoning that dropped it is
|
|
680
711
|
// worth keeping because it was sound at the time and is not any more.
|
|
681
712
|
//
|
|
@@ -715,8 +746,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
715
746
|
// had to invent something to say — and one that would not do that was left
|
|
716
747
|
// calling `agent_task_list` in a sleep loop. That was never the model
|
|
717
748
|
// misbehaving; it was the only move available.
|
|
718
|
-
|
|
719
|
-
|
|
749
|
+
// Two independent reasons not to mount the delegation surface, and the
|
|
750
|
+
// second one is not derivable from the first.
|
|
751
|
+
//
|
|
752
|
+
// An empty roster answers WHO may be called: nobody, so the tools have
|
|
753
|
+
// nothing to act on. `allowDelegation: false` answers WHETHER this run may
|
|
754
|
+
// call anyone, which a non-empty roster cannot settle — a host that runs a
|
|
755
|
+
// specialist by putting its persona into the supervisor shell and its id
|
|
756
|
+
// into the roster has a list of one and must still delegate to nobody.
|
|
757
|
+
// From inside this function that run is indistinguishable from a
|
|
758
|
+
// supervisor whose roster happens to hold a single specialist, so the
|
|
759
|
+
// caller states the fact rather than the SDK guessing it.
|
|
760
|
+
//
|
|
761
|
+
// `!== false` rather than truthiness, so an absent flag keeps today's
|
|
762
|
+
// behaviour exactly.
|
|
763
|
+
const canDelegate = agentIds.length > 0 && allowDelegation !== false
|
|
764
|
+
const tools: ToolDefinition[] = canDelegate
|
|
765
|
+
? [createTask, waitForTaskTool, cancelTask, agentTaskList]
|
|
766
|
+
: [agentTaskList]
|
|
720
767
|
|
|
721
768
|
if (getPlanManager) {
|
|
722
769
|
const approvePlan = defineTool({
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { TaskGateway, TaskHandle } from '../../types/agent/gateway.js'
|
|
2
|
+
import type { TaskId } from '../../types/ids/index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Waiting on a delegated worker, bounded by two different questions.
|
|
6
|
+
*
|
|
7
|
+
* A wall clock alone is the wrong instrument. It has to be long enough to
|
|
8
|
+
* serve as the outer bound for a child doing real work — an hour, here — and
|
|
9
|
+
* that is far too long to notice a child that wedged in its second minute.
|
|
10
|
+
* The same number cannot be both "how long is too long" and "how quiet is too
|
|
11
|
+
* quiet", so this keeps them apart:
|
|
12
|
+
*
|
|
13
|
+
* - the **run bound** counts elapsed time and is never refreshed. It exists
|
|
14
|
+
* for a worker that stays busy forever.
|
|
15
|
+
* - the **idle bound** counts time since the worker last did anything, and
|
|
16
|
+
* resets whenever it does. It exists for a worker that stopped.
|
|
17
|
+
*
|
|
18
|
+
* Whichever fires first ends the wait, and the result says WHICH — because
|
|
19
|
+
* "it went quiet" and "it ran too long" are different diagnoses and lead to
|
|
20
|
+
* different next moves. Telling a caller its worker timed out when the worker
|
|
21
|
+
* was making steady progress is the failure this replaces.
|
|
22
|
+
*
|
|
23
|
+
* The idle bound is only armed when the gateway can report progress.
|
|
24
|
+
* `onTaskProgress` is optional on the contract, because hosts implement
|
|
25
|
+
* `TaskGateway` and not all of them can observe their children — so a gateway
|
|
26
|
+
* without it is bounded by the wall clock alone, exactly as before. That is a
|
|
27
|
+
* real degradation and it is deliberately visible in the result rather than
|
|
28
|
+
* silent: `idleBoundArmed` says whether the quieter half was ever watching.
|
|
29
|
+
*/
|
|
30
|
+
export type WaitOutcome =
|
|
31
|
+
| { readonly kind: 'completed'; readonly handle: TaskHandle }
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: 'timeout'
|
|
34
|
+
/** Which clock ran out. */
|
|
35
|
+
readonly cause: 'idle' | 'run'
|
|
36
|
+
readonly elapsedMs: number
|
|
37
|
+
/** False when the gateway cannot report progress, so only the wall clock applied. */
|
|
38
|
+
readonly idleBoundArmed: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WaitBounds {
|
|
42
|
+
/** Elapsed-time ceiling, never refreshed. */
|
|
43
|
+
readonly runMs: number
|
|
44
|
+
/**
|
|
45
|
+
* Time-without-progress ceiling, refreshed on every progress signal.
|
|
46
|
+
*
|
|
47
|
+
* Omit to bound by the run clock alone.
|
|
48
|
+
*/
|
|
49
|
+
readonly idleMs?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Await a task under both bounds.
|
|
54
|
+
*
|
|
55
|
+
* Note what this does NOT do: it does not cancel the worker. A wait that ran
|
|
56
|
+
* out is a statement about the waiter, not about the work — the child keeps
|
|
57
|
+
* going, its completion still reaches the inbox, and the supervisor is still
|
|
58
|
+
* told what it produced. Killing a child because a parent stopped waiting was
|
|
59
|
+
* never asked for, and losing an eight-minute worker's output because a
|
|
60
|
+
* two-minute clock expired is the exact shape of the bug this whole area has
|
|
61
|
+
* been unpicking.
|
|
62
|
+
*/
|
|
63
|
+
export async function waitForTaskWithBounds(
|
|
64
|
+
gateway: TaskGateway,
|
|
65
|
+
taskId: TaskId,
|
|
66
|
+
bounds: WaitBounds,
|
|
67
|
+
now: () => number = Date.now,
|
|
68
|
+
): Promise<WaitOutcome> {
|
|
69
|
+
const startedAt = now()
|
|
70
|
+
let lastProgressAt = startedAt
|
|
71
|
+
let settled = false
|
|
72
|
+
|
|
73
|
+
const detach = gateway.onTaskProgress?.((id) => {
|
|
74
|
+
if (id === taskId) lastProgressAt = now()
|
|
75
|
+
})
|
|
76
|
+
const idleBoundArmed = detach !== undefined && bounds.idleMs !== undefined
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const completion = gateway.waitForTask(taskId).then(
|
|
80
|
+
(handle): WaitOutcome => ({ kind: 'completed', handle }),
|
|
81
|
+
// A gateway that rejects has answered the question; let it through
|
|
82
|
+
// rather than reporting a timeout that did not happen.
|
|
83
|
+
(err) => {
|
|
84
|
+
throw err
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const expiry = new Promise<WaitOutcome>((resolve) => {
|
|
89
|
+
// Polled rather than scheduled, because the idle deadline MOVES: a
|
|
90
|
+
// timer armed for it would have to be cleared and rearmed on every
|
|
91
|
+
// tick of progress, and the one that slipped through would be the
|
|
92
|
+
// one that mattered. A coarse tick is enough — these bounds are
|
|
93
|
+
// minutes, and being a second late to notice silence costs nothing.
|
|
94
|
+
const tick = setInterval(() => {
|
|
95
|
+
if (settled) return
|
|
96
|
+
const elapsed = now() - startedAt
|
|
97
|
+
if (elapsed >= bounds.runMs) {
|
|
98
|
+
clearInterval(tick)
|
|
99
|
+
resolve({ kind: 'timeout', cause: 'run', elapsedMs: elapsed, idleBoundArmed })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (idleBoundArmed && bounds.idleMs !== undefined) {
|
|
103
|
+
const quietFor = now() - lastProgressAt
|
|
104
|
+
if (quietFor >= bounds.idleMs) {
|
|
105
|
+
clearInterval(tick)
|
|
106
|
+
resolve({ kind: 'timeout', cause: 'idle', elapsedMs: elapsed, idleBoundArmed })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, POLL_INTERVAL_MS)
|
|
110
|
+
// Never the reason a process stays alive. This one is safe to unref
|
|
111
|
+
// where the park recorder was not, because nothing AWAITS it alone:
|
|
112
|
+
// it races a real completion promise, so the wait is held open by
|
|
113
|
+
// work that is genuinely outstanding rather than by this timer.
|
|
114
|
+
;(tick as { unref?: () => void }).unref?.()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return await Promise.race([completion, expiry])
|
|
118
|
+
} finally {
|
|
119
|
+
settled = true
|
|
120
|
+
detach?.()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How often the bounds are checked.
|
|
126
|
+
*
|
|
127
|
+
* Coarse on purpose: both bounds are measured in minutes, so a second of
|
|
128
|
+
* latency in noticing is irrelevant, and a tight interval would spend a timer
|
|
129
|
+
* wakeup per second per in-flight worker for nothing.
|
|
130
|
+
*/
|
|
131
|
+
const POLL_INTERVAL_MS = 1_000
|
|
132
|
+
|
|
133
|
+
/** What to tell the model, in the words that fit what actually happened. */
|
|
134
|
+
export function describeWaitTimeout(outcome: Extract<WaitOutcome, { kind: 'timeout' }>): string {
|
|
135
|
+
const seconds = Math.round(outcome.elapsedMs / 1000)
|
|
136
|
+
if (outcome.cause === 'idle') {
|
|
137
|
+
return `This worker went quiet: nothing has come from it for a while, after ${seconds}s. It has not been cancelled and may still finish — its result will arrive as a task notification if it does. Check agent_task_list, or start a different approach.`
|
|
138
|
+
}
|
|
139
|
+
return outcome.idleBoundArmed
|
|
140
|
+
? `This worker has been running for ${seconds}s without finishing, though it was still doing something. It has not been cancelled — its result will arrive as a task notification if it finishes.`
|
|
141
|
+
: `This worker has been running for ${seconds}s without finishing. This gateway cannot report progress, so there is no way to tell a busy worker from a stuck one here. It has not been cancelled — its result will arrive as a task notification if it finishes.`
|
|
142
|
+
}
|
package/src/types/agent/base.ts
CHANGED
|
@@ -24,6 +24,23 @@ export interface BaseAgentConfig {
|
|
|
24
24
|
permissionMode?: PermissionMode
|
|
25
25
|
env?: Record<string, string>
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Thinking mode and response-effort level for every model call this agent
|
|
29
|
+
* makes. See {@link import('../run/config.js').AgentRunConfig} for what
|
|
30
|
+
* each one controls and why they are siblings.
|
|
31
|
+
*
|
|
32
|
+
* They are declared HERE, on the shared base, rather than on each agent
|
|
33
|
+
* config that happens to want them. Every agent builds its `AgentRunConfig`
|
|
34
|
+
* by hand-listing fields, and a field absent from a hand-listed literal is
|
|
35
|
+
* dropped in silence — which is exactly how `thinking` came to be settable
|
|
36
|
+
* only through the raw kernel entry point while every ergonomic one quietly
|
|
37
|
+
* ignored it. Putting them on the base is what makes "did you forget to
|
|
38
|
+
* forward it" a type error in the places that matter rather than a support
|
|
39
|
+
* question.
|
|
40
|
+
*/
|
|
41
|
+
thinking?: import('../provider/index.js').ThinkingConfig
|
|
42
|
+
effort?: import('../provider/index.js').ReasoningEffort
|
|
43
|
+
|
|
27
44
|
/**
|
|
28
45
|
* Deduplicate a retried invocation instead of running it twice.
|
|
29
46
|
*
|
|
@@ -57,4 +57,28 @@ export interface TaskGateway {
|
|
|
57
57
|
listTasks(): TaskHandle[]
|
|
58
58
|
|
|
59
59
|
onTaskCompleted(callback: (handle: TaskHandle) => void): () => void
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Tell me when a task does something, not just when it finishes.
|
|
63
|
+
*
|
|
64
|
+
* This is what an idle bound is measured against. A wall clock says
|
|
65
|
+
* nothing about whether a worker is working: an hour is long enough to
|
|
66
|
+
* be useless as a stall detector, and short enough to kill a child that
|
|
67
|
+
* is making steady progress at minute fifty-nine. Time-without-progress
|
|
68
|
+
* is the quantity that separates "stuck" from "slow", and only the
|
|
69
|
+
* gateway can see it.
|
|
70
|
+
*
|
|
71
|
+
* OPTIONAL, and the absence is meaningful rather than an oversight: a
|
|
72
|
+
* gateway that cannot observe its children still works, and its waits
|
|
73
|
+
* are bounded by the wall clock alone — which is exactly the behaviour
|
|
74
|
+
* before this existed. It is optional because `TaskGateway` is
|
|
75
|
+
* implemented by hosts, and a required method would break every one of
|
|
76
|
+
* them for a capability not all of them can provide.
|
|
77
|
+
*
|
|
78
|
+
* Anything the worker did counts: a tool call, an emitted token, a state
|
|
79
|
+
* change. What must NOT count is the supervisor's own activity — the
|
|
80
|
+
* point is to notice a child that has gone quiet, and a parent polling
|
|
81
|
+
* about it is not the child speaking.
|
|
82
|
+
*/
|
|
83
|
+
onTaskProgress?(callback: (taskId: TaskId) => void): () => void
|
|
60
84
|
}
|