@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
|
@@ -110,6 +110,11 @@ export class ReactiveAgent extends AbstractAgent<ReactiveAgentConfig, ReactiveAg
|
|
|
110
110
|
costLimitUsd: config.costLimitUsd,
|
|
111
111
|
permissionMode: config.permissionMode,
|
|
112
112
|
env: config.env,
|
|
113
|
+
// Hand-listed, so anything not named here is dropped in silence.
|
|
114
|
+
// That is how both of these came to be unreachable from every
|
|
115
|
+
// entry point except the raw kernel one.
|
|
116
|
+
...(config.thinking ? { thinking: config.thinking } : {}),
|
|
117
|
+
...(config.effort ? { effort: config.effort } : {}),
|
|
113
118
|
},
|
|
114
119
|
agentId: this.metadata.id,
|
|
115
120
|
agentName: this.metadata.name,
|
|
@@ -191,6 +191,11 @@ export class SupervisorAgent extends AbstractAgent<SupervisorAgentConfig, Superv
|
|
|
191
191
|
workingDirectory: input.workingDirectory,
|
|
192
192
|
runtimeContext: input.runtimeContext,
|
|
193
193
|
allowedAgentIds: config.agentIds,
|
|
194
|
+
// The only hop between the config and the decision. Omit it and
|
|
195
|
+
// everything still compiles: the field is settable, documented, and
|
|
196
|
+
// read by nothing — which is the shape of a declaration this repo
|
|
197
|
+
// has had to go and delete before.
|
|
198
|
+
allowDelegation: config.allowDelegation,
|
|
194
199
|
taskStore: input.taskStore,
|
|
195
200
|
runId,
|
|
196
201
|
getPlanManager: () => planManagerRef,
|
|
@@ -282,6 +287,10 @@ export class SupervisorAgent extends AbstractAgent<SupervisorAgentConfig, Superv
|
|
|
282
287
|
maxIterations: config.maxIterations,
|
|
283
288
|
temperature: config.temperature,
|
|
284
289
|
env: config.env,
|
|
290
|
+
// See ReactiveAgent: a hand-listed literal drops what nobody
|
|
291
|
+
// remembered to add, and reports nothing when it does.
|
|
292
|
+
...(config.thinking ? { thinking: config.thinking } : {}),
|
|
293
|
+
...(config.effort ? { effort: config.effort } : {}),
|
|
285
294
|
},
|
|
286
295
|
questionParks,
|
|
287
296
|
pendingAnswers,
|
|
@@ -55,6 +55,7 @@ const hostTool = (name: string) =>
|
|
|
55
55
|
async function runWith(options: {
|
|
56
56
|
hostTools?: string[]
|
|
57
57
|
runtimeToolOverrides?: Record<string, 'active' | 'deferred' | 'disabled'>
|
|
58
|
+
allowDelegation?: boolean
|
|
58
59
|
}) {
|
|
59
60
|
const agent = new SupervisorAgent({
|
|
60
61
|
id: 'supervisor',
|
|
@@ -80,6 +81,9 @@ async function runWith(options: {
|
|
|
80
81
|
{
|
|
81
82
|
provider,
|
|
82
83
|
agentIds: ['worker'],
|
|
84
|
+
...(options.allowDelegation !== undefined
|
|
85
|
+
? { allowDelegation: options.allowDelegation }
|
|
86
|
+
: {}),
|
|
83
87
|
agentManager: stubManager(),
|
|
84
88
|
tools,
|
|
85
89
|
systemPrompt: 'You coordinate.',
|
|
@@ -146,3 +150,36 @@ describe('supervisor coordinator-tool registration', () => {
|
|
|
146
150
|
expect(describedAs('create_task')).toBe(HOST_TOOL_DESCRIPTION)
|
|
147
151
|
})
|
|
148
152
|
})
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The one hop between `SupervisorAgentConfig.allowDelegation` and the builder
|
|
156
|
+
* that acts on it.
|
|
157
|
+
*
|
|
158
|
+
* These go through `SupervisorAgent` rather than calling the builder directly,
|
|
159
|
+
* and that is the entire point. The builder has its own unit tests, and they
|
|
160
|
+
* pass whether or not the supervisor actually forwards the flag — measured:
|
|
161
|
+
* deleting the forward left the type-check clean and all 143 coordinator and
|
|
162
|
+
* agent tests green, with the field settable, documented, and read by nobody.
|
|
163
|
+
* That is the shape of a declaration this repository has had to go and delete
|
|
164
|
+
* before, so it gets a test that fails when the road is cut.
|
|
165
|
+
*/
|
|
166
|
+
describe('allowDelegation reaches the tool surface', () => {
|
|
167
|
+
it('withholds the delegation tools when the run declines to delegate', async () => {
|
|
168
|
+
const { names } = await runWith({ allowDelegation: false })
|
|
169
|
+
|
|
170
|
+
expect(names, 'the flag never reached buildCoordinatorTools').not.toContain('create_task')
|
|
171
|
+
expect(names).not.toContain('wait_for_task')
|
|
172
|
+
expect(names).not.toContain('cancel_task')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('keeps the listing, so a non-delegating run can still see what is running', async () => {
|
|
176
|
+
expect((await runWith({ allowDelegation: false })).names).toContain('agent_task_list')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('leaves an opting-in run exactly as it was', async () => {
|
|
180
|
+
const { names } = await runWith({ allowDelegation: true })
|
|
181
|
+
|
|
182
|
+
expect(names).toContain('create_task')
|
|
183
|
+
expect(names).toContain('agent_task_list')
|
|
184
|
+
})
|
|
185
|
+
})
|
package/src/agents/runAgent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ToolRegistry } from '../registry/tool/execute.js'
|
|
|
2
2
|
import { drainQuery } from '../runtime/query/index.js'
|
|
3
3
|
import type { ProjectId, SessionId, TenantId, ThreadId } from '../types/ids/index.js'
|
|
4
4
|
import type { Message } from '../types/message/index.js'
|
|
5
|
-
import type { LLMProvider } from '../types/provider/index.js'
|
|
5
|
+
import type { LLMProvider, ReasoningEffort, ThinkingConfig } from '../types/provider/index.js'
|
|
6
6
|
import type { Run, RunEventListener } from '../types/run/index.js'
|
|
7
7
|
import type { Skill } from '../types/skills/index.js'
|
|
8
8
|
import type { ToolRegistryContract } from '../types/tool/index.js'
|
|
@@ -83,6 +83,25 @@ export interface RunAgentOptions extends AgentIdentity {
|
|
|
83
83
|
timeoutMs?: number
|
|
84
84
|
temperature?: number
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Extended-thinking request and response-effort level, forwarded on every
|
|
88
|
+
* model call.
|
|
89
|
+
*
|
|
90
|
+
* These are here because the run config below is assembled by HAND, and a
|
|
91
|
+
* hand-listed literal silently drops whatever nobody remembered to add —
|
|
92
|
+
* which is precisely what happened. `thinking` shipped on `AgentRunConfig`
|
|
93
|
+
* and was reachable only from the raw kernel entry point, because this
|
|
94
|
+
* function, `ReactiveAgent` and `SupervisorAgent` each rebuilt the object
|
|
95
|
+
* from a fixed list. So the capability existed and the front door could not
|
|
96
|
+
* open it.
|
|
97
|
+
*
|
|
98
|
+
* A live run is what found it: the unit tests passed because they drove the
|
|
99
|
+
* kernel directly, and a real agent run through this function put no effort
|
|
100
|
+
* on the wire at all.
|
|
101
|
+
*/
|
|
102
|
+
thinking?: ThinkingConfig
|
|
103
|
+
effort?: ReasoningEffort
|
|
104
|
+
|
|
86
105
|
/** Names the agent in traces and events. Defaults to `Agent`. */
|
|
87
106
|
name?: string
|
|
88
107
|
|
|
@@ -184,6 +203,8 @@ export async function runAgent(options: RunAgentOptions): Promise<RunAgentResult
|
|
|
184
203
|
tokenBudget: options.tokenBudget ?? DEFAULT_TOKEN_BUDGET,
|
|
185
204
|
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
186
205
|
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
206
|
+
...(options.thinking ? { thinking: options.thinking } : {}),
|
|
207
|
+
...(options.effort ? { effort: options.effort } : {}),
|
|
187
208
|
},
|
|
188
209
|
// One option covers both. `drainQuery` separates the id from the
|
|
189
210
|
// display name because a fleet needs a stable key and a readable
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { toSchemaDialect } from '../../../registry/tool/dialect.js'
|
|
4
|
+
import { renderToolSchema } from '../../../registry/tool/schema.js'
|
|
5
|
+
import type { MCPJsonSchema } from '../../../types/connector/index.js'
|
|
6
|
+
import { mcpJsonSchemaToZod } from '../adapter.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A bridged tool's schema makes a round trip — server JSON Schema → Zod →
|
|
10
|
+
* JSON Schema on the wire — so this file asserts what comes out the FAR end,
|
|
11
|
+
* not what the Zod type is. Two failures live at that far end and neither is
|
|
12
|
+
* visible from the Zod side:
|
|
13
|
+
*
|
|
14
|
+
* - whatever the conversion drops is dropped from what the MODEL is shown,
|
|
15
|
+
* and a positional array was being flattened to "an array of anything";
|
|
16
|
+
* - whatever it emits has to be a construct the receiving wire accepts, and
|
|
17
|
+
* a rejected tool schema fails the WHOLE request rather than degrading one
|
|
18
|
+
* tool. So a faithful conversion that cannot be sent is worse than a lossy
|
|
19
|
+
* one that can, which is why the tuple gate is narrow rather than eager.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function wire(schema: MCPJsonSchema): Record<string, unknown> {
|
|
23
|
+
const rendered = renderToolSchema(mcpJsonSchemaToZod(schema))
|
|
24
|
+
const properties = toSchemaDialect(rendered, '2020-12').properties as Record<
|
|
25
|
+
string,
|
|
26
|
+
Record<string, unknown>
|
|
27
|
+
>
|
|
28
|
+
return properties.a as Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const wrap = (a: Record<string, unknown>): MCPJsonSchema =>
|
|
32
|
+
({ type: 'object', properties: { a }, required: ['a'] }) as unknown as MCPJsonSchema
|
|
33
|
+
|
|
34
|
+
describe('a server that pinned its positions gets a tuple', () => {
|
|
35
|
+
it('carries the draft-07 spelling through to bounded prefixItems', () => {
|
|
36
|
+
expect(
|
|
37
|
+
wire(
|
|
38
|
+
wrap({
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: [{ type: 'string' }, { type: 'number' }],
|
|
41
|
+
additionalItems: false,
|
|
42
|
+
minItems: 2,
|
|
43
|
+
maxItems: 2,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
).toEqual({
|
|
47
|
+
type: 'array',
|
|
48
|
+
minItems: 2,
|
|
49
|
+
maxItems: 2,
|
|
50
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('reaches the identical wire shape from the 2020-12 spelling', () => {
|
|
55
|
+
// The two spellings say the same thing and a server may use either.
|
|
56
|
+
// Converging them is the point: a bridged tool should not be shown
|
|
57
|
+
// differently to the model because of which dialect its author wrote.
|
|
58
|
+
expect(
|
|
59
|
+
wire(
|
|
60
|
+
wrap({
|
|
61
|
+
type: 'array',
|
|
62
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
63
|
+
items: false,
|
|
64
|
+
minItems: 2,
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
).toEqual({
|
|
68
|
+
type: 'array',
|
|
69
|
+
minItems: 2,
|
|
70
|
+
maxItems: 2,
|
|
71
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('a positional array the wire cannot carry keeps its shape in words', () => {
|
|
77
|
+
it('falls back when the server did not pin the length', () => {
|
|
78
|
+
// The inversion worth pinning: positional members do NOT constrain
|
|
79
|
+
// length. With no `minItems` the server is permitting a SHORTER array,
|
|
80
|
+
// and a tuple cannot express that — so an absent lower bound is a
|
|
81
|
+
// reason to fall back rather than a detail to round up.
|
|
82
|
+
const result = wire(
|
|
83
|
+
wrap({
|
|
84
|
+
type: 'array',
|
|
85
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(result.prefixItems).toBeUndefined()
|
|
90
|
+
expect(result.description).toContain('[0] string')
|
|
91
|
+
expect(result.description).toContain('[1] number')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('appends the shape to the description rather than replacing it', () => {
|
|
95
|
+
// The case this exists for is a server that documented its argument
|
|
96
|
+
// WELL and used a positional array. Assigning the description would
|
|
97
|
+
// have deleted its sentence to make room for ours.
|
|
98
|
+
const result = wire(
|
|
99
|
+
wrap({
|
|
100
|
+
type: 'array',
|
|
101
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
102
|
+
description: 'A coordinate pair.',
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
expect(result.description).toContain('A coordinate pair.')
|
|
107
|
+
expect(result.description).toContain('[0] string')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('keeps the bounds the server did state', () => {
|
|
111
|
+
// The fallback is a ZodArray precisely so the ordinary constraint pass
|
|
112
|
+
// still carries `minItems`/`maxItems` onto it.
|
|
113
|
+
const result = wire(
|
|
114
|
+
wrap({ type: 'array', prefixItems: [{ type: 'string' }], minItems: 1, maxItems: 9 }),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
expect(result.minItems).toBe(1)
|
|
118
|
+
expect(result.maxItems).toBe(9)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('names enums and literals in the description, not just types', () => {
|
|
122
|
+
const result = wire(
|
|
123
|
+
wrap({
|
|
124
|
+
type: 'array',
|
|
125
|
+
prefixItems: [{ enum: ['r', 'w'] }, { const: 7 }],
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(result.description).toContain('"r"|"w"')
|
|
130
|
+
expect(result.description).toContain('7')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('an ordinary list is untouched', () => {
|
|
135
|
+
it('still renders as a homogeneous array', () => {
|
|
136
|
+
expect(wire(wrap({ type: 'array', items: { type: 'string' } }))).toEqual({
|
|
137
|
+
type: 'array',
|
|
138
|
+
items: { type: 'string' },
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('a deep schema cannot take the process down', () => {
|
|
144
|
+
// `MAX_CONVERSION_DEPTH` promised in its own comment that a node past the
|
|
145
|
+
// ceiling is "left permissive rather than the process being taken down by
|
|
146
|
+
// a stack overflow". That was false for arrays and for unions: the counter
|
|
147
|
+
// was never passed down the array path, and even where it WAS passed
|
|
148
|
+
// correctly — the union path — nothing compared it to anything, because
|
|
149
|
+
// the only comparison lived in the object branch a pure array or union
|
|
150
|
+
// never reaches. A remote server's tool listing is untrusted input, so
|
|
151
|
+
// this was reachable denial of service.
|
|
152
|
+
const nestArrays = (depth: number): MCPJsonSchema => {
|
|
153
|
+
let inner: Record<string, unknown> = { type: 'string' }
|
|
154
|
+
for (let i = 0; i < depth; i += 1) inner = { type: 'array', items: inner }
|
|
155
|
+
return wrap(inner)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nestUnions = (depth: number): MCPJsonSchema => {
|
|
159
|
+
let inner: Record<string, unknown> = { type: 'string' }
|
|
160
|
+
for (let i = 0; i < depth; i += 1) inner = { anyOf: [inner] }
|
|
161
|
+
return wrap(inner)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
it('survives a deeply nested array', () => {
|
|
165
|
+
expect(() => mcpJsonSchemaToZod(nestArrays(5_000))).not.toThrow()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('survives a deeply nested union', () => {
|
|
169
|
+
expect(() => mcpJsonSchemaToZod(nestUnions(5_000))).not.toThrow()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('still converts a shallow schema faithfully', () => {
|
|
173
|
+
// The guard must not be so eager that it flattens ordinary nesting.
|
|
174
|
+
const result = wire(
|
|
175
|
+
wrap({ type: 'array', items: { type: 'array', items: { type: 'number' } } }),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(result).toEqual({
|
|
179
|
+
type: 'array',
|
|
180
|
+
items: { type: 'array', items: { type: 'number' } },
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -93,9 +93,20 @@ function jsonSchemaPropertyToZod(prop: unknown, depth = 0): z.ZodType {
|
|
|
93
93
|
base = base.nullable()
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// Appended, not assigned. The conversion itself can produce a description
|
|
97
|
+
// — a positional array that could not be expressed as a tuple carries its
|
|
98
|
+
// shape here, because that prose is the only place the model learns it —
|
|
99
|
+
// and `.describe()` REPLACES. Overwriting would have silently deleted the
|
|
100
|
+
// note in exactly the case it exists for: a server that documented its
|
|
101
|
+
// argument well AND used a positional array.
|
|
102
|
+
const carried = base.description
|
|
103
|
+
const declared = schema.description
|
|
104
|
+
const parts = [
|
|
105
|
+
typeof declared === 'string' && declared.length > 0 ? declared : undefined,
|
|
106
|
+
carried,
|
|
107
|
+
].filter((part): part is string => typeof part === 'string' && part.length > 0)
|
|
108
|
+
if (parts.length > 0) {
|
|
109
|
+
base = base.describe(parts.join(' '))
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
if (schema.default !== undefined) {
|
|
@@ -195,6 +206,12 @@ function baseTypeToZod(schema: Record<string, unknown>, depth = 0): z.ZodType {
|
|
|
195
206
|
|
|
196
207
|
const composite = (schema.anyOf ?? schema.oneOf) as unknown[] | undefined
|
|
197
208
|
if (Array.isArray(composite) && composite.length > 0) {
|
|
209
|
+
// The ceiling has to be CHECKED here, not merely counted. `depth` was
|
|
210
|
+
// threaded correctly through this branch from the start, and a
|
|
211
|
+
// 5000-deep union still overflowed the stack — because the only
|
|
212
|
+
// comparison against `MAX_CONVERSION_DEPTH` lived in `objectToZod`,
|
|
213
|
+
// which a pure union never reaches.
|
|
214
|
+
if (depth >= MAX_CONVERSION_DEPTH) return z.unknown()
|
|
198
215
|
const members = composite.map((member) => jsonSchemaPropertyToZod(member, depth + 1))
|
|
199
216
|
return members.length === 1
|
|
200
217
|
? (members[0] as z.ZodType)
|
|
@@ -229,11 +246,37 @@ function baseTypeToZod(schema: Record<string, unknown>, depth = 0): z.ZodType {
|
|
|
229
246
|
case 'null':
|
|
230
247
|
return z.null()
|
|
231
248
|
case 'array': {
|
|
249
|
+
// Both the counter and the check. `depth` was never passed to the
|
|
250
|
+
// element conversion below, so the counter reset to zero on every
|
|
251
|
+
// array level — and even threaded it would not have helped, since
|
|
252
|
+
// nothing on this path compared it to anything. Measured before
|
|
253
|
+
// and after: a 5000-deep array schema took the process down with a
|
|
254
|
+
// stack overflow, which is a denial of service reachable from a
|
|
255
|
+
// remote server's tool listing.
|
|
256
|
+
if (depth >= MAX_CONVERSION_DEPTH) return z.array(z.unknown())
|
|
257
|
+
|
|
258
|
+
// A positional array has two spellings and a server may use
|
|
259
|
+
// either: draft-07 puts the member schemas in `items` with the
|
|
260
|
+
// tail rule in `additionalItems`, 2020-12 moved them to
|
|
261
|
+
// `prefixItems` with the tail rule in `items`.
|
|
262
|
+
const positional = Array.isArray(schema.prefixItems)
|
|
263
|
+
? (schema.prefixItems as unknown[])
|
|
264
|
+
: Array.isArray(schema.items)
|
|
265
|
+
? (schema.items as unknown[])
|
|
266
|
+
: undefined
|
|
267
|
+
if (positional) return positionalToZod(positional, schema, depth)
|
|
268
|
+
|
|
232
269
|
const items = schema.items
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
270
|
+
return z.array(
|
|
271
|
+
// A boolean `items` is a tail RULE, not an element schema — it
|
|
272
|
+
// only has meaning next to `prefixItems`, which was handled
|
|
273
|
+
// above. Reaching it here means the server closed an array
|
|
274
|
+
// that has no positions, and an unconstrained element type is
|
|
275
|
+
// the permissive reading of that.
|
|
276
|
+
items === undefined || typeof items === 'boolean'
|
|
277
|
+
? z.unknown()
|
|
278
|
+
: jsonSchemaPropertyToZod(items, depth + 1),
|
|
279
|
+
)
|
|
237
280
|
}
|
|
238
281
|
case 'object':
|
|
239
282
|
return objectToZod(schema, depth)
|
|
@@ -242,6 +285,87 @@ function baseTypeToZod(schema: Record<string, unknown>, depth = 0): z.ZodType {
|
|
|
242
285
|
}
|
|
243
286
|
}
|
|
244
287
|
|
|
288
|
+
/**
|
|
289
|
+
* How many positions we will express as a tuple.
|
|
290
|
+
*
|
|
291
|
+
* Not a correctness bound — a server may pin any arity it likes. It is a
|
|
292
|
+
* prompt-cost bound: every member renders its own schema into the tool
|
|
293
|
+
* definition the model is shown, and past a couple of dozen positions the
|
|
294
|
+
* thing being described is a data payload rather than a call signature. Past
|
|
295
|
+
* the cap the shape still reaches the model, in the description.
|
|
296
|
+
*/
|
|
297
|
+
const MAX_TUPLE_ARITY = 32
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A positional array: a tuple when the server pinned it, a described list
|
|
301
|
+
* otherwise.
|
|
302
|
+
*
|
|
303
|
+
* This used to be `z.array(z.unknown())` unconditionally, so a server that
|
|
304
|
+
* spelled out `[string, number]` had the model told "an array of anything" —
|
|
305
|
+
* the positions, their types and their order all dropped from what the model
|
|
306
|
+
* reads, not merely from what is validated locally.
|
|
307
|
+
*
|
|
308
|
+
* The reason it is not simply converted is that the schema makes a ROUND TRIP:
|
|
309
|
+
* server JSON Schema → Zod → JSON Schema on the wire. So whatever is emitted
|
|
310
|
+
* here has to be a construct the receiving wire accepts, and a construct it
|
|
311
|
+
* rejects fails the ENTIRE request rather than degrading one tool — taking
|
|
312
|
+
* down every run that offered the toolset. A faithful conversion that cannot
|
|
313
|
+
* be sent is strictly worse than a lossy one that can.
|
|
314
|
+
*
|
|
315
|
+
* Hence the narrow gate. A tuple is emitted only where the server itself
|
|
316
|
+
* pinned the arity and closed the tail, because that renders as bounded
|
|
317
|
+
* `prefixItems` — the one positional shape measured as accepted, and the same
|
|
318
|
+
* shape a first-party builtin already ships. Everything else keeps the
|
|
319
|
+
* permissive array and gains the positional shape in its description.
|
|
320
|
+
*
|
|
321
|
+
* The subtlety worth stating, because it inverts the intuition: positional
|
|
322
|
+
* `items`/`prefixItems` does not constrain LENGTH. Without `minItems` the
|
|
323
|
+
* server is permitting a SHORTER array, and a tuple cannot express that — so
|
|
324
|
+
* an absent lower bound is a reason to fall back, not a detail to round up.
|
|
325
|
+
*/
|
|
326
|
+
function positionalToZod(
|
|
327
|
+
positional: readonly unknown[],
|
|
328
|
+
schema: Record<string, unknown>,
|
|
329
|
+
depth: number,
|
|
330
|
+
): z.ZodType {
|
|
331
|
+
// draft-07 spells the tail rule `additionalItems`; 2020-12 spells it
|
|
332
|
+
// `items`, which is only a tail rule when `prefixItems` holds the members.
|
|
333
|
+
const tail = Array.isArray(schema.items) ? schema.additionalItems : schema.items
|
|
334
|
+
|
|
335
|
+
const arity = positional.length
|
|
336
|
+
const pinnedLow = num(schema.minItems) === arity
|
|
337
|
+
const closedHigh = tail === false || num(schema.maxItems) === arity
|
|
338
|
+
|
|
339
|
+
if (arity === 0 || arity > MAX_TUPLE_ARITY || !pinnedLow || !closedHigh) {
|
|
340
|
+
// A ZodArray, deliberately: `applyConstraints` then carries the
|
|
341
|
+
// server's own `minItems`/`maxItems` onto it, so the loose case keeps
|
|
342
|
+
// whatever bounds the server did state.
|
|
343
|
+
return z.array(z.unknown()).describe(describePositional(positional))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const members = positional.map((member) => jsonSchemaPropertyToZod(member, depth + 1))
|
|
347
|
+
// Never `.rest()`. It renders a tail schema this wire has not been measured
|
|
348
|
+
// against, and the gate above has already established there is no tail.
|
|
349
|
+
return z.tuple(members as [z.ZodType, ...z.ZodType[]])
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The positional shape in prose, for the cases a tuple cannot carry. */
|
|
353
|
+
function describePositional(positional: readonly unknown[]): string {
|
|
354
|
+
const shape = positional
|
|
355
|
+
.map((member, index) => `[${index}] ${positionalTypeName(member)}`)
|
|
356
|
+
.join(', ')
|
|
357
|
+
return `Positional array — ${shape}.`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function positionalTypeName(member: unknown): string {
|
|
361
|
+
if (typeof member !== 'object' || member === null) return 'any'
|
|
362
|
+
const schema = member as Record<string, unknown>
|
|
363
|
+
if (schema.const !== undefined) return JSON.stringify(schema.const)
|
|
364
|
+
if (Array.isArray(schema.enum)) return schema.enum.map((v) => JSON.stringify(v)).join('|')
|
|
365
|
+
const type = Array.isArray(schema.type) ? schema.type.join('|') : schema.type
|
|
366
|
+
return typeof type === 'string' ? type : 'any'
|
|
367
|
+
}
|
|
368
|
+
|
|
245
369
|
/**
|
|
246
370
|
* Flatten `allOf` members into one object schema.
|
|
247
371
|
*
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { AgentCapabilities } from '../../types/agent/base.js'
|
|
2
2
|
import type { AgentManagerConfig } from '../../types/agent/task.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* **Nothing reads this.** No activity list is trimmed to it anywhere.
|
|
6
|
+
*
|
|
7
|
+
* @deprecated Unused. Removed in the next major.
|
|
8
|
+
*/
|
|
4
9
|
export const MAX_RECENT_ACTIVITIES = 5
|
|
5
10
|
|
|
6
11
|
export const AGENT_MANAGER_DEFAULTS: Readonly<AgentManagerConfig> = {
|
|
@@ -44,4 +44,19 @@ export const HOOK_TIMEOUT_MS = 5_000
|
|
|
44
44
|
* can sort itself after.
|
|
45
45
|
*/
|
|
46
46
|
export const DEFAULT_HOOK_PRIORITY = 100
|
|
47
|
+
/**
|
|
48
|
+
* **Nothing reads this.** Hooks run sequentially and always have.
|
|
49
|
+
*
|
|
50
|
+
* It reads as a concurrency cap that is in force, which is the misleading
|
|
51
|
+
* kind of dead: a reviewer sees a bound, assumes hook execution is batched
|
|
52
|
+
* at ten, and reasons about plugin behaviour that does not exist.
|
|
53
|
+
*
|
|
54
|
+
* Kept for one release rather than deleted, because it is reachable from the
|
|
55
|
+
* published typings. Do not "make it work" by batching hook execution —
|
|
56
|
+
* ordering is the contract hooks are written against, and parallelising them
|
|
57
|
+
* to justify a constant would change behaviour to match a number nobody
|
|
58
|
+
* chose deliberately.
|
|
59
|
+
*
|
|
60
|
+
* @deprecated Unused. Removed in the next major.
|
|
61
|
+
*/
|
|
47
62
|
export const HOOK_MAX_CONCURRENT = 10
|
|
@@ -192,6 +192,29 @@ describe('a launch nobody is waiting for holds the run open', () => {
|
|
|
192
192
|
expect(inbox.hasPendingWork).toBe(false)
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
+
it('keeps a result that already arrived, even when the task is cancelled', () => {
|
|
196
|
+
// The window: a worker finishes, its completion is queued for the next
|
|
197
|
+
// drain, and the model — told nothing yet, and reading a tool that says
|
|
198
|
+
// it cancels a RUNNING task — cancels it. Clearing the queue here threw
|
|
199
|
+
// away work that was done and output that existed nowhere else.
|
|
200
|
+
//
|
|
201
|
+
// `forget` is about pending work. A finished result is not pending work.
|
|
202
|
+
const { gateway, settle } = fakeGateway()
|
|
203
|
+
const inbox = new CompletionInbox()
|
|
204
|
+
inbox.attach(gateway)
|
|
205
|
+
inbox.expect('tsk_1' as TaskId)
|
|
206
|
+
|
|
207
|
+
settle(handleFor('tsk_1', 'the worker finished before the cancel landed'))
|
|
208
|
+
inbox.forget('tsk_1' as TaskId)
|
|
209
|
+
|
|
210
|
+
const drained = inbox.drain()
|
|
211
|
+
expect(
|
|
212
|
+
drained.map((h) => h.taskId),
|
|
213
|
+
'the finished result was discarded',
|
|
214
|
+
).toEqual(['tsk_1'])
|
|
215
|
+
expect(drained[0]?.result?.result).toBe('the worker finished before the cancel landed')
|
|
216
|
+
})
|
|
217
|
+
|
|
195
218
|
it('stops expecting a task that was cancelled', () => {
|
|
196
219
|
// `expect` is only cleared by a COMPLETION, so a cancelled worker used
|
|
197
220
|
// to keep the run open for the whole grace period, every time it tried
|
|
@@ -159,9 +159,23 @@ export class CompletionInbox {
|
|
|
159
159
|
*/
|
|
160
160
|
forget(taskId: TaskId): void {
|
|
161
161
|
this.outstanding.delete(taskId)
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
//
|
|
162
|
+
// `unheard` is deliberately NOT touched.
|
|
163
|
+
//
|
|
164
|
+
// The two sets mean different things. `outstanding` is pending WORK,
|
|
165
|
+
// and cancelling is exactly the statement that it should stop being
|
|
166
|
+
// waited for. `unheard` is a RESULT that already exists — the worker
|
|
167
|
+
// finished, the completion arrived, and it is queued for the next
|
|
168
|
+
// drain. Clearing it here destroyed that.
|
|
169
|
+
//
|
|
170
|
+
// The window is small and entirely reachable: nothing has told the
|
|
171
|
+
// model the worker finished, and `cancel_task` says it cancels a
|
|
172
|
+
// running task, so cancelling one that has just completed is the
|
|
173
|
+
// obvious move rather than a mistake. The run then reports "cancelled"
|
|
174
|
+
// over work that was done and output that no longer exists anywhere.
|
|
175
|
+
//
|
|
176
|
+
// Note the asymmetry with `claim`, which does clear `unheard` — and is
|
|
177
|
+
// right to, because there a tool has just handed the model the same
|
|
178
|
+
// result. This one hands over nothing.
|
|
165
179
|
for (const wake of [...this.arrivals]) wake()
|
|
166
180
|
}
|
|
167
181
|
|
package/src/gateway/local.ts
CHANGED
|
@@ -39,6 +39,8 @@ export class LocalTaskGateway implements TaskGateway {
|
|
|
39
39
|
private settledHandles: Map<TaskId, TaskHandle> = new Map()
|
|
40
40
|
|
|
41
41
|
private siblingFailurePolicy: SiblingFailurePolicy = 'continue'
|
|
42
|
+
/** See {@link onTaskProgress}. */
|
|
43
|
+
private readonly progressListeners = new Set<(taskId: TaskId) => void>()
|
|
42
44
|
|
|
43
45
|
constructor(
|
|
44
46
|
agentManager: AgentManagerContract,
|
|
@@ -86,7 +88,15 @@ export class LocalTaskGateway implements TaskGateway {
|
|
|
86
88
|
// allocated `maxBudgetFraction` of the SAME number — N x 50% of a
|
|
87
89
|
// budget that only had 100% in it.
|
|
88
90
|
this.taskContext,
|
|
89
|
-
this
|
|
91
|
+
// The host's listener still sees everything it always did; this
|
|
92
|
+
// only tees off the fact that SOMETHING happened, which is what an
|
|
93
|
+
// idle bound measures. The event itself is not forwarded — a
|
|
94
|
+
// progress signal that carried the child's output would be a
|
|
95
|
+
// second, undocumented way to read a worker's work.
|
|
96
|
+
(event) => {
|
|
97
|
+
this.listener?.(event)
|
|
98
|
+
for (const notify of this.progressListeners) notify(task.taskId)
|
|
99
|
+
},
|
|
90
100
|
)
|
|
91
101
|
|
|
92
102
|
this.trackedTaskIds.add(task.taskId)
|
|
@@ -208,6 +218,21 @@ export class LocalTaskGateway implements TaskGateway {
|
|
|
208
218
|
return handles
|
|
209
219
|
}
|
|
210
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Every event a child emits, reduced to "this one is still alive".
|
|
223
|
+
*
|
|
224
|
+
* Deliberately just the id. A caller that wanted the event itself has
|
|
225
|
+
* the run listener; what an idle clock needs is the fact, and passing
|
|
226
|
+
* the payload here would make this a second way to read a worker's
|
|
227
|
+
* output — one nobody documented and nothing frames as untrusted.
|
|
228
|
+
*/
|
|
229
|
+
onTaskProgress(callback: (taskId: TaskId) => void): () => void {
|
|
230
|
+
this.progressListeners.add(callback)
|
|
231
|
+
return () => {
|
|
232
|
+
this.progressListeners.delete(callback)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
onTaskCompleted(callback: (handle: TaskHandle) => void): () => void {
|
|
212
237
|
this.completionListeners.add(callback)
|
|
213
238
|
return () => {
|
|
@@ -296,6 +296,12 @@ export class AgentManager {
|
|
|
296
296
|
parentSpan: options.configOverrides?.parentSpan,
|
|
297
297
|
maxIterations: options.configOverrides?.maxIterations,
|
|
298
298
|
maxResponseTokens: options.configOverrides?.maxResponseTokens,
|
|
299
|
+
// A delegate spawned without a configBuilder lands here, and this
|
|
300
|
+
// list is the only thing it inherits. Omitting these meant a child
|
|
301
|
+
// silently ran at the default depth and effort its parent had
|
|
302
|
+
// deliberately moved off.
|
|
303
|
+
thinking: options.configOverrides?.thinking,
|
|
304
|
+
effort: options.configOverrides?.effort,
|
|
299
305
|
env: options.configOverrides?.env,
|
|
300
306
|
sessionId: spawnRecord.childSessionId,
|
|
301
307
|
threadId: context.threadId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThinkingConfig } from '../types/provider/index.js'
|
|
1
|
+
import type { ReasoningEffort, ThinkingConfig } from '../types/provider/index.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Refuse a thinking request a driver does not implement.
|
|
@@ -27,8 +27,25 @@ import type { ThinkingConfig } from '../types/provider/index.js'
|
|
|
27
27
|
*/
|
|
28
28
|
export function assertThinkingUnsupported(
|
|
29
29
|
driverName: string,
|
|
30
|
-
params: { thinking?: ThinkingConfig },
|
|
30
|
+
params: { thinking?: ThinkingConfig; effort?: ReasoningEffort },
|
|
31
31
|
): void {
|
|
32
|
+
// `effort` is refused on exactly the same reasoning, and it is the worse
|
|
33
|
+
// silence of the two. A dropped `thinking` at least leaves an empty
|
|
34
|
+
// reasoning list a caller could notice; a dropped `effort` leaves a
|
|
35
|
+
// perfectly ordinary answer, so a run someone believes they paid for at
|
|
36
|
+
// `max` is indistinguishable from one at the model's default — including
|
|
37
|
+
// on the bill.
|
|
38
|
+
//
|
|
39
|
+
// Checked before thinking because it is the cheaper mistake to make: a
|
|
40
|
+
// caller reaching for effort on a driver without it has usually pointed a
|
|
41
|
+
// working config at a new provider, and naming the field they set beats
|
|
42
|
+
// naming the neighbouring one.
|
|
43
|
+
if (params.effort !== undefined) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`${driverName} does not implement effort. Silently ignoring it would return an ordinary completion, so a run requested at "${params.effort}" would be indistinguishable from one at the model's default — including in what it cost. Drop \`effort\`, or use a driver that implements it.`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
const type = params.thinking?.type
|
|
33
50
|
if (type !== 'enabled' && type !== 'adaptive') return
|
|
34
51
|
throw new Error(
|