@namzu/sdk 7.0.0 → 9.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 (206) hide show
  1. package/CHANGELOG.md +652 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +186 -163
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts +2 -0
  6. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts.map +1 -0
  7. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js +177 -0
  8. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js.map +1 -0
  9. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
  12. package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +4 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +24 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/contracts/api.d.ts +1 -1
  20. package/dist/contracts/api.d.ts.map +1 -1
  21. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts +2 -0
  22. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts.map +1 -0
  23. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js +114 -0
  24. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js.map +1 -0
  25. package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
  26. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
  27. package/dist/gateway/completion-inbox.d.ts +94 -6
  28. package/dist/gateway/completion-inbox.d.ts.map +1 -1
  29. package/dist/gateway/completion-inbox.js +235 -15
  30. package/dist/gateway/completion-inbox.js.map +1 -1
  31. package/dist/gateway/local.d.ts.map +1 -1
  32. package/dist/gateway/local.js +19 -6
  33. package/dist/gateway/local.js.map +1 -1
  34. package/dist/manager/agent/__tests__/lifecycle.test.js +56 -0
  35. package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
  36. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  37. package/dist/manager/agent/lifecycle.js +62 -33
  38. package/dist/manager/agent/lifecycle.js.map +1 -1
  39. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts +2 -0
  40. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts.map +1 -0
  41. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js +73 -0
  42. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js.map +1 -0
  43. package/dist/manager/plan/lifecycle.d.ts +48 -0
  44. package/dist/manager/plan/lifecycle.d.ts.map +1 -1
  45. package/dist/manager/plan/lifecycle.js +56 -0
  46. package/dist/manager/plan/lifecycle.js.map +1 -1
  47. package/dist/manager/run/persistence.d.ts +8 -0
  48. package/dist/manager/run/persistence.d.ts.map +1 -1
  49. package/dist/manager/run/persistence.js +12 -0
  50. package/dist/manager/run/persistence.js.map +1 -1
  51. package/dist/run/reporter.d.ts.map +1 -1
  52. package/dist/run/reporter.js +11 -0
  53. package/dist/run/reporter.js.map +1 -1
  54. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
  55. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
  56. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
  57. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
  58. package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
  59. package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
  60. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
  61. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
  62. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
  63. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
  64. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts +2 -0
  65. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts.map +1 -0
  66. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js +62 -0
  67. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js.map +1 -0
  68. package/dist/runtime/query/events.d.ts.map +1 -1
  69. package/dist/runtime/query/events.js +5 -0
  70. package/dist/runtime/query/events.js.map +1 -1
  71. package/dist/runtime/query/guard.d.ts +28 -0
  72. package/dist/runtime/query/guard.d.ts.map +1 -1
  73. package/dist/runtime/query/guard.js +31 -0
  74. package/dist/runtime/query/guard.js.map +1 -1
  75. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
  76. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
  78. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
  79. package/dist/runtime/query/iteration/index.d.ts +92 -0
  80. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/index.js +814 -566
  82. package/dist/runtime/query/iteration/index.js.map +1 -1
  83. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
  84. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
  85. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
  86. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
  87. package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
  88. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  89. package/dist/runtime/query/iteration/phases/compaction.js +61 -4
  90. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  91. package/dist/store/task/disk.d.ts.map +1 -1
  92. package/dist/store/task/disk.js +14 -0
  93. package/dist/store/task/disk.js.map +1 -1
  94. package/dist/store/task/memory.d.ts.map +1 -1
  95. package/dist/store/task/memory.js +14 -0
  96. package/dist/store/task/memory.js.map +1 -1
  97. package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
  98. package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
  99. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts +2 -0
  100. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts.map +1 -0
  101. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js +101 -0
  102. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js.map +1 -0
  103. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
  104. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
  105. package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
  106. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  107. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts +2 -0
  108. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts.map +1 -0
  109. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js +99 -0
  110. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js.map +1 -0
  111. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  112. package/dist/tools/coordinator/agent.js +17 -23
  113. package/dist/tools/coordinator/agent.js.map +1 -1
  114. package/dist/tools/coordinator/index.d.ts.map +1 -1
  115. package/dist/tools/coordinator/index.js +142 -14
  116. package/dist/tools/coordinator/index.js.map +1 -1
  117. package/dist/tools/coordinator/outcome.d.ts +60 -0
  118. package/dist/tools/coordinator/outcome.d.ts.map +1 -0
  119. package/dist/tools/coordinator/outcome.js +68 -0
  120. package/dist/tools/coordinator/outcome.js.map +1 -0
  121. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  122. package/dist/tools/untrusted-envelope.js +9 -1
  123. package/dist/tools/untrusted-envelope.js.map +1 -1
  124. package/dist/types/agent/gateway.d.ts +37 -2
  125. package/dist/types/agent/gateway.d.ts.map +1 -1
  126. package/dist/types/agent/supervisor.d.ts +38 -1
  127. package/dist/types/agent/supervisor.d.ts.map +1 -1
  128. package/dist/types/plan/index.d.ts +26 -0
  129. package/dist/types/plan/index.d.ts.map +1 -1
  130. package/dist/types/provider/interface.d.ts +35 -0
  131. package/dist/types/provider/interface.d.ts.map +1 -1
  132. package/dist/types/run/entity.d.ts +16 -0
  133. package/dist/types/run/entity.d.ts.map +1 -1
  134. package/dist/types/run/events.d.ts +92 -0
  135. package/dist/types/run/events.d.ts.map +1 -1
  136. package/dist/types/run/events.js.map +1 -1
  137. package/dist/types/run/prepare-step.d.ts +17 -2
  138. package/dist/types/run/prepare-step.d.ts.map +1 -1
  139. package/dist/types/task/index.d.ts +19 -1
  140. package/dist/types/task/index.d.ts.map +1 -1
  141. package/dist/types/task/index.js +10 -1
  142. package/dist/types/task/index.js.map +1 -1
  143. package/dist/types/verification/index.d.ts +98 -0
  144. package/dist/types/verification/index.d.ts.map +1 -1
  145. package/dist/types/verification/index.js +10 -0
  146. package/dist/types/verification/index.js.map +1 -1
  147. package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
  148. package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
  149. package/dist/verification/__tests__/argument-pattern.test.js +122 -0
  150. package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
  151. package/dist/verification/gate.d.ts.map +1 -1
  152. package/dist/verification/gate.js +44 -0
  153. package/dist/verification/gate.js.map +1 -1
  154. package/dist/verification/rules.d.ts.map +1 -1
  155. package/dist/verification/rules.js +27 -0
  156. package/dist/verification/rules.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/agents/SupervisorAgent.ts +189 -167
  159. package/src/agents/__tests__/a-policy-nobody-can-select.test.ts +202 -0
  160. package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
  161. package/src/bridge/a2a/mapper.ts +4 -0
  162. package/src/bridge/sse/mapper.ts +25 -0
  163. package/src/contracts/api.ts +1 -0
  164. package/src/gateway/__tests__/a-knob-nobody-can-turn.test.ts +139 -0
  165. package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
  166. package/src/gateway/completion-inbox.ts +248 -16
  167. package/src/gateway/local.ts +19 -7
  168. package/src/manager/agent/__tests__/lifecycle.test.ts +74 -0
  169. package/src/manager/agent/lifecycle.ts +72 -42
  170. package/src/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.ts +88 -0
  171. package/src/manager/plan/lifecycle.ts +61 -0
  172. package/src/manager/run/persistence.ts +12 -0
  173. package/src/run/reporter.ts +12 -0
  174. package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
  175. package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
  176. package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
  177. package/src/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.ts +79 -0
  178. package/src/runtime/query/events.ts +5 -0
  179. package/src/runtime/query/guard.ts +32 -0
  180. package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
  181. package/src/runtime/query/iteration/index.ts +902 -636
  182. package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
  183. package/src/runtime/query/iteration/phases/compaction.ts +83 -10
  184. package/src/store/task/disk.ts +15 -0
  185. package/src/store/task/memory.ts +15 -0
  186. package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
  187. package/src/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.ts +133 -0
  188. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
  189. package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
  190. package/src/tools/coordinator/__tests__/the-plan-a-human-approves.test.ts +121 -0
  191. package/src/tools/coordinator/agent.ts +17 -25
  192. package/src/tools/coordinator/index.ts +155 -18
  193. package/src/tools/coordinator/outcome.ts +70 -0
  194. package/src/tools/untrusted-envelope.ts +9 -1
  195. package/src/types/agent/gateway.ts +37 -2
  196. package/src/types/agent/supervisor.ts +40 -1
  197. package/src/types/plan/index.ts +29 -0
  198. package/src/types/provider/interface.ts +39 -0
  199. package/src/types/run/entity.ts +17 -0
  200. package/src/types/run/events.ts +92 -0
  201. package/src/types/run/prepare-step.ts +17 -2
  202. package/src/types/task/index.ts +22 -3
  203. package/src/types/verification/index.ts +61 -0
  204. package/src/verification/__tests__/argument-pattern.test.ts +158 -0
  205. package/src/verification/gate.ts +45 -0
  206. package/src/verification/rules.ts +28 -0
