@namzu/sdk 6.1.0 → 6.2.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 (52) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +5 -0
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js +31 -0
  6. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js.map +1 -1
  7. package/dist/gateway/__tests__/completion-inbox.test.js +17 -0
  8. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
  9. package/dist/gateway/completion-inbox.d.ts.map +1 -1
  10. package/dist/gateway/completion-inbox.js +17 -3
  11. package/dist/gateway/completion-inbox.js.map +1 -1
  12. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts +2 -0
  13. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts.map +1 -0
  14. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js +122 -0
  15. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js.map +1 -0
  16. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  17. package/dist/runtime/query/iteration/phases/context.js +43 -13
  18. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  19. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts +2 -0
  20. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts.map +1 -0
  21. package/dist/tools/builtins/__tests__/bash.proc-test.js +115 -0
  22. package/dist/tools/builtins/__tests__/bash.proc-test.js.map +1 -0
  23. package/dist/tools/builtins/__tests__/bash.test.d.ts +2 -0
  24. package/dist/tools/builtins/__tests__/bash.test.d.ts.map +1 -0
  25. package/dist/tools/builtins/__tests__/bash.test.js +65 -0
  26. package/dist/tools/builtins/__tests__/bash.test.js.map +1 -0
  27. package/dist/tools/builtins/bash.d.ts.map +1 -1
  28. package/dist/tools/builtins/bash.js +89 -17
  29. package/dist/tools/builtins/bash.js.map +1 -1
  30. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts +2 -0
  31. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts.map +1 -0
  32. package/dist/tools/coordinator/__tests__/allow-delegation.test.js +94 -0
  33. package/dist/tools/coordinator/__tests__/allow-delegation.test.js.map +1 -0
  34. package/dist/tools/coordinator/index.d.ts +9 -0
  35. package/dist/tools/coordinator/index.d.ts.map +1 -1
  36. package/dist/tools/coordinator/index.js +19 -2
  37. package/dist/tools/coordinator/index.js.map +1 -1
  38. package/dist/types/agent/supervisor.d.ts +27 -0
  39. package/dist/types/agent/supervisor.d.ts.map +1 -1
  40. package/package.json +3 -2
  41. package/src/agents/SupervisorAgent.ts +5 -0
  42. package/src/agents/__tests__/supervisor-coordinator-registration.test.ts +37 -0
  43. package/src/gateway/__tests__/completion-inbox.test.ts +23 -0
  44. package/src/gateway/completion-inbox.ts +17 -3
  45. package/src/runtime/query/__tests__/run-survives-its-own-park.proc-test.ts +127 -0
  46. package/src/runtime/query/iteration/phases/context.ts +41 -15
  47. package/src/tools/builtins/__tests__/bash.proc-test.ts +142 -0
  48. package/src/tools/builtins/__tests__/bash.test.ts +79 -0
  49. package/src/tools/builtins/bash.ts +100 -18
  50. package/src/tools/coordinator/__tests__/allow-delegation.test.ts +120 -0
  51. package/src/tools/coordinator/index.ts +29 -2
  52. package/src/types/agent/supervisor.ts +28 -0
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { BashTool } from '../bash.js'
4
+
5
+ /**
6
+ * What `bash` promises before it runs anything.
7
+ *
8
+ * These need no shell, so they stay in the unit suite. The ones that actually
9
+ * execute a command live in `bash.proc-test.ts` — spawning real processes
10
+ * beside 2594 unit tests flaked four unrelated timing-sensitive ones, so the
11
+ * process suite is separate and has its own CI step.
12
+ */
13
+
14
+ describe('the two clocks agree', () => {
15
+ it('declares a deadline of its own', () => {
16
+ // The executor reads a tool's `timeoutMs` before falling back to its
17
+ // generic default. With none declared, `bash` inherited that default —
18
+ // the same two minutes as its OWN default — so the two agreed by
19
+ // coincidence and diverged the moment a model asked for longer because
20
+ // it knew a build was slow. It got two minutes, from a clock it had not
21
+ // been told about, reported as an abandoned tool rather than as a
22
+ // command that ran out of time.
23
+ expect(BashTool.timeoutMs).toBeDefined()
24
+ })
25
+
26
+ it('puts that deadline above the longest the model may request', () => {
27
+ // So this tool's own clock is the one that fires, and the executor's is
28
+ // a backstop rather than a second clock racing it.
29
+ const accepted = BashTool.inputSchema.safeParse({ command: 'true', timeout: 10 * 60 * 1000 })
30
+
31
+ expect(accepted.success).toBe(true)
32
+ expect(BashTool.timeoutMs as number).toBeGreaterThan(10 * 60 * 1000)
33
+ })
34
+
35
+ it('refuses an over-long request rather than silently shortening it', () => {
36
+ // Refuse, do not degrade. A number the model was not told had changed
37
+ // is how it learns to distrust its own arguments.
38
+ const overCeiling = BashTool.inputSchema.safeParse({
39
+ command: 'true',
40
+ timeout: 60 * 60 * 1000,
41
+ })
42
+
43
+ expect(overCeiling.success, 'the ceiling is not enforced').toBe(false)
44
+ })
45
+
46
+ it('refuses a nonsensical deadline', () => {
47
+ expect(BashTool.inputSchema.safeParse({ command: 'true', timeout: 0 }).success).toBe(false)
48
+ expect(BashTool.inputSchema.safeParse({ command: 'true', timeout: -1 }).success).toBe(false)
49
+ })
50
+
51
+ it('applies its default when none is given', () => {
52
+ const parsed = BashTool.inputSchema.parse({ command: 'true' })
53
+
54
+ expect(parsed.timeout).toBeGreaterThan(0)
55
+ })
56
+ })
57
+
58
+ describe('the input is closed before a shell ever sees it', () => {
59
+ it('refuses an empty command', () => {
60
+ expect(BashTool.inputSchema.safeParse({ command: '' }).success).toBe(false)
61
+ })
62
+
63
+ it('accepts a numeric timeout sent as a string', () => {
64
+ // Providers do this, and the coercion is deliberate.
65
+ const parsed = BashTool.inputSchema.parse({ command: 'true', timeout: '5000' })
66
+
67
+ expect(parsed.timeout).toBe(5000)
68
+ })
69
+ })
70
+
71
+ describe('the danger flag reads the command', () => {
72
+ it('marks a destructive command destructive', () => {
73
+ expect(BashTool.isDestructive?.({ command: 'rm -rf /', timeout: 1000 } as never)).toBe(true)
74
+ })
75
+
76
+ it('leaves an ordinary command alone', () => {
77
+ expect(BashTool.isDestructive?.({ command: 'ls -la', timeout: 1000 } as never)).toBe(false)
78
+ })
79
+ })
@@ -22,6 +22,25 @@ const DEFAULT_BASH_MAX_BUFFER_BYTES = readPositiveIntEnv(
22
22
  100 * 1024 * 1024,
23
23
  )
