deepclause-sdk 0.1.1 → 0.1.2

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 (79) hide show
  1. package/dist/agent.d.ts +12 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +116 -9
  4. package/dist/agent.js.map +1 -1
  5. package/dist/cli/config.d.ts +24 -24
  6. package/dist/cli/repl.d.ts +2 -0
  7. package/dist/cli/repl.d.ts.map +1 -1
  8. package/dist/cli/repl.js +26 -0
  9. package/dist/cli/repl.js.map +1 -1
  10. package/dist/cli/run.d.ts.map +1 -1
  11. package/dist/cli/run.js +10 -0
  12. package/dist/cli/run.js.map +1 -1
  13. package/dist/prolog/loader.d.ts +32 -0
  14. package/dist/prolog/loader.d.ts.map +1 -1
  15. package/dist/prolog/loader.js +87 -0
  16. package/dist/prolog/loader.js.map +1 -1
  17. package/dist/prolog-src/deepclause_mi.pl +1028 -118
  18. package/dist/repl/agent.d.ts +15 -1
  19. package/dist/repl/agent.d.ts.map +1 -1
  20. package/dist/repl/agent.js +44 -5
  21. package/dist/repl/agent.js.map +1 -1
  22. package/dist/repl/harness-tools.d.ts +24 -2
  23. package/dist/repl/harness-tools.d.ts.map +1 -1
  24. package/dist/repl/harness-tools.js +14 -5
  25. package/dist/repl/harness-tools.js.map +1 -1
  26. package/dist/repl/index.d.ts +8 -0
  27. package/dist/repl/index.d.ts.map +1 -1
  28. package/dist/repl/index.js +1 -0
  29. package/dist/repl/index.js.map +1 -1
  30. package/dist/repl/render.d.ts +8 -1
  31. package/dist/repl/render.d.ts.map +1 -1
  32. package/dist/repl/render.js +81 -16
  33. package/dist/repl/render.js.map +1 -1
  34. package/dist/repl/session.d.ts +61 -4
  35. package/dist/repl/session.d.ts.map +1 -1
  36. package/dist/repl/session.js +174 -9
  37. package/dist/repl/session.js.map +1 -1
  38. package/dist/repl/tool-arguments.d.ts +36 -0
  39. package/dist/repl/tool-arguments.d.ts.map +1 -0
  40. package/dist/repl/tool-arguments.js +123 -0
  41. package/dist/repl/tool-arguments.js.map +1 -0
  42. package/dist/repl/toolset.d.ts +28 -1
  43. package/dist/repl/toolset.d.ts.map +1 -1
  44. package/dist/repl/toolset.js +52 -4
  45. package/dist/repl/toolset.js.map +1 -1
  46. package/dist/runner.d.ts +31 -0
  47. package/dist/runner.d.ts.map +1 -1
  48. package/dist/runner.js +192 -17
  49. package/dist/runner.js.map +1 -1
  50. package/dist/system/assets/docs/AGENT_PROMPT.md +925 -43
  51. package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
  52. package/dist/system/assets/docs/DML_REFERENCE.md +620 -39
  53. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
  54. package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
  55. package/dist/system/environment.d.ts +36 -0
  56. package/dist/system/environment.d.ts.map +1 -0
  57. package/dist/system/environment.js +81 -0
  58. package/dist/system/environment.js.map +1 -0
  59. package/dist/system/runtime/shell-manager.d.ts +40 -2
  60. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  61. package/dist/system/runtime/shell-manager.js +82 -16
  62. package/dist/system/runtime/shell-manager.js.map +1 -1
  63. package/dist/system/runtime/shell-tool-events.js +25 -1
  64. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  65. package/dist/types.d.ts +20 -1
  66. package/dist/types.d.ts.map +1 -1
  67. package/package.json +4 -2
  68. package/src/prolog-src/deepclause_mi.pl +1028 -118
  69. package/dist/compaction.d.ts +0 -98
  70. package/dist/compaction.d.ts.map +0 -1
  71. package/dist/compaction.js +0 -471
  72. package/dist/compaction.js.map +0 -1
  73. package/dist/repl/tools.d.ts +0 -53
  74. package/dist/repl/tools.d.ts.map +0 -1
  75. package/dist/repl/tools.js +0 -307
  76. package/dist/repl/tools.js.map +0 -1
  77. package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
  78. package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
  79. package/dist/system/assets/docs/META_PROMPT.md +0 -68