@@ -0,0 +1,149 @@
1
+ import { mkdtemp } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { z } from 'zod'
6
+
7
+ import { MockLLMProvider } from '../../provider/mock.js'
8
+ import { ToolNameCollisionError, ToolRegistry } from '../../registry/tool/execute.js'
9
+ import { defineTool } from '../../tools/defineTool.js'
10
+ import type { TaskGateway, TaskHandle } from '../../types/agent/gateway.js'
11
+ import type { TaskId } from '../../types/ids/index.js'
12
+ import { SupervisorAgent } from '../SupervisorAgent.js'
13
+
14
+ /**
15
+ * A gateway the HOST owns, which is the case that goes wrong.
16
+ *
17
+ * `SupervisorAgentConfig.gateway` is a first-class option, and a host that
18
+ * built a gateway reuses it — across sequential runs, and across concurrent
19
+ * ones. The supervisor attached a fresh `CompletionInbox` to it on every run
20
+ * and never detached, so the subscription set only grew: three runs, three
21
+ * live listeners, each still holding its own run's handles and each still
22
+ * being handed every other run's completions.
23
+ */
24
+ class HostGateway implements TaskGateway {
25
+ readonly listeners = new Set<(h: TaskHandle) => void>()
26
+
27
+ async createTask(): Promise<TaskHandle> {
28
+ throw new Error('this test never launches')
29
+ }
30
+ async waitForTask(): Promise<TaskHandle> {
31
+ throw new Error('this test never waits')
32
+ }
33
+ async continueTask(): Promise<void> {}
34
+ cancelTask(): void {}
35
+ getTask(): TaskHandle | undefined {
36
+ return undefined
37
+ }
38
+ listTasks(): TaskHandle[] {
39
+ return []
40
+ }
41
+ onTaskCompleted(cb: (h: TaskHandle) => void): () => void {
42
+ this.listeners.add(cb)
43
+ return () => this.listeners.delete(cb)
44
+ }
45
+ }
46
+
47
+ /** A host tool whose name the coordinator surface also wants. */
48
+ const collidingTool = defineTool({
49
+ name: 'create_task',
50
+ description: 'a tool this host registered under a coordinator name',
51
+ inputSchema: z.object({}),
52
+ category: 'custom',
53
+ permissions: [],
54
+ readOnly: true,
55
+ destructive: false,
56
+ concurrencySafe: true,
57
+ async execute() {
58
+ return { success: true as const, output: 'never runs' }
59
+ },
60
+ })
61
+
62
+ async function runOnce(
63
+ gateway: TaskGateway,
64
+ id: string,
65
+ options: { collide?: boolean } = {},
66
+ ): Promise<void> {
67
+ const agent = new SupervisorAgent({
68
+ id,
69
+ name: 'Supervisor',
70
+ version: '1',
71
+ category: 'test',
72
+ description: 'coordinates workers',
73
+ })
74
+
75
+ const tools = new ToolRegistry()
76
+ if (options.collide) tools.register(collidingTool)
77
+
78
+ await agent.run(
79
+ {
80
+ messages: [{ role: 'user', content: 'go', timestamp: 1 }],
81
+ workingDirectory: await mkdtemp(join(tmpdir(), 'namzu-inbox-scope-')),
82
+ } as never,
83
+ {
84
+ provider: new MockLLMProvider({ turns: [{ text: 'nothing to delegate' }] }),
85
+ agentIds: ['worker'],
86
+ gateway,
87
+ tools,
88
+ systemPrompt: 'You coordinate.',
89
+ model: 'mock-model',
90
+ tokenBudget: 100_000,
91
+ timeoutMs: 30_000,
92
+ maxIterations: 2,
93
+ sessionId: 'ses_scope',
94
+ threadId: 'thd_scope',
95
+ projectId: 'prj_scope',
96
+ tenantId: 'tnt_scope',
97
+ } as never,
98
+ )
99
+ }
100
+
101
+ describe('a supervisor releases the gateway it borrowed', () => {
102
+ it('leaves no listener behind, however many runs the host makes', async () => {
103
+ const gateway = new HostGateway()
104
+
105
+ for (let i = 0; i < 3; i++) {
106
+ await runOnce(gateway, `sup_${i}`)
107
+ expect(gateway.listeners.size, `run ${i + 1} left its completion listener attached`).toBe(0)
108
+ }
109
+ }, 60_000)
110
+
111
+ it('releases it when setup throws before the run ever starts', async () => {
112
+ // The reason it is a `finally` covering the whole body and not a line
113
+ // after `drainQuery`. A host whose tool shares a coordinator name gets
114
+ // `ToolNameCollisionError` from the registration loop — after the inbox
115
+ // attached — then fixes its config and runs again. A leak of one
116
+ // listener per run becomes one per ATTEMPT, and the attempts are what
117
+ // there are most of.
118
+ const gateway = new HostGateway()
119
+
120
+ await expect(runOnce(gateway, 'sup_collide', { collide: true })).rejects.toThrow(
121
+ ToolNameCollisionError,
122
+ )
123
+
124
+ expect(gateway.listeners.size, 'a run that threw left its listener attached').toBe(0)
125
+ }, 60_000)
126
+
127
+ it('never hands a completion to a run that did not launch it', async () => {
128
+ // Two supervisors, one gateway. The second run's inbox is gone by the
129
+ // time this fires, but the assertion that matters is the one above it:
130
+ // nothing is listening that should not be.
131
+ const gateway = new HostGateway()
132
+ await runOnce(gateway, 'sup_a')
133
+ await runOnce(gateway, 'sup_b')
134
+
135
+ let delivered = 0
136
+ for (const cb of gateway.listeners) {
137
+ delivered += 1
138
+ cb({
139
+ taskId: 'tsk_foreign' as TaskId,
140
+ agentId: 'worker',
141
+ state: 'completed',
142
+ createdAt: 0,
143
+ completedAt: 1,
144
+ } as TaskHandle)
145
+ }
146
+
147
+ expect(delivered).toBe(0)
148
+ }, 60_000)
149
+ })
@@ -188,6 +188,10 @@ const MAPPING: {
188
188
  // Context management is kernel-internal bookkeeping; A2A peers model a
189
189
  // task lifecycle, not the host runtime's memory strategy.
190
190
  compaction_completed: null,
191
+ // Compaction, succeeded or declined, is a property of how this runtime
192
+ // manages its own context. A peer models a task lifecycle and cannot act on
193
+ // either outcome.
194
+ compaction_failed: null,
191
195
  // A refusal is the run's own policy decision; the peer sees it in the
192
196
  // terminal task state, not as a separate signal.
193
197
  guardrail_triggered: null,
@@ -92,6 +92,21 @@ const MAPPING: {
92
92
  }),
