@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.
Files changed (185) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +5 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +9 -0
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js +31 -0
  9. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js.map +1 -1
  10. package/dist/agents/runAgent.d.ts +19 -1
  11. package/dist/agents/runAgent.d.ts.map +1 -1
  12. package/dist/agents/runAgent.js +2 -0
  13. package/dist/agents/runAgent.js.map +1 -1
  14. package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts +2 -0
  15. package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts.map +1 -0
  16. package/dist/connector/mcp/__tests__/positional-arrays.test.js +142 -0
  17. package/dist/connector/mcp/__tests__/positional-arrays.test.js.map +1 -0
  18. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  19. package/dist/connector/mcp/adapter.js +123 -8
  20. package/dist/connector/mcp/adapter.js.map +1 -1
  21. package/dist/constants/agent/index.d.ts +5 -0
  22. package/dist/constants/agent/index.d.ts.map +1 -1
  23. package/dist/constants/agent/index.js +5 -0
  24. package/dist/constants/agent/index.js.map +1 -1
  25. package/dist/constants/plugin/index.d.ts +15 -0
  26. package/dist/constants/plugin/index.d.ts.map +1 -1
  27. package/dist/constants/plugin/index.js +15 -0
  28. package/dist/constants/plugin/index.js.map +1 -1
  29. package/dist/gateway/__tests__/completion-inbox.test.js +17 -0
  30. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
  31. package/dist/gateway/completion-inbox.d.ts.map +1 -1
  32. package/dist/gateway/completion-inbox.js +17 -3
  33. package/dist/gateway/completion-inbox.js.map +1 -1
  34. package/dist/gateway/local.d.ts +11 -0
  35. package/dist/gateway/local.d.ts.map +1 -1
  36. package/dist/gateway/local.js +27 -1
  37. package/dist/gateway/local.js.map +1 -1
  38. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  39. package/dist/manager/agent/lifecycle.js +6 -0
  40. package/dist/manager/agent/lifecycle.js.map +1 -1
  41. package/dist/provider/thinking-support.d.ts +2 -1
  42. package/dist/provider/thinking-support.d.ts.map +1 -1
  43. package/dist/provider/thinking-support.js +14 -0
  44. package/dist/provider/thinking-support.js.map +1 -1
  45. package/dist/public-runtime.d.ts +1 -1
  46. package/dist/public-runtime.d.ts.map +1 -1
  47. package/dist/public-runtime.js +9 -1
  48. package/dist/public-runtime.js.map +1 -1
  49. package/dist/runtime/query/__tests__/context.test.js +18 -0
  50. package/dist/runtime/query/__tests__/context.test.js.map +1 -1
  51. package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts +2 -0
  52. package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts.map +1 -0
  53. package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js +118 -0
  54. package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js.map +1 -0
  55. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts +2 -0
  56. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts.map +1 -0
  57. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js +122 -0
  58. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js.map +1 -0
  59. package/dist/runtime/query/__tests__/tool-timeout.test.js +34 -0
  60. package/dist/runtime/query/__tests__/tool-timeout.test.js.map +1 -1
  61. package/dist/runtime/query/context.d.ts.map +1 -1
  62. package/dist/runtime/query/context.js +16 -1
  63. package/dist/runtime/query/context.js.map +1 -1
  64. package/dist/runtime/query/executor.d.ts.map +1 -1
  65. package/dist/runtime/query/executor.js +11 -1
  66. package/dist/runtime/query/executor.js.map +1 -1
  67. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  68. package/dist/runtime/query/iteration/index.js +5 -0
  69. package/dist/runtime/query/iteration/index.js.map +1 -1
  70. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  71. package/dist/runtime/query/iteration/phases/context.js +43 -13
  72. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  73. package/dist/telemetry/__tests__/model-call-span.test.js +22 -4
  74. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -1
  75. package/dist/telemetry/__tests__/span-closure.test.js +12 -5
  76. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -1
  77. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts +2 -0
  78. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts.map +1 -0
  79. package/dist/tools/builtins/__tests__/bash.proc-test.js +115 -0
  80. package/dist/tools/builtins/__tests__/bash.proc-test.js.map +1 -0
  81. package/dist/tools/builtins/__tests__/bash.test.d.ts +2 -0
  82. package/dist/tools/builtins/__tests__/bash.test.d.ts.map +1 -0
  83. package/dist/tools/builtins/__tests__/bash.test.js +65 -0
  84. package/dist/tools/builtins/__tests__/bash.test.js.map +1 -0
  85. package/dist/tools/builtins/bash.d.ts.map +1 -1
  86. package/dist/tools/builtins/bash.js +89 -17
  87. package/dist/tools/builtins/bash.js.map +1 -1
  88. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts +2 -0
  89. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts.map +1 -0
  90. package/dist/tools/coordinator/__tests__/allow-delegation.test.js +94 -0
  91. package/dist/tools/coordinator/__tests__/allow-delegation.test.js.map +1 -0
  92. package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts +2 -0
  93. package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts.map +1 -0
  94. package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js +193 -0
  95. package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js.map +1 -0
  96. package/dist/tools/coordinator/index.d.ts +28 -0
  97. package/dist/tools/coordinator/index.d.ts.map +1 -1
  98. package/dist/tools/coordinator/index.js +101 -62
  99. package/dist/tools/coordinator/index.js.map +1 -1
  100. package/dist/tools/coordinator/wait-with-idle-bound.d.ts +66 -0
  101. package/dist/tools/coordinator/wait-with-idle-bound.d.ts.map +1 -0
  102. package/dist/tools/coordinator/wait-with-idle-bound.js +78 -0
  103. package/dist/tools/coordinator/wait-with-idle-bound.js.map +1 -0
  104. package/dist/types/agent/base.d.ts +16 -0
  105. package/dist/types/agent/base.d.ts.map +1 -1
  106. package/dist/types/agent/gateway.d.ts +23 -0
  107. package/dist/types/agent/gateway.d.ts.map +1 -1
  108. package/dist/types/agent/lifecycle-event.d.ts +9 -1
  109. package/dist/types/agent/lifecycle-event.d.ts.map +1 -1
  110. package/dist/types/agent/supervisor.d.ts +27 -0
  111. package/dist/types/agent/supervisor.d.ts.map +1 -1
  112. package/dist/types/agent/task.d.ts +5 -0
  113. package/dist/types/agent/task.d.ts.map +1 -1
  114. package/dist/types/hitl/index.d.ts +10 -0
  115. package/dist/types/hitl/index.d.ts.map +1 -1
  116. package/dist/types/hitl/index.js.map +1 -1
  117. package/dist/types/probe/registry.d.ts +6 -0
  118. package/dist/types/probe/registry.d.ts.map +1 -1
  119. package/dist/types/run/config.d.ts +25 -0
  120. package/dist/types/run/config.d.ts.map +1 -1
  121. package/dist/utils/__tests__/abort-reason.test.d.ts +2 -0
  122. package/dist/utils/__tests__/abort-reason.test.d.ts.map +1 -0
  123. package/dist/utils/__tests__/abort-reason.test.js +48 -0
  124. package/dist/utils/__tests__/abort-reason.test.js.map +1 -0
  125. package/dist/utils/abort.d.ts +26 -0
  126. package/dist/utils/abort.d.ts.map +1 -1
  127. package/dist/utils/abort.js +34 -0
  128. package/dist/utils/abort.js.map +1 -1
  129. package/dist/verification/__tests__/rule-order-and-reason.test.d.ts +2 -0
  130. package/dist/verification/__tests__/rule-order-and-reason.test.d.ts.map +1 -0
  131. package/dist/verification/__tests__/rule-order-and-reason.test.js +126 -0
  132. package/dist/verification/__tests__/rule-order-and-reason.test.js.map +1 -0
  133. package/dist/verification/gate.d.ts +17 -1
  134. package/dist/verification/gate.d.ts.map +1 -1
  135. package/dist/verification/gate.js +58 -2
  136. package/dist/verification/gate.js.map +1 -1
  137. package/dist/verification/index.d.ts +1 -1
  138. package/dist/verification/index.d.ts.map +1 -1
  139. package/dist/verification/index.js +1 -1
  140. package/dist/verification/index.js.map +1 -1
  141. package/package.json +3 -2
  142. package/src/agents/ReactiveAgent.ts +5 -0
  143. package/src/agents/SupervisorAgent.ts +9 -0
  144. package/src/agents/__tests__/supervisor-coordinator-registration.test.ts +37 -0
  145. package/src/agents/runAgent.ts +22 -1
  146. package/src/connector/mcp/__tests__/positional-arrays.test.ts +183 -0
  147. package/src/connector/mcp/adapter.ts +131 -7
  148. package/src/constants/agent/index.ts +5 -0
  149. package/src/constants/plugin/index.ts +15 -0
  150. package/src/gateway/__tests__/completion-inbox.test.ts +23 -0
  151. package/src/gateway/completion-inbox.ts +17 -3
  152. package/src/gateway/local.ts +26 -1
  153. package/src/manager/agent/lifecycle.ts +6 -0
  154. package/src/provider/thinking-support.ts +19 -2
  155. package/src/public-runtime.ts +9 -0
  156. package/src/runtime/query/__tests__/context.test.ts +24 -0
  157. package/src/runtime/query/__tests__/effort-reaches-the-wire.test.ts +135 -0
  158. package/src/runtime/query/__tests__/run-survives-its-own-park.proc-test.ts +127 -0
  159. package/src/runtime/query/__tests__/tool-timeout.test.ts +38 -0
  160. package/src/runtime/query/context.ts +16 -1
  161. package/src/runtime/query/executor.ts +11 -1
  162. package/src/runtime/query/iteration/index.ts +5 -0
  163. package/src/runtime/query/iteration/phases/context.ts +41 -15
  164. package/src/telemetry/__tests__/model-call-span.test.ts +22 -5
  165. package/src/telemetry/__tests__/span-closure.test.ts +12 -5
  166. package/src/tools/builtins/__tests__/bash.proc-test.ts +142 -0
  167. package/src/tools/builtins/__tests__/bash.test.ts +79 -0
  168. package/src/tools/builtins/bash.ts +100 -18
  169. package/src/tools/coordinator/__tests__/allow-delegation.test.ts +120 -0
  170. package/src/tools/coordinator/__tests__/wait-with-idle-bound.test.ts +247 -0
  171. package/src/tools/coordinator/index.ts +112 -65
  172. package/src/tools/coordinator/wait-with-idle-bound.ts +142 -0
  173. package/src/types/agent/base.ts +17 -0
  174. package/src/types/agent/gateway.ts +24 -0
  175. package/src/types/agent/lifecycle-event.ts +7 -0
  176. package/src/types/agent/supervisor.ts +28 -0
  177. package/src/types/agent/task.ts +5 -0
  178. package/src/types/hitl/index.ts +10 -0
  179. package/src/types/probe/registry.ts +6 -0
  180. package/src/types/run/config.ts +26 -0
  181. package/src/utils/__tests__/abort-reason.test.ts +56 -0
  182. package/src/utils/abort.ts +34 -0
  183. package/src/verification/__tests__/rule-order-and-reason.test.ts +149 -0
  184. package/src/verification/gate.ts +61 -3
  185. 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
+ })
@@ -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
- const description = schema.description
97
- if (typeof description === 'string' && description.length > 0) {
98
- base = base.describe(description)
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
- // A tuple (`items` as an array) is rare in tool schemas; treat it
234
- // as a heterogeneous list rather than pretending to model it.
235
- if (Array.isArray(items)) return z.array(z.unknown())
236
- return z.array(items === undefined ? z.unknown() : jsonSchemaPropertyToZod(items))
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
- this.unheard.delete(taskId)
163
- // Anyone waiting should re-check rather than sit out their deadline
164
- // for a task that is no longer coming.
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
 
@@ -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.listener,
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(