package/dist/runner.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import { mountWorkspace } from './prolog/loader.js';
5
5
  import { runAgentLoop } from './agent.js';
6
+ import { takePrologMessages } from './prolog/loader.js';
7
+ import { validateToolArguments } from './repl/tool-arguments.js';
6
8
  import { judgeAnswersToValues, judgeCacheKey, normalizeJudgeState, parseJudgeQuestions, validateJudgeAnswers, valuesToPrologList } from './judge/marshal.js';
7
9
  import { buildToolCompletionEvent, buildToolFailureEvent, buildToolStartEvent, } from './system/runtime/shell-tool-events.js';
8
10
  import { checkToolPolicy } from './tools.js';
@@ -111,11 +113,66 @@ export function decodeAgentOutputVars(rawOutputVars) {
111
113
  && 'type' in value
112
114
  && typeof value.name === 'string'
113
115
  && typeof value.type === 'string') {
114
- return value;
116
+ const record = value;
117
+ const typed = value;
118
+ if (Array.isArray(record.fields)) {
119
+ typed.fields = record.fields
120
+ .map((raw) => {
121
+ const field = toJsValue(raw);
122
+ if (!field || typeof field !== 'object')
123
+ return undefined;
124
+ const f = field;
125
+ const name = f.name === undefined ? undefined : String(toJsValue(f.name));
126
+ if (!name)
127
+ return undefined;
128
+ const required = Number(toJsValue(f.required)) === 1;
129
+ const rawDefault = toJsValue(f.default);
130
+ const hasDefault = rawDefault !== undefined && rawDefault !== 'no_default';
131
+ return { name, type: String(toJsValue(f.type) ?? 'string'), required, ...(hasDefault ? { default: rawDefault } : {}) };
132
+ })
133
+ .filter((field) => field !== undefined);
134
+ }
135
+ return typed;
115
136
  }
116
137
  return String(value);
117
138
  });
118
139
  }
140
+ /**
141
+ * User tool info from Prolog
142
+ */
143
+ /**
144
+ * The error a tool call gets when the tool is already running further up the
145
+ * call chain. Shared by the runtime guard and the withheld-tool stub.
146
+ */
147
+ export function unavailableToolError(toolName) {
148
+ return `Tool '${toolName}' is not available in this task: it is already running further up the `
149
+ + 'call chain, and calling it again would recurse. Do not call it again; work with the text '
150
+ + 'you were given, or say what is missing.';
151
+ }
152
+ /**
153
+ * Parse the `toolScope` a `task/N` carries from `with_tools/2` / `without_tools/2`.
154
+ *
155
+ * Shape: `{$t:'t', whitelist:[[Names]]}` / `blacklist:[[Names]]`, or the atom
156
+ * `none`. The extra list nesting comes from the term's argument list.
157
+ */
158
+ export function parseToolScope(raw) {
159
+ const value = toJsValue(raw);
160
+ if (!value || typeof value !== 'object' || !('$t' in value)) {
161
+ return { type: 'none', list: [] };
162
+ }
163
+ const compound = value;
164
+ const kind = 'whitelist' in compound ? 'whitelist' : 'blacklist' in compound ? 'blacklist' : null;
165
+ if (!kind)
166
+ return { type: 'none', list: [] };
167
+ const args = toJsValue(compound[kind]);
168
+ if (!Array.isArray(args) || args.length === 0)
169
+ return { type: kind, list: [] };
170
+ const inner = toJsValue(args[0]);
171
+ if (!Array.isArray(inner) || inner.length === 0)
172
+ return { type: kind, list: [] };
173
+ const names = toJsValue(inner[0]);
174
+ return { type: kind, list: Array.isArray(names) ? names.map((n) => String(n)) : [] };
175
+ }
119
176
  /**
120
177
  * Map Prolog types to JSON Schema types
121
178
  */
