@namzu/sdk 1.0.0 → 1.1.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 (215) hide show
  1. package/CHANGELOG.md +167 -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 +22 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js +47 -14
  8. package/dist/agents/SupervisorAgent.js.map +1 -1
  9. package/dist/agents/__tests__/supervisor-ledger.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-ledger.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-ledger.test.js +131 -0
  12. package/dist/agents/__tests__/supervisor-ledger.test.js.map +1 -0
  13. package/dist/config/runtime.d.ts +24 -0
  14. package/dist/config/runtime.d.ts.map +1 -1
  15. package/dist/config/runtime.js +9 -0
  16. package/dist/config/runtime.js.map +1 -1
  17. package/dist/connector/builtins/webhook.test.js +17 -0
  18. package/dist/connector/builtins/webhook.test.js.map +1 -1
  19. package/dist/connector/index.d.ts +1 -0
  20. package/dist/connector/index.d.ts.map +1 -1
  21. package/dist/connector/index.js +1 -0
  22. package/dist/connector/index.js.map +1 -1
  23. package/dist/connector/mcp/client.d.ts.map +1 -1
  24. package/dist/connector/mcp/client.js +4 -0
  25. package/dist/connector/mcp/client.js.map +1 -1
  26. package/dist/connector/mcp/index.d.ts +1 -0
  27. package/dist/connector/mcp/index.d.ts.map +1 -1
  28. package/dist/connector/mcp/index.js +1 -0
  29. package/dist/connector/mcp/index.js.map +1 -1
  30. package/dist/connector/mcp/streamable-http.d.ts +22 -0
  31. package/dist/connector/mcp/streamable-http.d.ts.map +1 -0
  32. package/dist/connector/mcp/streamable-http.js +128 -0
  33. package/dist/connector/mcp/streamable-http.js.map +1 -0
  34. package/dist/connector/mcp/streamable-http.test.d.ts +2 -0
  35. package/dist/connector/mcp/streamable-http.test.d.ts.map +1 -0
  36. package/dist/connector/mcp/streamable-http.test.js +132 -0
  37. package/dist/connector/mcp/streamable-http.test.js.map +1 -0
  38. package/dist/persona/assembler.d.ts.map +1 -1
  39. package/dist/persona/assembler.js +18 -9
  40. package/dist/persona/assembler.js.map +1 -1
  41. package/dist/provider/collect.d.ts.map +1 -1
  42. package/dist/provider/collect.js +4 -1
  43. package/dist/provider/collect.js.map +1 -1
  44. package/dist/public-runtime.d.ts +1 -1
  45. package/dist/public-runtime.d.ts.map +1 -1
  46. package/dist/public-runtime.js +1 -1
  47. package/dist/public-runtime.js.map +1 -1
  48. package/dist/registry/tool/execute.d.ts +27 -0
  49. package/dist/registry/tool/execute.d.ts.map +1 -1
  50. package/dist/registry/tool/execute.js +124 -21
  51. package/dist/registry/tool/execute.js.map +1 -1
  52. package/dist/registry/tool/execute.test.d.ts +9 -6
  53. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  54. package/dist/registry/tool/execute.test.js +72 -10
  55. package/dist/registry/tool/execute.test.js.map +1 -1
  56. package/dist/registry/toolset/catalog.test.js +62 -0
  57. package/dist/registry/toolset/catalog.test.js.map +1 -1
  58. package/dist/runtime/query/__tests__/deferred-tools.test.js +43 -5
  59. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
  60. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts +2 -0
  61. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts.map +1 -0
  62. package/dist/runtime/query/__tests__/long-document-flow.test.js +154 -0
  63. package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -0
  64. package/dist/runtime/query/__tests__/prompt.test.js +15 -11
  65. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  66. package/dist/runtime/query/index.d.ts +9 -0
  67. package/dist/runtime/query/index.d.ts.map +1 -1
  68. package/dist/runtime/query/index.js +23 -2
  69. package/dist/runtime/query/index.js.map +1 -1
  70. package/dist/runtime/query/iteration/index.d.ts +2 -0
  71. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  72. package/dist/runtime/query/iteration/index.js +99 -6
  73. package/dist/runtime/query/iteration/index.js.map +1 -1
  74. package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
  75. package/dist/runtime/query/iteration/phases/checkpoint.js +2 -2
  76. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  77. package/dist/runtime/query/iteration/phases/compaction.d.ts +8 -0
  78. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/compaction.js +84 -3
  80. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  81. package/dist/runtime/query/iteration/phases/context.d.ts +12 -0
  82. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  83. package/dist/runtime/query/iteration/phases/context.js +53 -0
  84. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  85. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts +2 -0
  86. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts.map +1 -0
  87. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js +73 -0
  88. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js.map +1 -0
  89. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts +2 -0
  90. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts.map +1 -0
  91. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js +120 -0
  92. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js.map +1 -0
  93. package/dist/runtime/query/iteration/phases/tool-review.d.ts +1 -1
  94. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/tool-review.js +7 -2
  96. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts +19 -0
  98. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts.map +1 -0
  99. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js +177 -0
  100. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js.map +1 -0
  101. package/dist/runtime/query/iteration/phases/working-memory.d.ts +31 -0
  102. package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -0
  103. package/dist/runtime/query/iteration/phases/working-memory.js +81 -0
  104. package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -0
  105. package/dist/session/workspace/__tests__/shared-run.test.js +1 -1
  106. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -1
  107. package/dist/session/workspace/shared-run.js +1 -0
  108. package/dist/session/workspace/shared-run.js.map +1 -1
  109. package/dist/skills/loader.test.js +8 -8
  110. package/dist/skills/loader.test.js.map +1 -1
  111. package/dist/streaming/coalesce.test.js.map +1 -1
  112. package/dist/tools/builtins/__tests__/payload-budget.test.js +48 -0
  113. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
  114. package/dist/tools/builtins/__tests__/read-file.test.js +9 -0
  115. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -1
  116. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  117. package/dist/tools/builtins/read-file.js +50 -2
  118. package/dist/tools/builtins/read-file.js.map +1 -1
  119. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  120. package/dist/tools/builtins/search-tools.js +32 -7
  121. package/dist/tools/builtins/search-tools.js.map +1 -1
  122. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts +2 -0
  123. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts.map +1 -0
  124. package/dist/tools/coordinator/__tests__/approve-plan.test.js +114 -0
  125. package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -0
  126. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts +2 -0
  127. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts.map +1 -0
  128. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +309 -0
  129. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -0
  130. package/dist/tools/coordinator/index.d.ts +8 -0
  131. package/dist/tools/coordinator/index.d.ts.map +1 -1
  132. package/dist/tools/coordinator/index.js +195 -15
  133. package/dist/tools/coordinator/index.js.map +1 -1
  134. package/dist/types/agent/index.d.ts +1 -0
  135. package/dist/types/agent/index.d.ts.map +1 -1
  136. package/dist/types/agent/index.js +1 -0
  137. package/dist/types/agent/index.js.map +1 -1
  138. package/dist/types/agent/reactive.d.ts +15 -0
  139. package/dist/types/agent/reactive.d.ts.map +1 -1
  140. package/dist/types/agent/supervisor.d.ts +18 -0
  141. package/dist/types/agent/supervisor.d.ts.map +1 -1
  142. package/dist/types/agent/working-memory.d.ts +29 -0
  143. package/dist/types/agent/working-memory.d.ts.map +1 -0
  144. package/dist/types/agent/working-memory.js +2 -0
  145. package/dist/types/agent/working-memory.js.map +1 -0
  146. package/dist/types/common/index.d.ts +10 -0
  147. package/dist/types/common/index.d.ts.map +1 -1
  148. package/dist/types/common/index.js +18 -0
  149. package/dist/types/common/index.js.map +1 -1
  150. package/dist/types/common/usage-merge.test.d.ts +2 -0
  151. package/dist/types/common/usage-merge.test.d.ts.map +1 -0
  152. package/dist/types/common/usage-merge.test.js +48 -0
  153. package/dist/types/common/usage-merge.test.js.map +1 -0
  154. package/dist/types/connector/mcp.d.ts +9 -2
  155. package/dist/types/connector/mcp.d.ts.map +1 -1
  156. package/dist/types/hitl/index.d.ts +42 -0
  157. package/dist/types/hitl/index.d.ts.map +1 -1
  158. package/dist/types/hitl/index.js +11 -0
  159. package/dist/types/hitl/index.js.map +1 -1
  160. package/dist/types/provider/chat.d.ts +9 -0
  161. package/dist/types/provider/chat.d.ts.map +1 -1
  162. package/dist/version.d.ts.map +1 -1
  163. package/dist/version.js +25 -4
  164. package/dist/version.js.map +1 -1
  165. package/package.json +1 -1
  166. package/src/agents/ReactiveAgent.ts +5 -0
  167. package/src/agents/SupervisorAgent.ts +56 -16
  168. package/src/agents/__tests__/supervisor-ledger.test.ts +185 -0
  169. package/src/config/runtime.ts +9 -0
  170. package/src/connector/builtins/webhook.test.ts +21 -0
  171. package/src/connector/index.ts +1 -0
  172. package/src/connector/mcp/client.ts +4 -0
  173. package/src/connector/mcp/index.ts +1 -0
  174. package/src/connector/mcp/streamable-http.test.ts +169 -0
  175. package/src/connector/mcp/streamable-http.ts +152 -0
  176. package/src/persona/assembler.ts +18 -6
  177. package/src/provider/collect.ts +4 -1
  178. package/src/public-runtime.ts +1 -0
  179. package/src/registry/tool/execute.test.ts +85 -10
  180. package/src/registry/tool/execute.ts +124 -21
  181. package/src/registry/toolset/catalog.test.ts +68 -0
  182. package/src/runtime/query/__tests__/deferred-tools.test.ts +52 -5
  183. package/src/runtime/query/__tests__/long-document-flow.test.ts +180 -0
  184. package/src/runtime/query/__tests__/prompt.test.ts +15 -11
  185. package/src/runtime/query/index.ts +34 -2
  186. package/src/runtime/query/iteration/index.ts +98 -6
  187. package/src/runtime/query/iteration/phases/checkpoint.ts +7 -2
  188. package/src/runtime/query/iteration/phases/compaction.ts +89 -3
  189. package/src/runtime/query/iteration/phases/context.ts +58 -0
  190. package/src/runtime/query/iteration/phases/hitl-abort-race.test.ts +86 -0
  191. package/src/runtime/query/iteration/phases/hitl-answer-question.test.ts +139 -0
  192. package/src/runtime/query/iteration/phases/tool-review.ts +7 -3
  193. package/src/runtime/query/iteration/phases/working-memory-compaction.test.ts +215 -0
  194. package/src/runtime/query/iteration/phases/working-memory.ts +86 -0
  195. package/src/session/workspace/__tests__/shared-run.test.ts +1 -1
  196. package/src/session/workspace/shared-run.ts +1 -0
  197. package/src/skills/loader.test.ts +8 -8
  198. package/src/streaming/coalesce.test.ts +3 -3
  199. package/src/tools/builtins/__tests__/payload-budget.test.ts +63 -0
  200. package/src/tools/builtins/__tests__/read-file.test.ts +12 -0
  201. package/src/tools/builtins/read-file.ts +50 -2
  202. package/src/tools/builtins/search-tools.ts +38 -7
  203. package/src/tools/coordinator/__tests__/approve-plan.test.ts +139 -0
  204. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +370 -0
  205. package/src/tools/coordinator/index.ts +227 -17
  206. package/src/types/agent/index.ts +1 -0
  207. package/src/types/agent/reactive.ts +17 -0
  208. package/src/types/agent/supervisor.ts +20 -0
  209. package/src/types/agent/working-memory.ts +29 -0
  210. package/src/types/common/index.ts +19 -0
  211. package/src/types/common/usage-merge.test.ts +53 -0
  212. package/src/types/connector/mcp.ts +14 -2
  213. package/src/types/hitl/index.ts +52 -1
  214. package/src/types/provider/chat.ts +10 -0
  215. package/src/version.ts +26 -4
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
  import type { PlanManager } from '../../manager/plan/lifecycle.js'