93
93
  },
94
94
 
95
+ // Carried for the same reason its sibling is: a host that can show a user
96
+ // context was dropped must also be able to show them it was not, because a
97
+ // run continuing at full context is the state that ends in an opaque
98
+ // provider rejection later.
99
+ compaction_failed: {
100
+ wire: 'compaction.failed',
101
+ transform: (e, runId) => ({
102
+ run_id: runId,
103
+ iteration: e.iteration,
104
+ cause: e.cause,
105
+ messages: e.messages,
106
+ ...(e.error !== undefined ? { error: e.error } : {}),
107
+ }),
108
+ },
109
+
95
110
  tool_executing: {
96
111
  wire: 'tool.executing',
97
112
  transform: (e, runId) => ({
@@ -212,6 +227,16 @@ const MAPPING: {
212
227
  run_id: runId,
213
228
  usage: e.usage,
214
229
  cost: e.cost,
230
+ // Carried, and named apart from `usage` on the wire as well as in
231
+ // the type. A remote surface has exactly the same opportunity to
232
+ // divide cumulative spend by a context window as a local one, and
233
+ // no more information with which to notice.
234
+ ...(e.contextTokens !== undefined ? { context_tokens: e.contextTokens } : {}),
235
+ ...(e.contextMeasuredBy !== undefined ? { context_measured_by: e.contextMeasuredBy } : {}),
236
+ ...(e.contextWindowTokens !== undefined
237
+ ? { context_window_tokens: e.contextWindowTokens }
238
+ : {}),
239
+ ...(e.windowSource !== undefined ? { window_source: e.windowSource } : {}),
215
240
  }),
216
241
  },
217
242
 
@@ -155,6 +155,7 @@ export type StreamEventType =
155
155
  * transcript needs to know its middle was dropped, not infer it.
156
156
  */
157
157
  | 'compaction.completed'
158
+ | 'compaction.failed'
158
159
  /** A guardrail refused or corrected the run. */
159
160
  | 'guardrail.triggered'
160
161
  /**
@@ -0,0 +1,139 @@
1
+ import type { Span } from '@opentelemetry/api'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import type { Agent } from '../../types/agent/core.js'
5
+ import type { AgentManagerContract } from '../../types/agent/manager.js'
6
+ import type {
7
+ AgentTask,
8
+ AgentTaskContext,
9
+ AgentTaskState,
10
+ SendMessageOptions,
11
+ } from '../../types/agent/task.js'
12
+ import type { AgentId, RunId, SessionId, TaskId, TenantId } from '../../types/ids/index.js'
13
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
14
+ import { LocalTaskGateway } from '../local.js'
15
+
16
+ /**
17
+ * `CreateTaskOptions.configOverrides` was declared, typed, and never read.
18
+ *
19
+ * `createTask` built its OWN `configOverrides` object out of `parentSpan`
20
+ * alone, so a caller pinning a delegated run to a cheaper model, or capping
21
+ * its iterations, got the agent's defaults and nothing to say otherwise. The
22
+ * field type-checked, the call succeeded, and the run was not the run that was
23
+ * asked for.
24
+ *
25
+ * Reachability, not behaviour: what `configOverrides` DOES once it lands on
26
+ * `sendMessage` is the agent manager's business and is tested there. What was
27
+ * broken is the hop.
28
+ */
29
+
30
+ /** Records what the gateway actually asked the manager for. */
31
+ class RecordingManager implements AgentManagerContract {
32
+ readonly sent: SendMessageOptions[] = []
33
+
34
+ async sendMessage(options: SendMessageOptions): Promise<AgentTask> {
35
+ this.sent.push(options)
36
+ return {
37
+ taskId: `task_${this.sent.length}` as TaskId,
38
+ agentId: options.agentId,
39
+ agent: {} as Agent<never, never>,
40
+ childAbortController: new AbortController(),
41
+ context: {} as AgentTaskContext,
42
+ state: 'completed' as AgentTaskState,
43
+ pendingMessages: [],
44
+ createdAt: 1,
45
+ } as AgentTask
46
+ }
47
+
48
+ cancel(): void {}
49
+ cancelAll(): void {}
50
+ async continueTask(): Promise<void> {}
51
+ queueMessage(): void {}
52
+ drainMessages() {
53
+ return []
54
+ }
55
+ async waitForCompletion(): Promise<void> {}
56
+ getInstance(): AgentTask | undefined {
57
+ return undefined
58
+ }
59
+ listByParent(): AgentTask[] {
60
+ return []
61
+ }
62
+ listActive(): AgentTask[] {
63
+ return []
64
+ }
65
+ getState(): AgentTaskState | undefined {
66
+ return undefined
67
+ }
68
+ on(): void {}
69
+ off(): void {}
70
+ cleanup(): void {}
71
+ dispose(): void {}
72
+ }
73
+
74
+ function context(): AgentTaskContext {
75
+ return {
76
+ parentRunId: 'run_parent' as RunId,
77
+ parentAgentId: 'supervisor',
78
+ parentAbortController: new AbortController(),
79
+ depth: 0,
80
+ budgetTracker: { total: 100_000, remaining: 100_000 },
81
+ tenantId: 'tnt_k' as TenantId,
82
+ threadId: 'thd_k' as ThreadId,
83
+ sessionId: 'ses_k' as SessionId,
84
+ projectId: 'prj_k' as ProjectId,
85
+ parentActor: { kind: 'agent', agentId: 'supervisor' as AgentId, tenantId: 'tnt_k' as TenantId },
86
+ } as AgentTaskContext
87
+ }
88
+
89
+ describe('a delegated run is built with the config its caller asked for', () => {
90
+ it('forwards configOverrides to the spawn', async () => {
91
+ const manager = new RecordingManager()
92
+ const gateway = new LocalTaskGateway(manager, context())
93
+
94
+ await gateway.createTask({
95
+ agentId: 'worker',
96
+ prompt: 'work',
97
+ workingDirectory: '/tmp',
98
+ configOverrides: { model: 'cheap-model', maxIterations: 3 },
99
+ })
100
+
101
+ expect(manager.sent[0]?.configOverrides).toMatchObject({
102
+ model: 'cheap-model',
103
+ maxIterations: 3,
104
+ })
105
+ })
106
+
107
+ it('keeps the dedicated parentSpan option winning when both name a span', async () => {
108
+ // A caller who sets both is saying the same thing twice, and the named
109
+ // field is the specific one for the job — so it is applied last.
110
+ const manager = new RecordingManager()
111
+ const gateway = new LocalTaskGateway(manager, context())
112
+ const named = { spanContext: () => ({ traceId: 'named' }) } as unknown as Span
113
+ const buried = { spanContext: () => ({ traceId: 'buried' }) } as unknown as Span
114
+
115
+ await gateway.createTask({
116
+ agentId: 'worker',
117
+ prompt: 'work',
118
+ workingDirectory: '/tmp',
119
+ parentSpan: named,
120
+ configOverrides: { parentSpan: buried, model: 'cheap-model' },
121
+ })
122
+
123
+ expect(manager.sent[0]?.configOverrides?.parentSpan).toBe(named)
124
+ // ...and the rest of the caller's overrides survive alongside it.
125
+ expect(manager.sent[0]?.configOverrides?.model).toBe('cheap-model')
126
+ })
127
+
128
+ it('still sends no configOverrides at all when the caller set neither', async () => {
129
+ // The absent case has to stay absent: an empty object here would put
130
+ // `configOverrides: {}` on every spawn and override nothing, which is
131
+ // harmless until something starts reading its presence as intent.
132
+ const manager = new RecordingManager()
133
+ const gateway = new LocalTaskGateway(manager, context())
134
+
135
+ await gateway.createTask({ agentId: 'worker', prompt: 'work', workingDirectory: '/tmp' })
136
+
137
+ expect(manager.sent[0]).not.toHaveProperty('configOverrides')
138
+ })
139
+ })