@@ -145,6 +202,16 @@ export class DMLRunner {
145
202
  currentMemory = [];
146
203
  stepCounter = 0;
147
204
  judgeCache = new Map();
205
+ /**
206
+ * DML tools currently executing, innermost last.
207
+ *
208
+ * A tool must not be reachable by any agent it spawns, or `foo(A) :- task(...)`
209
+ * recurses: the agent inside `task/N` calls `foo`, which spawns another agent,
210
+ * and so on. The tool list is built *before* the tool runs (so a static list
211
+ * cannot know), which is why the check is applied when the model calls the
212
+ * tool rather than when the list is built.
213
+ */
214
+ toolCallStack = [];
148
215
  /**
149
216
  * Runner-owned frame table: FrameId -> { frame, engine }.
150
217
  * The Prolog session owns the authoritative engine handles
@@ -203,6 +270,9 @@ export class DMLRunner {
203
270
  yield { type: 'error', content: engineResult.error };
204
271
  return;
205
272
  }
273
+ // Now that the session module exists, install the workspace predicates so
274
+ // an SDK run resolves `read_file/2` and friends exactly like the REPL.
275
+ this.query(`deepclause_mi:repl_inject_workspace_predicates('${this.sessionId}')`);
206
276
  const frame = this.defaultFrameId ? this.frames.get(this.defaultFrameId)?.frame : undefined;
207
277
  if (!frame) {
208
278
  yield { type: 'error', content: 'Failed to register default query frame' };
@@ -256,6 +326,15 @@ export class DMLRunner {
256
326
  }
257
327
  // Step the frame
258
328
  const step = this.stepFrame(frame.id);
329
+ // Prolog's own output (warnings from the term reader, `format(user_error,
330
+ // ...)`, a stray `write/1`) is captured by the loader rather than printed,
331
+ // and surfaced here so it renders in order and through the normal path.
332
+ // This is what keeps a warning from scribbling over an interactive frame.
333
+ for (const message of takePrologMessages()) {
334
+ yield message.channel === 'stdout'
335
+ ? { type: 'output', content: message.text, channel: message.channel }
336
+ : { type: 'log', content: message.text, level: message.level, channel: message.channel };
337
+ }
259
338
  if (process.env.DEBUG_STEPS) {
260
339
  console.log('[MAIN_LOOP] step:', step.status, step.content?.substring(0, 50));
261
340
  }
@@ -753,14 +832,32 @@ export class DMLRunner {
753
832
  // Build available tools for the agent: the harness's DML tool/2 predicates
754
833
  // plus registered TS tools (reachable via exec/2).
755
834
  const selfReportingTools = undefined;
756
- const availableTools = this.buildAgentTools(userTools, options.toolPolicy, async (toolName, args) => {
835
+ // Tools already executing further up the call chain are NOT offered to this
836
+ // agent. This is the `task/N` inside a tool: the tool that spawned this agent
837
+ // is on the stack, and offering it would invite the model to call the thing
838
+ // that is already running. A withheld tool is still declared (with the reason
839
+ // in its description) so a stray call gets an explanation, but it cannot run.
840
+ const withheld = [...this.toolCallStack];
841
+ // `with_tools/2` / `without_tools/2` on this `task/N` narrow the list further.
842
+ // This path used to ignore the scope, so scoping a `task/N` inside a predicate
843
+ // silently did nothing.
844
+ const scope = parseToolScope(rawPayload.toolScope);
845
+ const scopedUserTools = scope.type === 'whitelist'
846
+ ? userTools.filter((t) => scope.list.includes(t.name))
847
+ : scope.type === 'blacklist'
848
+ ? userTools.filter((t) => !scope.list.includes(t.name))
849
+ : userTools;
850
+ const availableTools = this.buildAgentTools(scopedUserTools, options.toolPolicy, async (toolName, args) => {
851
+ if (this.toolCallStack.includes(toolName)) {
852
+ return { result: { error: unavailableToolError(toolName) }, outputs: [] };
853
+ }
757
854
  return this.executeToolIsolated(toolName, args, augmentedTools, onToolOutput, {
758
855
  workspacePath: options.workspacePath,
759
856
  signal: options.signal,
760
857
  toolPolicy: options.toolPolicy,
761
858
  onModelCall: options.onModelCall,
762
- }, [], emitToolCall, emitToolEvent);
763
- });
859
+ }, withheld, emitToolCall, emitToolEvent);
860
+ }, withheld);
764
861
  // Run agent loop with streaming support
765
862
  const resultPromise = runAgentLoop({
766
863
  taskDescription,
@@ -768,6 +865,7 @@ export class DMLRunner {
768
865
  memory: effectiveMemory,
769
866
  tools: availableTools,
770
867
  workspacePath: options.workspacePath,
868
+ environment: this.options.environment,
771
869
  resolveMedia: (refs) => this.readMediaRefs(options.workspacePath, refs),
772
870
  modelOptions: {
773
871
  model: scopedModel?.id ?? this.options.model,
@@ -1127,7 +1225,11 @@ export class DMLRunner {
1127
1225
  ? this.internalAskUserTool(options)
1128
1226
  : options.tools.get(toolName);
1129
1227
  if (!tool) {
1130
- yield { type: 'log', content: `Tool '${toolName}' not found` };
1228
+ // Emit start+failure so the renderer shows the reason. A lone `log` event
1229
+ // is not rendered in goal mode, so an unknown tool used to fail with no
1230
+ // explanation at all — just `false.`
1231
+ yield buildToolStartEvent(toolName, {});
1232
+ yield buildToolFailureEvent(toolName, {}, new Error(`Tool not found: ${toolName}`));
1131
1233
  this.postExecResult({ success: false, error: `Tool not found: ${toolName}` });
1132
1234
  return;
1133
1235
  }
@@ -1135,6 +1237,15 @@ export class DMLRunner {
1135
1237
  try {
1136
1238
  // Execute tool
1137
1239
  argsObj = this.argsToObject(args, tool);
1240
+ // A call that names a parameter the tool does not have cannot mean what it
1241
+ // says, and dropping the key silently is how `stdin` looked like it worked.
1242
+ const validation = validateToolArguments(toolName, tool.parameters, argsObj);
1243
+ if (!validation.ok) {
1244
+ yield buildToolStartEvent(toolName, argsObj);
1245
+ yield buildToolFailureEvent(toolName, argsObj, new Error(validation.error));
1246
+ this.postExecResult({ success: false, error: validation.error });
1247
+ return;
1248
+ }
1138
1249
  // Emit tool_call event before execution
1139
1250
  yield buildToolStartEvent(toolName, argsObj);
1140
1251
  const result = await tool.execute(argsObj, { signal: options.signal });
@@ -1309,14 +1420,26 @@ export class DMLRunner {
1309
1420
  * Only includes tools defined in DML - registered TypeScript tools are NOT exposed to the LLM.
1310
1421
  * DML tools can internally call exec/2 to use registered tools.
1311
1422
  */
1312
- buildAgentTools(userTools, _policy, executeToolInline) {
1423
+ buildAgentTools(userTools, _policy, executeToolInline, excludedTools = []) {
1313
1424
  const result = new Map();
1425
+ if (process.env.DC_TRACE_TOOLS) {
1426
+ // Append to a file: inside the TUI, stderr is not visible.
1427
+ try {
1428
+ const { appendFileSync } = require('fs');
1429
+ appendFileSync(process.env.DC_TRACE_TOOLS, `executing=${JSON.stringify(this.toolCallStack)} withheld=${JSON.stringify(excludedTools)} offered=${JSON.stringify(userTools.map((t) => t.name))}\n`);
1430
+ }
1431
+ catch { /* diagnostics must not break a run */ }
1432
+ }
1314
1433
  // Add user-defined tools from DML file with schema info
1315
1434
  // These are the ONLY tools the LLM can call during task()
1316
1435
  // Do not apply the host runtime-tool policy to these DML predicates. Their
1317
1436
  // nested exec/2 calls pass through handleExec/executeToolIsolated, where
1318
1437
  // the policy still restricts actual host capabilities.
1319
1438
  for (const tool of userTools) {
1439
+ // A withheld tool keeps its place in the list (so a call gets the reason),
1440
+ // but carries no schema and a refusing implementation.
1441
+ if (excludedTools.includes(tool.name))
1442
+ continue;
1320
1443
  // Build JSON Schema properties from inputs
1321
1444
  const properties = {};
1322
1445
  const required = [];
@@ -1339,12 +1462,33 @@ export class DMLRunner {
1339
1462
  required: required.length > 0 ? required : undefined,
1340
1463
  },
1341
1464
  execute: async (args) => {
1465
+ if (process.env.DC_TRACE_TOOLS) {
1466
+ // eslint-disable-next-line no-console
1467
+ console.error('[toolCall] %s executing=[%s] -> %s', toolName, this.toolCallStack.join(','), this.toolCallStack.includes(toolName) ? 'REFUSED' : 'ran');
1468
+ }
1469
+ // The list is built before the tool runs, so a tool already on the call
1470
+ // stack is refused here rather than being withheld from the list.
1471
+ if (this.toolCallStack.includes(toolName)) {
1472
+ return { error: unavailableToolError(toolName) };
1473
+ }
1342
1474
  // Execute tool inline via main Prolog engine (shares state with task)
1343
1475
  const { result: toolResult } = await executeToolInline(toolName, args);
1344
1476
  return toolResult;
1345
1477
  },
1346
1478
  });
1347
1479
  }
1480
+ // Withheld tools are still declared, but their implementation refuses with
1481
+ // the reason. Dropping them entirely makes a call look like a missing tool,
1482
+ // which reads as a broken harness and invites a retry loop.
1483
+ for (const tool of userTools) {
1484
+ if (!excludedTools.includes(tool.name))
1485
+ continue;
1486
+ result.set(tool.name, {
1487
+ description: `${tool.description || `Tool: ${tool.name}`} (unavailable in this task)`,
1488
+ parameters: { type: 'object', properties: {} },
1489
+ execute: async () => ({ error: unavailableToolError(tool.name) }),
1490
+ });
1491
+ }
1348
1492
  // NOTE: Registered TypeScript tools are NOT added here.
1349
1493
  // They are only available via exec/2 in DML code.
1350
1494
  // DML tools can call exec/2 internally to use registered tools.
@@ -1366,6 +1510,20 @@ export class DMLRunner {
1366
1510
  * @param onToolCall - Callback to emit tool call events from nested tasks
1367
1511
  */
1368
1512
  async executeToolIsolated(toolName, args, registeredTools, onOutput, runContext, excludedTools = [], onToolCall, onToolEvent) {
1513
+ if (this.toolCallStack.includes(toolName)) {
1514
+ return { result: { error: unavailableToolError(toolName) }, outputs: [] };
1515
+ }
1516
+ this.toolCallStack.push(toolName);
1517
+ try {
1518
+ return await this.executeToolIsolatedInner(toolName, args, registeredTools, onOutput, runContext, excludedTools, onToolCall, onToolEvent);
1519
+ }
1520
+ finally {
1521
+ const index = this.toolCallStack.lastIndexOf(toolName);
1522
+ if (index !== -1)
1523
+ this.toolCallStack.splice(index, 1);
1524
+ }
1525
+ }
1526
+ async executeToolIsolatedInner(toolName, args, registeredTools, onOutput, runContext, excludedTools = [], onToolCall, onToolEvent) {
1369
1527
  const outputs = [];
1370
1528
  if (process.env.DEBUG_RUNNER) {
1371
1529
  console.log('[RUNNER] executeToolIsolated called for:', toolName);
@@ -1422,6 +1580,8 @@ export class DMLRunner {
1422
1580
  const tool = registeredTools.get(execToolName);
1423
1581
  if (!tool) {
1424
1582
  // Tool not found - post failure and continue
1583
+ onToolEvent?.(buildToolStartEvent(execToolName, {}));
1584
+ onToolEvent?.(buildToolFailureEvent(execToolName, {}, new Error(`Tool not found: ${execToolName}`)));
1425
1585
  const postGoal = `deepclause_mi:post_exec_result('${this.sessionId}', failure, "Tool not found: ${execToolName}")`;
1426
1586
  this.query(postGoal);
1427
1587
  }
@@ -1429,6 +1589,13 @@ export class DMLRunner {
1429
1589
  let argsObj = null;
1430
1590
  try {
1431
1591
  argsObj = this.argsToObject(execArgs, tool);
1592
+ const validation = validateToolArguments(execToolName, tool.parameters, argsObj);
1593
+ if (!validation.ok) {
1594
+ onToolEvent?.(buildToolStartEvent(execToolName, argsObj));
1595
+ onToolEvent?.(buildToolFailureEvent(execToolName, argsObj, new Error(validation.error)));
1596
+ this.query(`deepclause_mi:post_exec_result('${this.sessionId}', failure, ${JSON.stringify(validation.error)})`);
1597
+ continue;
1598
+ }
1432
1599
  onToolEvent?.(buildToolStartEvent(execToolName, argsObj));
1433
1600
  const execResult = await tool.execute(argsObj, { signal: runContext.signal });
1434
1601
  onToolEvent?.(buildToolCompletionEvent(execToolName, argsObj, execResult));
@@ -1623,19 +1790,24 @@ export class DMLRunner {
1623
1790
  }
1624
1791
  }
1625
1792
  }
1626
- // Build the call stack exclusion list (current tool + previously excluded)
1627
- const nestedExcludedTools = [...excludedTools, toolName];
1628
- // Filter tools based on:
1629
- // 1. Call stack exclusion (prevent recursion)
1630
- // 2. Manual scope (if specified via with_tools/without_tools)
1631
- let filteredTools = userTools.filter(t => !nestedExcludedTools.includes(t.name));
1793
+ // Withhold every tool that is executing at this point, not just this one
1794
+ // and whatever was passed in: a tool reached by a longer chain (tool →
1795
+ // tool → task/N) must not be offered to the agent at the end of it
1796
+ // either. `this.toolCallStack` is the authoritative stack.
1797
+ const nestedExcludedTools = [
1798
+ ...new Set([...excludedTools, ...this.toolCallStack, toolName]),
1799
+ ];
1800
+ // Manual scope (with_tools/without_tools). Call-stack exclusion is
1801
+ // applied inside buildAgentTools, which needs the withheld names so
1802
+ // it can declare them and refuse them with the reason.
1803
+ let scopedTools = userTools;
1632
1804
  if (toolScopeType === 'whitelist') {
1633
1805
  // Only include tools in the whitelist
1634
- filteredTools = filteredTools.filter(t => toolScopeList.includes(t.name));
1806
+ scopedTools = scopedTools.filter(t => toolScopeList.includes(t.name));
1635
1807
  }
1636
1808
  else if (toolScopeType === 'blacklist') {
1637
1809
  // Exclude tools in the blacklist
1638
- filteredTools = filteredTools.filter(t => !toolScopeList.includes(t.name));
1810
+ scopedTools = scopedTools.filter(t => !toolScopeList.includes(t.name));
1639
1811
  }
1640
1812
  // Extract memory
1641
1813
  const memory = this.extractMemoryFromPayload(payload);
@@ -1663,7 +1835,7 @@ export class DMLRunner {
1663
1835
  console.log('[RUNNER] Tool agent outputVars:', outputVars);
1664
1836
  console.log('[RUNNER] Call stack excluded tools:', nestedExcludedTools);
1665
1837
  console.log('[RUNNER] Tool scope:', toolScopeType, toolScopeList);
1666
- console.log('[RUNNER] Available tools for nested agent:', filteredTools.map(t => t.name));
1838
+ console.log('[RUNNER] Available tools for nested agent:', scopedTools.map(t => t.name));
1667
1839
  console.log('[RUNNER] Memory passed to nested agent:', memory.length, 'messages');
1668
1840
  if (memory.length > 0) {
1669
1841
  console.log('[RUNNER] Memory contents:', JSON.stringify(memory, null, 2));
@@ -1673,8 +1845,8 @@ export class DMLRunner {
1673
1845
  const nestedToolCallback = async (nestedToolName, nestedArgs) => {
1674
1846
  return this.executeToolIsolated(nestedToolName, nestedArgs, registeredTools, onOutput, runContext, nestedExcludedTools, onToolCall, onToolEvent);
1675
1847
  };
1676
- const availableTools = this.buildAgentTools(filteredTools, null, // No policy override for nested
1677
- nestedToolCallback);
1848
+ const availableTools = this.buildAgentTools(scopedTools, null, // No policy override for nested
1849
+ nestedToolCallback, nestedExcludedTools);
1678
1850
  const nestedStepId = `step_${this.stepCounter++}`;
1679
1851
  onToolEvent?.({ type: 'task_activity', taskState: 'started', taskDescription, taskId: nestedStepId });
1680
1852
  try {
@@ -1684,6 +1856,9 @@ export class DMLRunner {
1684
1856
  outputVars,
1685
1857
  memory: nestedEffectiveMemory,
1686
1858
  tools: availableTools,
1859
+ // Withheld to break a call cycle; declared to the model so a
1860
+ // call can be answered with the reason rather than "not found".
1861
+ excludedTools: nestedExcludedTools,
1687
1862
  modelOptions: {
1688
1863
  model: this.options.model,
1689
1864
  provider: this.options.provider,