3
3
  import type { AgentRuntimeContext } from '../../types/agent/base.js'
4
4
  import type { TaskGateway } from '../../types/agent/gateway.js'
5
+ import type { ResumeHandler, UserQuestionOption } from '../../types/hitl/index.js'
5
6
  import type { RunId, TaskId } from '../../types/ids/index.js'
6
7
  import type { TaskStore } from '../../types/task/index.js'
7
8
  import type { ToolDefinition } from '../../types/tool/index.js'
@@ -36,6 +37,52 @@ export interface CoordinatorToolsOptions {
36
37
  getPlanManager?: () => PlanManager | undefined
37
38
 
38
39
  onTaskLaunched?: TaskLaunchedCallback
40
+
41
+ /**
42
+ * HITL park channel for `ask_user_question`. The tool is registered
43
+ * only when BOTH `resumeHandler` and `runId` are present — without a
44
+ * handler there is no one to route the question to, and without a
45
+ * runId the park request cannot be addressed.
46
+ */
47
+ resumeHandler?: ResumeHandler
48
+ }
49
+
50
+ const approvePlanStepSchema = z.object({
51
+ description: z.string().describe('What this step does'),
52
+ agent_id: z
53
+ .string()
54
+ .optional()
55
+ .describe('Which agent handles this (omit for orchestrator-owned steps)'),
56
+ depends_on: z.array(z.string()).optional().describe('Step descriptions this depends on'),
57
+ })
58
+
59
+ function normalizeApprovePlanSteps(value: unknown): unknown {
60
+ if (typeof value !== 'string') return value
61
+
62
+ const trimmed = value.trim()
63
+ if (!trimmed) return []
64
+
65
+ if (trimmed.startsWith('[')) {
66
+ try {
67
+ return JSON.parse(trimmed)
68
+ } catch {
69
+ // Fall through to plain-text line parsing.
70
+ }
71
+ }
72
+
73
+ const lines = trimmed
74
+ .split(/\r?\n+/)
75
+ .map((line) =>
76
+ line
77
+ .trim()
78
+ .replace(/^(?:[-*•]|\d+[.)])\s*/, '')
79
+ .trim(),
80
+ )
81
+ .filter(Boolean)
82
+
83
+ return (lines.length ? lines : [trimmed]).map((description) => ({
84
+ description,
85
+ }))
39
86
  }