24
24
 
25
+ /**
26
+ * The longest deadline this tool will accept from the model.
27
+ *
28
+ * There are two clocks on a bash call and until now only one of them was
29
+ * declared. This tool enforces `input.timeout` itself; the EXECUTOR enforces
30
+ * a separate per-tool deadline, and with none declared here it fell back to
31
+ * its own generic default — also two minutes. The two agreed by coincidence,
32
+ * so a model that asked for five minutes because it knew the build was slow
33
+ * got two, from a clock it had not been told about, reported as an abandoned
34
+ * tool rather than as a command that ran out of time.
35
+ *
36
+ * So the tool declares a ceiling and the executor is given a deadline above
37
+ * it (see `timeoutMs` on the definition), which makes this the only clock
38
+ * that can fire in practice. A request past the ceiling is REFUSED rather
39
+ * than quietly clamped: the model asked for something specific, and silently
40
+ * giving it a different number is how it learns to distrust the answer.
41
+ */
42
+ const MAX_BASH_TIMEOUT_MS = readPositiveIntEnv('NAMZU_BASH_MAX_TIMEOUT_MS', 10 * 60 * 1000)
43
+
25
44
  const inputSchema = z.object({
26
45
  command: z
27
46
  .string()
@@ -32,9 +51,11 @@ const inputSchema = z.object({
32
51
  timeout: z
33
52
  .preprocess(
34
53
  (v) => (typeof v === 'string' ? Number(v) : v),
35
- z.number().default(DEFAULT_BASH_TIMEOUT_MS),
54
+ z.number().positive().max(MAX_BASH_TIMEOUT_MS).default(DEFAULT_BASH_TIMEOUT_MS),
36
55
  )
37
- .describe(`Command timeout in milliseconds. Default: ${DEFAULT_BASH_TIMEOUT_MS}`),
56
+ .describe(
57
+ `Command timeout in milliseconds. Default: ${DEFAULT_BASH_TIMEOUT_MS}, maximum: ${MAX_BASH_TIMEOUT_MS}. For work that legitimately runs longer than the maximum, start it in the background and poll, rather than holding the turn open.`,
58
+ ),
38
59
  })
39
60
 
40
61
  type BashInput = z.infer<typeof inputSchema>
@@ -53,6 +74,12 @@ export const BashTool = defineTool({
53
74
  readOnly: false,
54
75
  destructive: (input: BashInput) => isDangerousCommand(input.command),
55
76
  concurrencySafe: false,
77
+ // Above the ceiling the input schema accepts, so the executor's deadline
78
+ // is a backstop rather than a second clock racing this tool's own. It used
79
+ // to be undefined, which meant the executor's generic default applied —
80
+ // the same two minutes as this tool's DEFAULT, so they agreed by accident
81
+ // and diverged the moment a model asked for longer.
82
+ timeoutMs: MAX_BASH_TIMEOUT_MS + 30_000,
56
83
 
57
84
  async execute(input, context) {
58
85
  if (isDangerousCommand(input.command)) {
@@ -133,26 +160,81 @@ export const BashTool = defineTool({
133
160
  // a Stop tore down the model stream and left the command running,
134
161
  // and the executor's deadline could only ever DETACH from the tool
135
162
  // rather than end the work it started.
136
- const { stdout, stderr } = await execAsync(input.command, {
137
- cwd: context.workingDirectory,
138
- timeout: input.timeout,
139
- env: { ...process.env, ...context.env },
140
- maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
141
- signal: context.abortSignal,
142
- })
143
-
144
- const output = [stdout ? `STDOUT:\n${stdout}` : '', stderr ? `STDERR:\n${stderr}` : '']
145
- .filter(Boolean)
146
- .join('\n\n')
147
-
148
- return {
149
- success: true,
150
- output: output || '(no output)',
151
- data: { exitCode: 0 },
163
+ // `exec` REJECTS on a non-zero exit, on its own timeout, and on a
164
+ // kill — and the rejection carries `stdout`, `stderr`, `code` and
165
+ // `killed`. Letting it propagate threw all of that away: the registry
166
+ // turned the throw into a structured failure, so the model was told a
167
+ // command failed and not one word about how.
168
+ //
169
+ // That is the common case, not an edge one. A failing test run and a
170
+ // failing build are the two things an agent runs bash for most, and
171
+ // both exit non-zero WITH the output that explains why. The sandbox
172
+ // branch above already reports all of it; this branch did not, so the
173
+ // same command told the model two different amounts depending on where
174
+ // it happened to run.
175
+ try {
176
+ const { stdout, stderr } = await execAsync(input.command, {
177
+ cwd: context.workingDirectory,
178
+ timeout: input.timeout,
179
+ env: { ...process.env, ...context.env },
180
+ maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
181
+ signal: context.abortSignal,
182
+ })
183
+
184
+ return {
185
+ success: true,
186
+ output: formatShellOutput(stdout, stderr) || '(no output)',
187
+ data: { exitCode: 0 },
188
+ }
189
+ } catch (err) {
190
+ const failure = err as NodeJS.ErrnoException & {
191
+ stdout?: string
192
+ stderr?: string
193
+ code?: number | string
194
+ killed?: boolean
195
+ signal?: string
196
+ }
197
+
198
+ // A caller-owned Stop is the caller's, not a command failure.
199
+ if (context.abortSignal?.aborted) throw err
200
+
201
+ // `exec` reports its own timeout as a kill, and the distinction
202
+ // matters to the model: "ran out of time" is a different next move
203
+ // from "exited 1".
204
+ const timedOut = failure.killed === true && failure.signal === 'SIGTERM'
205
+ const exitCode = typeof failure.code === 'number' ? failure.code : undefined
206
+ const output = formatShellOutput(failure.stdout, failure.stderr)
207
+
208
+ return {
209
+ success: false,
210
+ output: output || '(no output)',
211
+ data: {
212
+ ...(exitCode !== undefined ? { exitCode } : {}),
213
+ timedOut,
214
+ ...(failure.signal ? { signal: failure.signal } : {}),
215
+ },
216
+ error: timedOut
217
+ ? `Command timed out after ${input.timeout}ms. Any output it produced before the deadline is above.`
218
+ : exitCode !== undefined
219
+ ? `Command exited with code ${exitCode}`
220
+ : `Command failed: ${failure.message}`,
221
+ }
152
222
  }
153
223
  },
154
224
  })
155
225
 
226
+ /**
227
+ * The two streams, labelled, with empty ones left out.
228
+ *
229
+ * Shared by the success and failure paths so a command tells the model the
230
+ * same shape either way — the failure path used to tell it nothing at all.
231
+ */
232
+ function formatShellOutput(stdout: string | undefined, stderr: string | undefined): string {
233
+ return [stdout ? `STDOUT:\n${stdout}` : '', stderr ? `STDERR:\n${stderr}` : '']
234
+ .filter(Boolean)
235
+ .join('\n\n')
236
+ }
237
+
156
238
  function readPositiveIntEnv(key: string, fallback: number): number {
157
239
  const value = process.env[key]?.trim()
158
240
  if (!value) return fallback
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { TaskGateway } from '../../../types/agent/gateway.js'
4
+ import { buildCoordinatorTools } from '../index.js'
5
+
6
+ /**
7
+ * Whether a run may delegate is not the same question as who it may delegate
8
+ * to, and only the caller can answer the first.
9
+ *
10
+ * The roster answers WHO. It cannot answer WHETHER, because two runs are
11
+ * indistinguishable in it: a supervisor whose roster happens to hold one
12
+ * specialist, where delegating is the point; and a run whose own persona IS
13
+ * that specialist, where delegating is delegating to itself. A host builds the
14
+ * second by putting a specialist's persona into the supervisor shell and its
15
+ * id into the roster — so a predicate comparing the roster against the
16
+ * executing agent sees two different ids and cheerfully says "can delegate".
17
+ *
18
+ * Measured before this existed: such a run carried `create_task`,
19
+ * `wait_for_task`, `cancel_task` and `agent_task_list`, byte-identical to a
20
+ * run that could actually delegate, and the model could only discover the
21
+ * refusal by spending a turn on it.
22
+ */
23
+
24
+ const gateway = {
25
+ listTasks: () => [],
26
+ onTaskCompleted: () => () => {},
27
+ } as unknown as TaskGateway
28
+
29
+ function namesFor(opts: {
30
+ agentIds: string[]
31
+ allowDelegation?: boolean
32
+ withHitl?: boolean
33
+ }): string[] {
34
+ return buildCoordinatorTools({
35
+ gateway,
36
+ workingDirectory: '/tmp/test',
37
+ allowedAgentIds: opts.agentIds,
38
+ ...(opts.allowDelegation !== undefined ? { allowDelegation: opts.allowDelegation } : {}),
39
+ ...(opts.withHitl
40
+ ? { resumeHandler: (async () => ({ action: 'continue' })) as never, runId: 'run_1' as never }
41
+ : {}),
42
+ }).map((t) => t.name)
43
+ }
44
+
45
+ describe('a run can decline to delegate while still naming who it would have called', () => {
46
+ it('withholds the delegation tools when delegation is off', () => {
47
+ const names = namesFor({ agentIds: ['specialist'], allowDelegation: false })
48
+
49
+ expect(names).not.toContain('create_task')
50
+ expect(names).not.toContain('wait_for_task')
51
+ expect(names).not.toContain('cancel_task')
52
+ })
53
+
54
+ it('produces exactly the empty-roster surface', () => {
55
+ // The two reasons differ but the outcome is the same one tool, so a
56
+ // reader does not have to hold two shapes in their head.
57
+ expect(namesFor({ agentIds: ['specialist'], allowDelegation: false })).toEqual(
58
+ namesFor({ agentIds: [] }),
59
+ )
60
+ })
61
+
62
+ it('keeps the listing, because a run may still want to see what is running', () => {
63
+ expect(namesFor({ agentIds: ['specialist'], allowDelegation: false })).toContain(
64
+ 'agent_task_list',
65
+ )
66
+ })
67
+
68
+ it('leaves the human channel alone — that is not delegation', () => {
69
+ // `approve_plan` and `ask_user_question` are the HITL park surface. A
70
+ // run that must not delegate needs them as much as any other.
71
+ const names = namesFor({ agentIds: ['specialist'], allowDelegation: false, withHitl: true })
72
+
73
+ expect(names).toContain('ask_user_question')
74
+ })
75
+ })
76
+
77
+ describe('an absent flag changes nothing', () => {
78
+ it('mounts the full surface, as it always did', () => {
79
+ const names = namesFor({ agentIds: ['specialist'] })
80
+
81
+ expect(names).toContain('create_task')
82
+ expect(names).toContain('wait_for_task')
83
+ expect(names).toContain('cancel_task')
84
+ expect(names).toContain('agent_task_list')
85
+ })
86
+
87
+ it('is identical to opting in explicitly', () => {
88
+ // So adopting the flag cannot change a caller that says yes out loud.
89
+ expect(namesFor({ agentIds: ['a', 'b'], allowDelegation: true })).toEqual(
90
+ namesFor({ agentIds: ['a', 'b'] }),
91
+ )
92
+ })
93
+
94
+ it('still withholds everything on an empty roster, flag or no flag', () => {
95
+ expect(namesFor({ agentIds: [], allowDelegation: true })).toEqual(['agent_task_list'])
96
+ })
97
+ })
98
+
99
+ describe('the flag is absolute', () => {
100
+ it('cannot be overridden back on', () => {
101
+ // Worth a test precisely because the opposite is the intuitive guess:
102
+ // "explicit beats implicit" would say a runtime override should win.
103
+ // It cannot, mechanically — the override pass in SupervisorAgent runs
104
+ // over the array this builder returns, and there is no entry for it to
105
+ // act on. And it should not: both values come from the same caller in
106
+ // the same call, so "this run must not delegate" plus "give it
107
+ // create_task" is a caller contradicting itself, not one who knows
108
+ // something extra.
109
+ //
110
+ // Same rule the empty roster has always had.
111
+ const tools = buildCoordinatorTools({
112
+ gateway,
113
+ workingDirectory: '/tmp/test',
114
+ allowedAgentIds: ['specialist'],
115
+ allowDelegation: false,
116
+ })
117
+
118
+ expect(tools.find((t) => t.name === 'create_task')).toBeUndefined()
119
+ })
120
+ })
@@ -34,6 +34,16 @@ export interface CoordinatorToolsOptions {
34
34
  runtimeContext?: AgentRuntimeContext
35
35
  allowedAgentIds: string[]
36
36
 
37
+ /**
38
+ * May this run delegate at all? Defaults to true.
39
+ *
40
+ * Same field, same name, as SupervisorAgentConfig.allowDelegation — the
41
+ * name is kept identical deliberately. This options bag already renames
42
+ * agentIds to allowedAgentIds, and a second rename on the road between
43
+ * the config and the decision would make the road untraceable.
44
+ */
45
+ allowDelegation?: boolean
46
+
37
47
  taskStore?: TaskStore
38
48
 
39
49
  runId?: RunId
@@ -253,6 +263,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
253
263
  const {
254
264
  gateway,
255
265
  allowedAgentIds: agentIds,
266
+ allowDelegation,
256
267
  taskStore,
257
268
  runId,
258
269
  getPlanManager,
@@ -715,8 +726,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
715
726
  // had to invent something to say — and one that would not do that was left
716
727
  // calling `agent_task_list` in a sleep loop. That was never the model
717
728
  // misbehaving; it was the only move available.
718
- const tools: ToolDefinition[] =
719
- agentIds.length > 0 ? [createTask, waitForTaskTool, cancelTask, agentTaskList] : [agentTaskList]
729
+ // Two independent reasons not to mount the delegation surface, and the
730
+ // second one is not derivable from the first.
731
+ //
732
+ // An empty roster answers WHO may be called: nobody, so the tools have
733
+ // nothing to act on. `allowDelegation: false` answers WHETHER this run may
734
+ // call anyone, which a non-empty roster cannot settle — a host that runs a
735
+ // specialist by putting its persona into the supervisor shell and its id
736
+ // into the roster has a list of one and must still delegate to nobody.
737
+ // From inside this function that run is indistinguishable from a
738
+ // supervisor whose roster happens to hold a single specialist, so the
739
+ // caller states the fact rather than the SDK guessing it.
740
+ //
741
+ // `!== false` rather than truthiness, so an absent flag keeps today's
742
+ // behaviour exactly.
743
+ const canDelegate = agentIds.length > 0 && allowDelegation !== false
744
+ const tools: ToolDefinition[] = canDelegate
745
+ ? [createTask, waitForTaskTool, cancelTask, agentTaskList]
746
+ : [agentTaskList]
720
747
 
721
748
  if (getPlanManager) {
722
749
  const approvePlan = defineTool({
@@ -19,6 +19,34 @@ export interface SupervisorAgentConfig extends BaseAgentConfig {
19
19
 
20
20
  agentIds: string[]
21
21
 
22
+ /**
23
+ * May this run invoke subagents at all? Defaults to `true`.
24
+ *
25
+ * `agentIds` answers WHO may be called; this answers WHETHER, and they are
26
+ * different questions. A run whose own persona is the single agent on the
27
+ * list has a non-empty list and still must not call anyone.
28
+ *
29
+ * It cannot be derived. Comparing the list against the executing agent
30
+ * fails where a host substitutes a specialist's persona into the
31
+ * supervisor shell — the two ids differ, so the predicate says "can
32
+ * delegate" about a run that cannot. And no predicate over `agentIds`
33
+ * could work, because a supervisor whose list holds one specialist and a
34
+ * run that IS that specialist are indistinguishable in it. The fact lives
35
+ * with the caller, so the caller states it.
36
+ *
37
+ * Positive polarity on purpose: `allowDelegation: false` reads correctly
38
+ * the first time, where a `delegationDisabled` spelling inverts twice at
39
+ * every site that consults it.
40
+ *
41
+ * **Absolute.** `runtimeToolOverrides` cannot put the delegation tools
42
+ * back — the override pass runs over the tools this flag already declined
43
+ * to build, and both values come from the same caller in the same call, so
44
+ * "must not delegate" plus "give it `create_task`" is a contradiction
45
+ * rather than extra knowledge. This matches `agentIds: []`, which is
46
+ * absolute today for the same reason.
47
+ */
48
+ allowDelegation?: boolean
49
+
22
50
  gateway?: TaskGateway
23
51
  agentManager?: AgentManagerContract
24
52
  tools?: ToolRegistryContract