@namzu/sdk 10.0.0 → 11.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 (39) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/manager/plan/lifecycle.d.ts +10 -0
  3. package/dist/manager/plan/lifecycle.d.ts.map +1 -1
  4. package/dist/manager/plan/lifecycle.js +14 -0
  5. package/dist/manager/plan/lifecycle.js.map +1 -1
  6. package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.d.ts +2 -0
  7. package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.d.ts.map +1 -0
  8. package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.js +90 -0
  9. package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.js.map +1 -0
  10. package/dist/runtime/query/result.d.ts.map +1 -1
  11. package/dist/runtime/query/result.js +17 -1
  12. package/dist/runtime/query/result.js.map +1 -1
  13. package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.d.ts +2 -0
  14. package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.d.ts.map +1 -0
  15. package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.js +139 -0
  16. package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.js.map +1 -0
  17. package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.d.ts +2 -0
  18. package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.d.ts.map +1 -0
  19. package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.js +160 -0
  20. package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.js.map +1 -0
  21. package/dist/tools/coordinator/__tests__/approve-plan.test.js +21 -6
  22. package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -1
  23. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +4 -0
  24. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
  25. package/dist/tools/coordinator/__tests__/task-list.test.js +44 -15
  26. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  27. package/dist/tools/coordinator/index.d.ts.map +1 -1
  28. package/dist/tools/coordinator/index.js +159 -8
  29. package/dist/tools/coordinator/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/manager/plan/lifecycle.ts +14 -0
  32. package/src/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.ts +109 -0
  33. package/src/runtime/query/result.ts +18 -1
  34. package/src/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.ts +171 -0
  35. package/src/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.ts +215 -0
  36. package/src/tools/coordinator/__tests__/approve-plan.test.ts +32 -11
  37. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +7 -0
  38. package/src/tools/coordinator/__tests__/task-list.test.ts +47 -20
  39. package/src/tools/coordinator/index.ts +177 -8
@@ -308,6 +308,33 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
308
308
  // compatibility (Agent tool consumes it from its own path).
309
309
  } = opts
310
310
  const cwd = opts.workingDirectory
311
+
312
+ /**
313
+ * The tasks THIS surface launched — the scope of everything it will read back.
314
+ *
315
+ * A `TaskGateway` is shared on purpose: `SupervisorAgentConfig.gateway`
316
+ * exists so a host can hand the same one to several runs. `listTasks()` is
317
+ * therefore gateway-wide by design, and `agent_task_list` used to hand that
318
+ * straight to the model — so a supervisor could read a sibling run's worker
319
+ * output, including the `result` field, by listing. `wait_for_task` had the
320
+ * same reach through `getTask`.
321
+ *
322
+ * That is the leak `CompletionInbox` closed on the push side, through a
323
+ * different door: the inbox refuses a completion for a task it was not told
324
+ * about, precisely because `onTaskCompleted` is a broadcast. The pull side
325
+ * kept no such record and asked the gateway directly.
326
+ *
327
+ * The scope lives here rather than in `listTasks()` because the two answer
328
+ * different questions. A host calling `listTasks()` is the operator and may
329
+ * legitimately want everything on its gateway; a model calling
330
+ * `agent_task_list` is one run asking about its own work. Narrowing the
331
+ * gateway method would take the operator's view away to fix the model's.
332
+ *
333
+ * Consequence worth stating: a task launched through a DIFFERENT surface on
334
+ * the same gateway — `buildAgentTool`, or the host itself — is not listed
335
+ * here. That is the same rule, not an exception to it.
336
+ */
337
+ const launchedHere = new Set<TaskId>()
311
338
  void opts.onTaskLaunched
312
339
 
313
340
  const agentIdEnum = delegateSchema(agentIds)
@@ -386,6 +413,12 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
386
413
  .describe(
387
414
  'Existing planning task ID to link. If omitted, a planning task is auto-created.',
388
415
  ),