40
87
 
41
88
  export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
@@ -45,6 +92,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
45
92
  taskStore,
46
93
  runId,
47
94
  getPlanManager,
95
+ resumeHandler,
48
96
  // `onTaskLaunched` was the entry point for the old
49
97
  // non-blocking + envelope-injection flow. create_task is now
50
98
  // blocking, so the callback is no longer wired here.
@@ -282,26 +330,18 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
282
330
  .describe('Short title for the plan (e.g. "TypeScript Security & Performance Review")'),
283
331
  summary: z.string().describe('1-3 sentence summary of what you plan to do'),
284
332
  steps: z
285
- .array(
286
- z.object({
287
- description: z.string().describe('What this step does'),
288
- agent_id: z
289
- .string()
290
- .optional()
291
- .describe('Which agent handles this (omit for orchestrator-owned steps)'),
292
- depends_on: z
293
- .array(z.string())
294
- .optional()
295
- .describe('Step descriptions this depends on'),
296
- }),
297
- )
333
+ .preprocess(normalizeApprovePlanSteps, z.array(approvePlanStepSchema))
298
334
  .describe('Ordered list of planned steps'),
299
335
  }),
300
336
  category: 'custom',
301
337
  permissions: [],
302
338
  readOnly: true,
303
339
  destructive: false,
304
- concurrencySafe: true,
340
+ // Parks through the SAME runId-keyed host resume registry as
341
+ // ask_user_question — concurrent parks in one batch clobber the
342
+ // registry entry and deadlock the loser, so the executor must
343
+ // serialize this tool exactly like the question tool.
344
+ concurrencySafe: false,
305
345
  async execute({ title, summary, steps }) {
306
346
  const pm = getPlanManager()
307
347
  if (!pm) {
@@ -330,17 +370,27 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
330
370
 
331
371
  if (response.approved) {
332
372
  pm.startExecution()
373
+ // Approve-with-edits: when the user attached feedback to an
374
+ // approval, embed it in the model-visible output so the
375
+ // supervisor applies the edits during execution. A bare
376
+ // approve keeps the historical output byte-identical.
377
+ const output = response.feedback
378
+ ? `Plan approved by user with required edits — apply them during execution:\n${response.feedback}\nProceed with execution — launch workers via create_task.`
379
+ : 'Plan approved by user. Proceed with execution — launch workers via create_task.'
333
380
  return {
334
381
  success: true,
335
- output:
336
- 'Plan approved by user. Proceed with execution — launch workers via create_task.',
382
+ output,
337
383
  data: { approved: true, feedback: response.feedback },
338
384
  }
339
385
  }
340
386
 
387
+ // Rejection guidance follows the FEEDBACK, not a baked-in
388
+ // revise loop: the old unconditional "revise … and call
389
+ // approve_plan again" contradicted stop-style feedback, so a
390
+ // user rejecting a plan got another plan instead of a halt.
341
391
  return {
342
392
  success: false,
343
- output: `Plan rejected. User feedback: ${response.feedback ?? 'No feedback provided'}. Revise your plan based on this feedback and call approve_plan again, or ask the user for clarification.`,
393
+ output: `Plan rejected. User feedback: ${response.feedback ?? 'No feedback provided'}. Follow this feedback: if it requests changes, revise your plan and call approve_plan again; if it asks you to stop, acknowledge briefly and end your turn. If no feedback was provided, ask the user how to proceed before planning again.`,
344
394
  data: { approved: false, feedback: response.feedback },
345
395
  }
346
396
  },
@@ -348,5 +398,165 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
348
398
  tools.push(approvePlan)
349
399
  }
350
400
 