416
+ plan_step_id: z
417
+ .string()
418
+ .optional()
419
+ .describe(
420
+ 'The approve_plan step this launch carries out (e.g. "step_2"). Pass it and the step reports its own outcome — running on launch, completed or failed when the worker settles — so the plan can say how it went. Omit it only when this launch is not part of the approved plan.',
421
+ ),
389
422
  ...(canLaunchInBackground
390
423
  ? {
391
424
  background: z
@@ -407,9 +440,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
407
440
  // children were observed at 8m04s, which it would have survived by
408
441
  // under two minutes.
409
442
  timeoutMs: DELEGATION_TIMEOUT_MS,
410
- async execute({ agent_id, prompt, description, plan_task_id, background }, _context) {
443
+ async execute(
444
+ { agent_id, prompt, description, plan_task_id, plan_step_id, background },
445
+ _context,
446
+ ) {
411
447
  let resolvedPlanTaskId = plan_task_id
412
448
 
449
+ // The binding between a plan step and the work that carries it out.
450
+ // Without it a plan's steps had no relationship to any tool call, so
451
+ // nothing could ever observe how a step went — which is why a plan
452
+ // could report `failed` or stay `executing` forever but never
453
+ // `completed`.
454
+ const planStepId = plan_step_id
455
+ const reportStep = (status: 'running' | 'completed' | 'failed', error?: string): void => {
456
+ if (!planStepId) return
457
+ getPlanManager?.()?.updateStepStatus(planStepId, status, error)
458
+ }
459
+ reportStep('running')
460
+
413
461
  if (taskStore) {
414
462
  if (resolvedPlanTaskId) {
415
463
  await taskStore.update(resolvedPlanTaskId as `task_${string}`, {
@@ -446,6 +494,9 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
446
494
  // is exactly the blocking launch whose wait was abandoned.
447
495
  completionInbox?.launched(handle.taskId)
448
496
 
497
+ // ...and whose it is for the READ side too. See `launchedHere`.
498
+ launchedHere.add(handle.taskId)
499
+
449
500
  // A background launch asked for with nowhere to deliver it is
450
501
  // REFUSED, not quietly turned into a blocking one.
451
502
  //
@@ -469,6 +520,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
469
520
  description: 'Failed: the launch was refused before any worker started',
470
521
  })
471
522
  }
523
+ // Same reason the plan task is closed here: nothing is running,
524
+ // so a step left `running` would show work underway with no
525
+ // worker behind it, and the plan could never settle.
526
+ reportStep('failed', 'the launch was refused before any worker started')
472
527
  return {
473
528
  success: false,
474
529
  output: '',
@@ -569,6 +624,11 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
569
624
  })
570
625
  }
571
626
 
627
+ // The step reports the same outcome, from the same two authorities.
628
+ // This is the only point in a delegated launch where the answer is
629
+ // actually known.
630
+ reportStep(success ? 'completed' : 'failed', success ? undefined : resultText.slice(0, 200))
631
+
572
632
  return {
573
633
  success,
574
634
  // Framed, because a delegated worker is the component MOST
@@ -631,6 +691,21 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
631
691
  // waiting. Same bound as the launch it is waiting on.
632
692
  timeoutMs: DELEGATION_TIMEOUT_MS,
633
693
  async execute({ task_id }, _context) {
694
+ // Same scope as the listing — see `launchedHere`. Asked FIRST, so a
695
+ // task belonging to a sibling run on a shared gateway is refused
696
+ // here rather than waited on and then read.
697
+ if (!launchedHere.has(task_id as TaskId)) {
698
+ // Deliberately does not distinguish "never existed" from
699
+ // "belongs to someone else". The second answer is itself the
700
+ // leak in miniature: it confirms a task id a run was not
701
+ // supposed to know about.
702
+ return {
703
+ success: false,
704
+ output: `No task ${task_id} was launched by this run. Call agent_task_list to see the tasks you can wait on.`,
705
+ data: { task_id },
706
+ }
707
+ }
708
+
634
709
  const known = gateway.getTask(task_id as TaskId)
635
710
  if (!known) {
636
711
  return {
@@ -716,7 +791,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
716
791
 
717
792
  const agentTaskList = defineTool({
718
793
  name: 'agent_task_list',
719
- description: `Inspect the live state of every agent task launched on this gateway via create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Distinct from the plan-task store's \`task_list\` (which lists planning tasks): this tool lists running/completed worker invocations. ${listingAdvice}`,
794
+ description: `Inspect the live state of the agent tasks YOU launched with create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Tasks launched by another run are not listed, even when it shares this gateway. Distinct from the plan-task store's \`task_list\` (which lists planning tasks): this tool lists running/completed worker invocations. ${listingAdvice}`,
720
795
  inputSchema: z.object({
721
796
  state: z
722
797
  .enum(['pending', 'running', 'completed', 'failed', 'canceled'])
@@ -729,7 +804,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
729
804
  destructive: false,
730
805
  concurrencySafe: true,
731
806
  async execute({ state }) {
732
- const handles = gateway.listTasks()
807
+ // Scoped to this run's own launches — see `launchedHere`. The
808
+ // gateway may hold a sibling run's tasks, and this listing is not
809
+ // the door to them.
810
+ const handles = gateway.listTasks().filter((h) => launchedHere.has(h.taskId))
733
811
  const filtered = state ? handles.filter((h) => h.state === state) : handles
734
812
  const items = filtered.map((h) => {
735
813
  const runStatus = h.result?.status
@@ -965,17 +1043,38 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
965
1043
 
966
1044
  if (response.approved) {
967
1045
  pm.startExecution()
1046
+
1047
+ // The step ids, because nothing else tells the model what
1048
+ // they are. `plan_step_id` on create_task and `step_id` on
1049
+ // update_plan_step are both unusable without them — a
1050
+ // binding the caller cannot name is a binding that does not
1051
+ // exist. Rendered from the plan as approved, so a plan the
1052
+ // user edited lists the steps they actually approved.
1053
+ const roster = (pm.active?.steps ?? [])
1054
+ .map((s) => ` ${s.id} — ${s.description}${s.agentId ? ` (${s.agentId})` : ''}`)
1055
+ .join('\n')
1056
+ const howToReport = roster
1057
+ ? `\nSteps, and how each reports its outcome:\n${roster}\nPass plan_step_id to create_task for a delegated step; call update_plan_step for one you do yourself. The plan cannot report success until every step has reported.`
1058
+ : ''
1059
+
968
1060
  // Approve-with-edits: when the user attached feedback to an
969
1061
  // approval, embed it in the model-visible output so the
970
- // supervisor applies the edits during execution. A bare
971
- // approve keeps the historical output byte-identical.
1062
+ // supervisor applies the edits during execution.
972
1063
  const output = response.feedback
973
- ? `Plan approved by user with required edits — apply them during execution:\n${response.feedback}\nProceed with execution — launch workers via create_task.`
974
- : 'Plan approved by user. Proceed with execution — launch workers via create_task.'
1064
+ ? `Plan approved by user with required edits — apply them during execution:\n${response.feedback}\nProceed with execution — launch workers via create_task.${howToReport}`
1065
+ : `Plan approved by user. Proceed with execution — launch workers via create_task.${howToReport}`
975
1066
  return {
976
1067
  success: true,
977
1068
  output,
978
- data: { approved: true, feedback: response.feedback },
1069
+ data: {
1070
+ approved: true,
1071
+ feedback: response.feedback,
1072
+ steps: (pm.active?.steps ?? []).map((s) => ({
1073
+ step_id: s.id,
1074
+ description: s.description,
1075
+ agent_id: s.agentId,
1076
+ })),
1077
+ },
979
1078
  }
980
1079
  }
981
1080
 
@@ -991,6 +1090,76 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
991
1090
  },
992
1091
  })
993
1092
  tools.push(approvePlan)
1093
+
1094
+ /**
1095
+ * How a step the ORCHESTRATOR did reports its own outcome.
1096
+ *
1097
+ * `create_task` reports the steps it carries out, which covers every
1098
+ * delegated step. A step with no `agent_id` is the orchestrator's own
1099
+ * work and has no tool call to bind to — so without this there is no
1100
+ * way for it to ever report, and a plan containing one could never
1101
+ * settle no matter how well it went.
1102
+ *
1103
+ * `skipped` is a first-class outcome and not a euphemism for failure:
1104
+ * a plan that turned out not to need a step went right, and forcing
1105
+ * that into `completed` or `failed` would make the plan lie in one
1106
+ * direction or the other.
1107
+ */
1108
+ const updatePlanStep = defineTool({
1109
+ name: 'update_plan_step',
1110
+ description:
1111
+ 'Report how a step of the approved plan went. Use it for steps YOU carried out — steps delegated with create_task report themselves when you pass plan_step_id. Call it as each step settles, not in a batch at the end: the plan cannot say it succeeded until every step has reported, and an unreported step is not scored as a failure, it simply leaves the plan unsettled. Use "skipped" for a step that turned out not to be needed; that is a successful outcome, not a failure.',
1112
+ inputSchema: z.object({
1113
+ step_id: z.string().describe('The plan step id, e.g. "step_2".'),
1114
+ status: z
1115
+ .enum(['completed', 'skipped', 'failed'])
1116
+ .describe(
1117
+ 'How it went. "skipped" means the step was not needed and the plan is still on track.',
1118
+ ),
1119
+ error: z
1120
+ .string()
1121
+ .optional()
1122
+ .describe('What went wrong. Only meaningful with status "failed".'),
1123
+ }),
1124
+ category: 'custom',
1125
+ permissions: [],
1126
+ readOnly: false,
1127
+ destructive: false,
1128
+ concurrencySafe: true,
1129
+ async execute({ step_id, status, error }) {
1130
+ const pm = getPlanManager?.()
1131
+ if (!pm?.active) {
1132
+ return {
1133
+ success: false,
1134
+ output: '',
1135
+ error: 'There is no active plan to report against. Call approve_plan first.',
1136
+ }
1137
+ }
1138
+
1139
+ const step = pm.updateStepStatus(step_id, status, error)
1140
+ if (!step) {
1141
+ const known = pm.active.steps.map((s) => s.id).join(', ')
1142
+ return {
1143
+ success: false,
1144
+ output: '',
1145
+ error: `No plan step "${step_id}". This plan's steps are: ${known || '(none)'}.`,
1146
+ }
1147
+ }
1148
+
1149
+ const outstanding = pm.unreportedSteps
1150
+ return {
1151
+ success: true,
1152
+ output:
1153
+ outstanding.length === 0
1154
+ ? `Step ${step_id} reported as ${status}. Every step has now reported.`
1155
+ : `Step ${step_id} reported as ${status}. Still unreported: ${outstanding
1156
+ .map((s) => s.id)
1157
+ .join(', ')}.`,
1158
+ data: { step_id, status, unreported: outstanding.map((s) => s.id) },
1159
+ }
1160
+ },
1161
+ })
1162
+ tools.push(updatePlanStep)
994
1163
  }
995
1164
 
996
1165
  if (resumeHandler && runId) {