401
+ if (resumeHandler && runId) {
402
+ const parkRunId = runId
403
+ const parkHandler = resumeHandler
404
+ const askUserQuestion = defineTool({
405
+ name: 'ask_user_question',
406
+ description:
407
+ 'Ask the user ONE question ONLY when you are blocked on a decision that is genuinely theirs to make — one you cannot resolve from their request, your tools, the files you can read, or sensible defaults. The question must be the genuinely undecidable thing in THIS task. Never ask for information a tool can discover (do not ask what you can read, list, or search), never re-ask what the conversation already answers, and never ask meta-questions like "Shall I proceed?" — plan ratification goes through approve_plan. Provide 2-4 genuinely distinct options derived from the actual context — concrete paths, never generic placeholders (for example, asked to prepare a presentation, ask "Who is the audience?" with options like Board / Engineering team / Customer); keep labels short (1-5 words) and give each option a one-line description of what practically changes if it is chosen. Put your recommended option FIRST and append " (Recommended)" to its label. Set multiSelect: true only when several options can apply at once. A free-text "Something else" escape hatch is always shown automatically — do not add your own "Other" option. Ask ONE question per call and prefer at most one question per assistant turn; if several decisions block you, ask only the ones that materially change your next actions, in sequence — most work needs at most 2-3 questions, so prefer proceeding on stated defaults over interrogating the user. Never invent answers or synthetic content on the user\'s behalf unless they explicitly asked for a random/test scenario. The answer arrives as this tool\'s result; if the result says the user did not answer, do not ask this or any other question again — proceed on your best judgment without assuming consent.',
408
+ inputSchema: z.object({
409
+ question: z
410
+ .string()
411
+ .min(1)
412
+ .describe('Full question text — clear, specific, ends with a question mark.'),
413
+ header: z
414
+ .string()
415
+ .max(24)
416
+ .optional()
417
+ .describe('Very short topic label for the question (e.g. "Audience", "Auth method").'),
418
+ options: z
419
+ .array(
420
+ z.object({
421
+ label: z
422
+ .string()
423
+ .min(1)
424
+ .max(80)
425
+ .describe(
426
+ 'Concise option label (1-5 words). Recommended option goes first with " (Recommended)" appended.',
427
+ ),
428
+ description: z
429
+ .string()
430
+ .max(300)
431
+ .optional()
432
+ .describe('One line on what practically changes if this option is chosen.'),
433
+ }),
434
+ )
435
+ .min(2)
436
+ .max(4)
437
+ .describe('2-4 genuinely distinct, context-derived options.'),
438
+ multiSelect: z
439
+ .boolean()
440
+ .optional()
441
+ .default(false)
442
+ .describe('True only when several options can apply at once.'),
443
+ allowFreeText: z
444
+ .boolean()
445
+ .optional()
446
+ .default(true)
447
+ .describe('Whether the user may answer in their own words.'),
448
+ }),
449
+ category: 'custom',
450
+ permissions: [],
451
+ readOnly: true,
452
+ destructive: false,
453
+ // MUST stay false: the executor serializes non-concurrency-safe
454
+ // tools in a single chain, so N question blocks in one assistant
455
+ // turn park strictly one-at-a-time. Hosts key their park/resolve
456
+ // registries by runId — concurrent parks on one run clobber each
457
+ // other and the first promise never resolves (run hangs to TTL).
458
+ concurrencySafe: false,
459
+ async execute({ question, header, options, multiSelect, allowFreeText }, context) {
460
+ const toolUseId = context.toolUseId
461
+ if (!toolUseId) {
462
+ // Without the executing tool_use_id the question has no
463
+ // stable identity: the host could never merge the awaiting
464
+ // card with its resolution, and answers could not be matched
465
+ // back. Hard-fail instead of parking an unmergeable id.
466
+ return {
467
+ success: false,
468
+ output: '',
469
+ error:
470
+ 'ask_user_question requires an executor that threads ToolContext.toolUseId; the question cannot be tracked without it.',
471
+ }
472
+ }
473
+
474
+ const questionOptions: UserQuestionOption[] = options.map((opt, i) => ({
475
+ id: `opt_${i + 1}`,
476
+ label: opt.label,
477
+ ...(opt.description !== undefined ? { description: opt.description } : {}),
478
+ }))
479
+
480
+ const decision = await parkHandler({
481
+ type: 'user_question',
482
+ runId: parkRunId,
483
+ checkpointId: `cp_question_${toolUseId}`,
484
+ question: {
485
+ questionId: toolUseId,
486
+ question,
487
+ ...(header !== undefined ? { header } : {}),
488
+ options: questionOptions,
489
+ multiSelect,
490
+ allowFreeText,
491
+ },
492
+ })
493
+
494
+ // The no-answer sentinel (explicitly NOT consent — fixes the
495
+ // "empty answer reads as approval" ambiguity): used for empty
496
+ // answers, misdirected legacy decisions (e.g. a stale replica
497
+ // resolving with approve/continue verbs), and answers that
498
+ // carry a different question's id.
499
+ const noAnswer = {
500
+ success: true,
501
+ output:
502
+ 'The user did not answer this question. Do not assume a choice or consent; proceed on your best judgment or continue without this information.',
503
+ data: { question, answered: false },
504
+ }
505
+
506
+ if (decision.action === 'abort') {
507
+ return {
508
+ success: false,
509
+ output:
510
+ 'The user declined to answer and asked to stop. Acknowledge briefly and end your turn.',
511
+ data: { question, answered: false, declined: true },
512
+ }
513
+ }
514
+
515
+ if (decision.action !== 'answer_question') return noAnswer
516
+ if (decision.questionId !== undefined && decision.questionId !== toolUseId) {
517
+ // Misdirection guard: this answer was meant for a different
518
+ // question parked under the same run (stale client). Never
519
+ // fabricate a selection against the wrong question.
520
+ return noAnswer
521
+ }
522
+
523
+ const stripRecommended = (label: string) =>
524
+ label.replace(/\s*\(recommended\)\s*$/i, '').trim()
525
+
526
+ const selected = decision.selectedOptionIds
527
+ .map((id) => questionOptions.find((opt) => opt.id === id))
528
+ .filter((opt): opt is UserQuestionOption => opt !== undefined)
529
+ .map((opt) => ({ id: opt.id, label: stripRecommended(opt.label) }))
530
+
531
+ const freeText = decision.freeText?.trim() ?? ''
532
+
533
+ if (selected.length === 0 && !freeText) return noAnswer
534
+
535
+ let output: string
536
+ if (selected.length > 0) {
537
+ const joined = selected.map((s) => `"${s.label}"`).join(', ')
538
+ output = `User answered "${question}": ${joined}`
539
+ if (freeText) {
540
+ output += `\nAdditional note from the user: "${freeText}"`
541
+ }
542
+ } else {
543
+ output = `User answered "${question}" in their own words: "${freeText}"`
544
+ }
545
+
546
+ return {
547
+ success: true,
548
+ output,
549
+ data: {
550
+ question,
551
+ selected,
552
+ ...(freeText ? { freeText } : {}),
553
+ answered: true,
554
+ },
555
+ }
556
+ },
557
+ })
558
+ tools.push(askUserQuestion)
559
+ }
560
+
351
561
  return tools
352
562
  }
@@ -9,3 +9,4 @@ export * from './gateway.js'
9
9
  export * from './task.js'
10
10
  export * from './lifecycle-event.js'
11
11
  export * from './manager.js'
12
+ export * from './working-memory.js'
@@ -1,3 +1,4 @@
1
+ import type { CompactionConfig } from '../../config/runtime.js'
1
2
  import type { AdvisoryConfig } from '../advisory/index.js'
2
3
  import type { AgentPersona } from '../persona/index.js'
3
4
  import type { LLMProvider } from '../provider/index.js'
@@ -6,6 +7,7 @@ import type { Skill } from '../skills/index.js'
6
7
  import type { ToolRegistryContract } from '../tool/index.js'
7
8
  import type { VerificationGateConfig } from '../verification/index.js'
8
9
  import type { BaseAgentConfig, BaseAgentResult } from './base.js'
10
+ import type { WorkingMemoryProvider } from './working-memory.js'
9
11
 
10
12
  export interface ReactiveAgentConfig extends BaseAgentConfig {
11
13
  systemPrompt?: string
@@ -42,6 +44,21 @@ export interface ReactiveAgentConfig extends BaseAgentConfig {
42
44
  * on top of the provider keeps the underlying container alive.
43
45
  */
44
46
  sandboxProvider?: SandboxProvider
47
+
48
+ /**
49
+ * Optional structured-compaction config. Omitted ⇒ byte-identical run path
50
+ * (no `WorkingStateManager`, compaction early-returns). Mirrors the field
51
+ * on `SupervisorAgentConfig` so a child specialist can share the
52
+ * supervisor's compaction settings.
53
+ */
54
+ compactionConfig?: CompactionConfig
55
+
56
+ /**
57
+ * Optional neutral working-memory seam — same contract as the field on
58
+ * `SupervisorAgentConfig`. Absent ⇒ no block injected. A child specialist
59
+ * sharing the supervisor's output dir passes the same provider.
60
+ */
61
+ workingMemoryProvider?: WorkingMemoryProvider
45
62
  }
46
63
 
47
64
  export interface ReactiveAgentResult extends BaseAgentResult {
@@ -1,3 +1,4 @@
1
+ import type { CompactionConfig } from '../../config/runtime.js'
1
2
  import type { AdvisoryConfig } from '../advisory/index.js'
2
3
  import type { ResumeHandler } from '../hitl/index.js'
3
4
  import type { LLMProvider } from '../provider/index.js'
@@ -10,6 +11,7 @@ import type { BaseAgentConfig, BaseAgentResult } from './base.js'
10
11
  import type { AgentFactoryOptions } from './factory.js'
11
12
  import type { TaskGateway } from './gateway.js'
12
13
  import type { AgentManagerContract } from './manager.js'
14
+ import type { WorkingMemoryProvider } from './working-memory.js'
13
15
 
14
16
  export interface SupervisorAgentConfig extends BaseAgentConfig {
15
17
  provider: LLMProvider
@@ -68,6 +70,24 @@ export interface SupervisorAgentConfig extends BaseAgentConfig {
68
70
  * + children share one ephemeral container per task.
69
71
  */
70
72
  sandboxProvider?: SandboxProvider
73
+
74
+ /**
75
+ * Optional structured-compaction config. When omitted, `query()` never
76
+ * builds a `WorkingStateManager` and compaction early-returns — the run
77
+ * path is byte-identical to a non-compacting run. Hosts opt in (e.g. with
78
+ * a `contextWindowTokens`) to keep a long single run alive instead of
79
+ * silently truncating.
80
+ */
81
+ compactionConfig?: CompactionConfig
82
+
83
+ /**
84
+ * Optional neutral working-memory seam. When set, the SDK re-renders the
85
+ * provider's string into a single pinned leading system message every
86
+ * iteration (the primacy-edge, compaction-preserved slot). Absent ⇒ no
87
+ * block is ever injected. The SDK only positions the string; the host owns
88
+ * its content and trust framing.
89
+ */
90
+ workingMemoryProvider?: WorkingMemoryProvider
71
91
  }
72
92
 
73
93
  export interface AgentTaskResult {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Per-turn context the SDK is allowed to hand a working-memory provider.
3
+ *
4
+ * NEUTRAL by construction: opaque strings + a number, NO host/Postgres/disk
5
+ * types. It mirrors `AgentRuntimeContext.notes: readonly string[]` and the
6
+ * `web-search` seam discipline — the host closes over everything else
7
+ * (threadId, output dir, tenancy) when it builds the provider, so the SDK
8
+ * never learns a domain type. The SDK only POSITIONS the returned string at
9
+ * the primacy edge; the host OWNS the content and its authority framing.
10
+ */
11
+ export interface WorkingMemoryTurnContext {
12
+ /** The active run id (opaque). */
13
+ readonly runId: string
14
+ /** 1-based iteration counter for this run. */
15
+ readonly iteration: number
16
+ }
17
+
18
+ /**
19
+ * Returns a rendered, already-sanitized working-memory block to pin at the
20
+ * primacy edge (a single leading system message, refreshed every iteration).
21
+ *
22
+ * The SDK does no parsing of the returned string — it stores it verbatim under
23
+ * a sentinel header and re-pins it each turn. An empty/blank return ⇒ no block
24
+ * is injected (the byte-identical-when-empty property). Async so the host can
25
+ * `stat` the output dir / read the registry each turn. Failure-isolated by the
26
+ * SDK: a throwing/slow provider degrades to "no refresh this turn", never
27
+ * breaks the run.
28
+ */
29
+ export type WorkingMemoryProvider = (ctx: WorkingMemoryTurnContext) => string | Promise<string>
@@ -22,6 +22,25 @@ export function accumulateTokenUsage(current: TokenUsage, addition: TokenUsage):
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ * Merge two usage snapshots seen WITHIN a single streamed turn. Provider usage
27
+ * frames over one stream are cumulative/monotonic (input set once early, output
28
+ * grows), but a late frame can OMIT a field (report 0) — e.g. Anthropic's
29
+ * `message_delta` may carry only output tokens. A naive last-write-wins
30
+ * (`usage = chunk.usage`) then drops the earlier prompt/cache counts and
31
+ * under-reports the turn. Taking the per-field high-water mark preserves every
32
+ * field. DISTINCT from {@link accumulateTokenUsage}, which SUMS across turns.
33
+ */
34
+ export function mergeTokenUsage(current: TokenUsage, next: TokenUsage): TokenUsage {
35
+ return {
36
+ promptTokens: Math.max(current.promptTokens, next.promptTokens),
37
+ completionTokens: Math.max(current.completionTokens, next.completionTokens),
38
+ totalTokens: Math.max(current.totalTokens, next.totalTokens),
39
+ cachedTokens: Math.max(current.cachedTokens, next.cachedTokens),
40
+ cacheWriteTokens: Math.max(current.cacheWriteTokens, next.cacheWriteTokens),
41
+ }
42
+ }
43
+
25
44
  export interface CostInfo {
26
45
  inputCostPer1M: number
27
46
  outputCostPer1M: number
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { type TokenUsage, accumulateTokenUsage, mergeTokenUsage } from './index.js'
4
+
5
+ const zero: TokenUsage = {
6
+ promptTokens: 0,
7
+ completionTokens: 0,
8
+ totalTokens: 0,
9
+ cachedTokens: 0,
10
+ cacheWriteTokens: 0,
11
+ }
12
+
13
+ /**
14
+ * M7: within one streamed turn, usage frames are cumulative/monotonic but a
15
+ * late frame may omit fields. `mergeTokenUsage` keeps the per-field high-water
16
+ * mark so a trailing output-only frame does not zero the earlier prompt/cache
17
+ * counts (the under-report the audit flagged).
18
+ */
19
+ describe('mergeTokenUsage (within-turn, per-field max)', () => {
20
+ it('preserves prompt/cache tokens when a later frame reports only output', () => {
21
+ // message_start-like frame: input + cache set, output still 0.
22
+ const early: TokenUsage = {
23
+ ...zero,
24
+ promptTokens: 1200,
25
+ cachedTokens: 800,
26
+ cacheWriteTokens: 200,
27
+ }
28
+ // message_delta-like frame: only output tokens, input/cache dropped to 0.
29
+ const lateOutputOnly: TokenUsage = { ...zero, completionTokens: 350 }
30
+
31
+ const merged = mergeTokenUsage(early, lateOutputOnly)
32
+ expect(merged.promptTokens).toBe(1200) // NOT zeroed by the late frame
33
+ expect(merged.cachedTokens).toBe(800)
34
+ expect(merged.cacheWriteTokens).toBe(200)
35
+ expect(merged.completionTokens).toBe(350)
36
+ })
37
+
38
+ it('takes the growing (max) completion count across frames', () => {
39
+ const a: TokenUsage = { ...zero, promptTokens: 100, completionTokens: 10 }
40
+ const b: TokenUsage = { ...zero, promptTokens: 100, completionTokens: 42 }
41
+ expect(mergeTokenUsage(a, b).completionTokens).toBe(42)
42
+ // Order-independent (a late smaller frame never regresses the max).
43
+ expect(mergeTokenUsage(b, a).completionTokens).toBe(42)
44
+ })
45
+
46
+ it('is distinct from accumulateTokenUsage (which sums across turns)', () => {
47
+ const t1: TokenUsage = { ...zero, promptTokens: 100, completionTokens: 20 }
48
+ const t2: TokenUsage = { ...zero, promptTokens: 100, completionTokens: 30 }
49
+ // merge = max within a turn; accumulate = sum across turns.
50
+ expect(mergeTokenUsage(t1, t2).promptTokens).toBe(100)
51
+ expect(accumulateTokenUsage(t1, t2).promptTokens).toBe(200)
52
+ })
53
+ })
@@ -6,7 +6,9 @@ import type {
6
6
  ConnectorInstance,
7
7
  } from './definition.js'
8
8
 
9
- export type MCPTransportType = 'stdio' | 'http-sse'
9
+ export type MCPStreamableHttpTransportType = 'streamable_http' | 'streamable-http'
10
+
11
+ export type MCPTransportType = 'stdio' | 'http-sse' | MCPStreamableHttpTransportType
10
12
 
11
13
  export interface MCPTransportConfigBase {
12
14
  type: MCPTransportType
@@ -27,7 +29,17 @@ export interface MCPHttpSseTransportConfig extends MCPTransportConfigBase {
27
29
  timeoutMs?: number
28
30
  }
29
31
 
30
- export type MCPTransportUnion = MCPStdioTransportConfig | MCPHttpSseTransportConfig
32
+ export interface MCPStreamableHttpTransportConfig extends MCPTransportConfigBase {
33
+ type: MCPStreamableHttpTransportType
34
+ url: string
35
+ headers?: Record<string, string>
36
+ timeoutMs?: number
37
+ }
38
+
39
+ export type MCPTransportUnion =
40
+ | MCPStdioTransportConfig
41
+ | MCPHttpSseTransportConfig
42
+ | MCPStreamableHttpTransportConfig
31
43
 
32
44
  export interface MCPJsonRpcError {
33
45
  code: number
@@ -7,11 +7,27 @@ export type { CheckpointId }
7
7
 
8
8
  export type HITLResumeDecision =
9
9
  | { action: 'continue' }
10
- | { action: 'approve_plan' }
10
+ | { action: 'approve_plan'; feedback?: string }
11
11
  | { action: 'reject_plan'; feedback: string }
12
12
  | { action: 'approve_tools' }
13
13
  | { action: 'modify_tools'; modifications: ToolModification[] }
14
14
  | { action: 'reject_tools'; feedback: string }
15
+ | {
16
+ action: 'answer_question'
17
+ selectedOptionIds: string[]
18
+ freeText?: string
19
+ /**
20
+ * Echo of `UserQuestionData.questionId` — the misdirection
21
+ * guard. The park/resolve registry on hosts is typically
22
+ * keyed by run, so a stale client can answer question N
23
+ * after question N+1 re-parked under the same run. When
24
+ * present and it does not match the asking tool's own
25
+ * questionId, the tool treats the decision as unanswered
26
+ * instead of fabricating a selection against the wrong
27
+ * question.
28
+ */
29
+ questionId?: string
30
+ }
15
31
  | { action: 'pause'; reason: string }
16
32
  | { action: 'abort'; reason: string }
17
33
 
@@ -24,6 +40,7 @@ export type HITLDecisionRequest =
24
40
  checkpointId: CheckpointId
25
41
  summary: CheckpointSummary
26
42
  }
43
+ | { type: 'user_question'; runId: RunId; checkpointId: CheckpointId; question: UserQuestionData }
27
44
 
28
45
  export type ResumeHandler = (request: HITLDecisionRequest) => Promise<HITLResumeDecision>
29
46
 
@@ -40,6 +57,29 @@ export interface ToolModification {
40
57
  modifiedInput?: unknown
41
58
  }
42
59
 
60
+ export interface UserQuestionOption {
61
+ id: string
62
+ label: string
63
+ description?: string
64
+ }
65
+
66
+ /**
67
+ * A model-authored question for the user, parked through the
68
+ * `ResumeHandler` exactly like a plan approval. `questionId` equals
69
+ * the asking `tool_use_id` so the host can mint stable, mergeable
70
+ * activity ids per question and so answers can be matched back to
71
+ * the question that asked them (see
72
+ * `HITLResumeDecision['answer_question'].questionId`).
73
+ */
74
+ export interface UserQuestionData {
75
+ questionId: string
76
+ question: string
77
+ header?: string
78
+ options: UserQuestionOption[]
79
+ multiSelect: boolean
80
+ allowFreeText: boolean
81
+ }
82
+
43
83
  export interface PlanApprovalData {
44
84
  planId: PlanId
45
85
  title: string
@@ -108,6 +148,17 @@ export function autoApproveHandler(request: HITLDecisionRequest): Promise<HITLRe
108
148
  return Promise.resolve({ action: 'approve_tools' })
109
149
  case 'iteration_checkpoint':
110
150
  return Promise.resolve({ action: 'continue' })
151
+ case 'user_question':
152
+ // Headless runs must never deadlock on a question and must
153
+ // never fabricate a user choice: answer with an explicit
154
+ // no-selection sentinel so the asking tool renders "the user
155
+ // did not answer" rather than consent.
156
+ return Promise.resolve({
157
+ action: 'answer_question',
158
+ selectedOptionIds: [],
159
+ freeText: 'No user is available to answer. Proceed using your best judgment.',
160
+ questionId: request.question.questionId,
161
+ })
111
162
  default: {
112
163
  const _exhaustive: never = request
113
164
  throw new Error(`Unhandled HITL request type: ${(_exhaustive as HITLDecisionRequest).type}`)
@@ -28,6 +28,16 @@ export interface ChatCompletionParams {
28
28
  stream?: boolean
29
29
  stop?: string[]
30
30
 
31
+ /**
32
+ * Per-call cancellation. Aborting it tears down the in-flight model
33
+ * request (the provider passes it to the underlying fetch / SDK) AND the
34
+ * runtime races the stream consumer against it, so a Stop stops the
35
+ * CURRENT turn mid-flight — not only between turns. Optional and inert
36
+ * when unset: a non-aborted signal is behaviourally identical to omitting
37
+ * it, so existing callers are unaffected.
38
+ */
39
+ signal?: AbortSignal
40
+
31
41
  toolChoice?: ToolChoice
32
42
  parallelToolCalls?: boolean
33
43
 
package/src/version.ts CHANGED
@@ -1,6 +1,28 @@
1
- import { createRequire } from 'node:module'
1
+ import { readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
2
4
 
3
- const require = createRequire(import.meta.url)
4
- const pkg = require('../package.json') as { version: string }
5
+ /**
6
+ * The SDK's package version, read from package.json relative to this module.
7
+ *
8
+ * Wrapped in try/catch (mirroring the CLI's readPackageVersion): a bundler
9
+ * like esbuild collapses the whole dist tree into a single file, so at runtime
10
+ * `../package.json` no longer resolves next to the bundle. Without the guard,
11
+ * this module's top-level read throws at import time and takes the entire
12
+ * process down — which is exactly what broke the bundled desktop runtime
13
+ * (`Cannot find module '../package.json'` on any code path that touches the
14
+ * SDK runtime). The version string is cosmetic, so a placeholder is fine.
15
+ */
16
+ function readVersion(): string {
17
+ try {
18
+ const here = dirname(fileURLToPath(import.meta.url))
19
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')) as {
20
+ version?: unknown
21
+ }
22
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
23
+ } catch {
24
+ return '0.0.0'
25
+ }
26
+ }
5
27
 
6
- export const VERSION: string = pkg.version
28
+ export const VERSION: string = readVersion()