deepclause-sdk 0.1.0 → 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 (142) hide show
  1. package/README.md +83 -12
  2. package/dist/agent.d.ts +14 -1
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +117 -9
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +86 -26
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +15 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.js +38 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/repl.d.ts +27 -0
  13. package/dist/cli/repl.d.ts.map +1 -1
  14. package/dist/cli/repl.js +191 -5
  15. package/dist/cli/repl.js.map +1 -1
  16. package/dist/cli/run.d.ts.map +1 -1
  17. package/dist/cli/run.js +16 -0
  18. package/dist/cli/run.js.map +1 -1
  19. package/dist/cli/visualize.d.ts +4 -0
  20. package/dist/cli/visualize.d.ts.map +1 -1
  21. package/dist/cli/visualize.js +29 -4
  22. package/dist/cli/visualize.js.map +1 -1
  23. package/dist/prolog/bridge.d.ts +3 -1
  24. package/dist/prolog/bridge.d.ts.map +1 -1
  25. package/dist/prolog/bridge.js +2 -0
  26. package/dist/prolog/bridge.js.map +1 -1
  27. package/dist/prolog/loader.d.ts +32 -0
  28. package/dist/prolog/loader.d.ts.map +1 -1
  29. package/dist/prolog/loader.js +87 -0
  30. package/dist/prolog/loader.js.map +1 -1
  31. package/dist/prolog-src/deepclause_mi.pl +1217 -120
  32. package/dist/repl/agent.d.ts +33 -1
  33. package/dist/repl/agent.d.ts.map +1 -1
  34. package/dist/repl/agent.js +91 -8
  35. package/dist/repl/agent.js.map +1 -1
  36. package/dist/repl/backend.d.ts +31 -0
  37. package/dist/repl/backend.d.ts.map +1 -1
  38. package/dist/repl/backend.js +122 -77
  39. package/dist/repl/backend.js.map +1 -1
  40. package/dist/repl/harness-tools.d.ts +49 -2
  41. package/dist/repl/harness-tools.d.ts.map +1 -1
  42. package/dist/repl/harness-tools.js +87 -7
  43. package/dist/repl/harness-tools.js.map +1 -1
  44. package/dist/repl/imagegen.d.ts +29 -0
  45. package/dist/repl/imagegen.d.ts.map +1 -0
  46. package/dist/repl/imagegen.js +84 -0
  47. package/dist/repl/imagegen.js.map +1 -0
  48. package/dist/repl/index.d.ts +20 -2
  49. package/dist/repl/index.d.ts.map +1 -1
  50. package/dist/repl/index.js +6 -0
  51. package/dist/repl/index.js.map +1 -1
  52. package/dist/repl/model-resolver.d.ts +59 -0
  53. package/dist/repl/model-resolver.d.ts.map +1 -0
  54. package/dist/repl/model-resolver.js +156 -0
  55. package/dist/repl/model-resolver.js.map +1 -0
  56. package/dist/repl/render.d.ts +36 -2
  57. package/dist/repl/render.d.ts.map +1 -1
  58. package/dist/repl/render.js +175 -17
  59. package/dist/repl/render.js.map +1 -1
  60. package/dist/repl/session.d.ts +129 -4
  61. package/dist/repl/session.d.ts.map +1 -1
  62. package/dist/repl/session.js +380 -17
  63. package/dist/repl/session.js.map +1 -1
  64. package/dist/repl/tool-arguments.d.ts +36 -0
  65. package/dist/repl/tool-arguments.d.ts.map +1 -0
  66. package/dist/repl/tool-arguments.js +123 -0
  67. package/dist/repl/tool-arguments.js.map +1 -0
  68. package/dist/repl/toolset.d.ts +28 -1
  69. package/dist/repl/toolset.d.ts.map +1 -1
  70. package/dist/repl/toolset.js +52 -4
  71. package/dist/repl/toolset.js.map +1 -1
  72. package/dist/repl/tui/transcript.d.ts +2 -0
  73. package/dist/repl/tui/transcript.d.ts.map +1 -1
  74. package/dist/repl/tui/transcript.js +16 -0
  75. package/dist/repl/tui/transcript.js.map +1 -1
  76. package/dist/runner.d.ts +44 -2
  77. package/dist/runner.d.ts.map +1 -1
  78. package/dist/runner.js +293 -33
  79. package/dist/runner.js.map +1 -1
  80. package/dist/system/assets/docs/AGENT_PROMPT.md +1000 -44
  81. package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
  82. package/dist/system/assets/docs/DML_REFERENCE.md +652 -39
  83. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
  84. package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
  85. package/dist/system/environment.d.ts +36 -0
  86. package/dist/system/environment.d.ts.map +1 -0
  87. package/dist/system/environment.js +81 -0
  88. package/dist/system/environment.js.map +1 -0
  89. package/dist/system/runtime/shell-manager.d.ts +40 -2
  90. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  91. package/dist/system/runtime/shell-manager.js +82 -16
  92. package/dist/system/runtime/shell-manager.js.map +1 -1
  93. package/dist/system/runtime/shell-tool-events.js +25 -1
  94. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  95. package/dist/types.d.ts +76 -2
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/visualize/ascii.d.ts +55 -0
  98. package/dist/visualize/ascii.d.ts.map +1 -0
  99. package/dist/visualize/ascii.js +242 -0
  100. package/dist/visualize/ascii.js.map +1 -0
  101. package/dist/visualize/display.d.ts +24 -0
  102. package/dist/visualize/display.d.ts.map +1 -0
  103. package/dist/visualize/display.js +29 -0
  104. package/dist/visualize/display.js.map +1 -0
  105. package/dist/visualize/graphics.d.ts +35 -0
  106. package/dist/visualize/graphics.d.ts.map +1 -0
  107. package/dist/visualize/graphics.js +96 -0
  108. package/dist/visualize/graphics.js.map +1 -0
  109. package/dist/visualize/image.d.ts +38 -0
  110. package/dist/visualize/image.d.ts.map +1 -0
  111. package/dist/visualize/image.js +95 -0
  112. package/dist/visualize/image.js.map +1 -0
  113. package/dist/visualize/index.d.ts +7 -0
  114. package/dist/visualize/index.d.ts.map +1 -1
  115. package/dist/visualize/index.js +9 -0
  116. package/dist/visualize/index.js.map +1 -1
  117. package/dist/visualize/raster.d.ts +23 -0
  118. package/dist/visualize/raster.d.ts.map +1 -0
  119. package/dist/visualize/raster.js +47 -0
  120. package/dist/visualize/raster.js.map +1 -0
  121. package/dist/visualize/svg.d.ts +39 -0
  122. package/dist/visualize/svg.d.ts.map +1 -0
  123. package/dist/visualize/svg.js +148 -0
  124. package/dist/visualize/svg.js.map +1 -0
  125. package/dist/visualize/transcode.d.ts +13 -0
  126. package/dist/visualize/transcode.d.ts.map +1 -0
  127. package/dist/visualize/transcode.js +40 -0
  128. package/dist/visualize/transcode.js.map +1 -0
  129. package/docs/images/repl.png +0 -0
  130. package/package.json +10 -6
  131. package/src/prolog-src/deepclause_mi.pl +1217 -120
  132. package/dist/compaction.d.ts +0 -98
  133. package/dist/compaction.d.ts.map +0 -1
  134. package/dist/compaction.js +0 -471
  135. package/dist/compaction.js.map +0 -1
  136. package/dist/repl/tools.d.ts +0 -53
  137. package/dist/repl/tools.d.ts.map +0 -1
  138. package/dist/repl/tools.js +0 -307
  139. package/dist/repl/tools.js.map +0 -1
  140. package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
  141. package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
  142. package/dist/system/assets/docs/META_PROMPT.md +0 -68
package/dist/runner.js CHANGED
@@ -3,12 +3,16 @@
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';
9
11
  import { generateLlmReply, sampleSingleToken } from './prolog/bridge.js';
10
12
  import { buildReasoningProviderOptions } from './system/config/model-database.js';
11
13
  import { resolveMediaRefs } from './system/runtime/media.js';
14
+ import { resolveMedia } from './visualize/image.js';
15
+ import { parseModelSpec } from './repl/model-resolver.js';
12
16
  /**
13
17
  * Convert swipl-wasm value to plain JavaScript value
14
18
  * Handles PrologString, PrologAtom, PrologList, etc.
@@ -109,11 +113,66 @@ export function decodeAgentOutputVars(rawOutputVars) {
109
113
  && 'type' in value
110
114
  && typeof value.name === 'string'
111
115
  && typeof value.type === 'string') {
112
- 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;
113
136
  }
114
137
  return String(value);
115
138
  });
116
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
+ }
117
176
  /**
118
177
  * Map Prolog types to JSON Schema types
119
178
  */
@@ -143,6 +202,16 @@ export class DMLRunner {
143
202
  currentMemory = [];
144
203
  stepCounter = 0;
145
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 = [];
146
215
  /**
147
216
  * Runner-owned frame table: FrameId -> { frame, engine }.
148
217
  * The Prolog session owns the authoritative engine handles
@@ -201,6 +270,9 @@ export class DMLRunner {
201
270
  yield { type: 'error', content: engineResult.error };
202
271
  return;
203
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}')`);
204
276
  const frame = this.defaultFrameId ? this.frames.get(this.defaultFrameId)?.frame : undefined;
205
277
  if (!frame) {
206
278
  yield { type: 'error', content: 'Failed to register default query frame' };
@@ -254,6 +326,15 @@ export class DMLRunner {
254
326
  }
255
327
  // Step the frame
256
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
+ }
257
338
  if (process.env.DEBUG_STEPS) {
258
339
  console.log('[MAIN_LOOP] step:', step.status, step.content?.substring(0, 50));
259
340
  }
@@ -266,6 +347,27 @@ export class DMLRunner {
266
347
  yield { type: 'output', content: step.content };
267
348
  }
268
349
  break;
350
+ case 'media': {
351
+ // `output(media(...))` — resolve to a base64 PNG for inline display.
352
+ const payload = (step.payload ?? {});
353
+ const media = {
354
+ path: typeof payload.path === 'string' && payload.path ? payload.path : undefined,
355
+ mimeType: typeof payload.mimeType === 'string' ? payload.mimeType : 'image/png',
356
+ kind: typeof payload.kind === 'string' ? payload.kind : undefined,
357
+ data: typeof payload.data === 'string' && payload.data ? payload.data : undefined,
358
+ };
359
+ const resolved = await resolveMedia(options.workspacePath ?? process.cwd(), media);
360
+ yield {
361
+ type: 'media',
362
+ media: {
363
+ path: media.path,
364
+ mimeType: media.mimeType,
365
+ kind: media.kind,
366
+ data: resolved?.pngBase64,
367
+ },
368
+ };
369
+ break;
370
+ }
269
371
  case 'log':
270
372
  if (step.content) {
271
373
  yield { type: 'log', content: step.content };
@@ -630,14 +732,48 @@ export class DMLRunner {
630
732
  };
631
733
  // Extract memory from payload (now passed via state threading)
632
734
  const preparedMemory = this.extractMemoryFromPayload(rawPayload);
735
+ // A task with a `media(X)` typed output generates an image instead of running
736
+ // a text agent turn; the harness displays it only when it calls output(media(X)).
737
+ const mediaOutputs = outputVars.filter((v) => typeof v !== 'string' && v.type === 'media');
738
+ if (mediaOutputs.length > 0) {
739
+ const imageBackend = this.options.imageBackend;
740
+ if (!imageBackend) {
741
+ yield { type: 'error', content: 'image generation is unavailable: no image backend is configured' };
742
+ this.postAgentResult({ success: false, outputs: [], variables: {}, messages: preparedMemory }, preparedMemory);
743
+ stepSucceeded = false;
744
+ return;
745
+ }
746
+ const refs = [];
747
+ for (const message of preparedMemory) {
748
+ for (const ref of message.media ?? []) {
749
+ refs.push(ref);
750
+ }
751
+ }
752
+ const generated = await imageBackend.generate({
753
+ prompt: taskDescription,
754
+ refs: refs.length > 0 ? refs : undefined,
755
+ signal: options.signal,
756
+ });
757
+ const variables = {};
758
+ for (const output of mediaOutputs) {
759
+ variables[output.name] = generated.path;
760
+ }
761
+ this.postAgentResult({ success: true, outputs: [], variables, messages: preparedMemory }, preparedMemory);
762
+ stepSucceeded = true;
763
+ return;
764
+ }
633
765
  // Extract reasoning effort and recipe context (from with_reasoning/with_recipe scoping)
634
766
  const reasoningEffort = String(toJsValue(rawPayload.reasoningEffort) ?? 'none');
635
767
  const recipeContext = String(toJsValue(rawPayload.recipeContext) ?? '');
768
+ // Per-call model override from with_model/2 (undefined = the session model).
769
+ const scopedModel = await this.resolveScope(rawPayload.modelSpec);
636
770
  // Build provider options — merge reasoning effort if set
637
771
  const baseProviderOptions = this.options.providerOptions ?? {};
638
772
  let effectiveProviderOptions = baseProviderOptions;
639
- if (reasoningEffort !== 'none' && this.options.reasoningType && this.options.reasoningType !== 'none') {
640
- const reasoningOpts = buildReasoningProviderOptions(reasoningEffort, this.options.reasoningType, this.options.reasoningBudgetMap);
773
+ const reasoningType = scopedModel?.reasoningType ?? this.options.reasoningType;
774
+ const reasoningBudgetMap = scopedModel?.reasoningBudgetMap ?? this.options.reasoningBudgetMap;
775
+ if (reasoningEffort !== 'none' && reasoningType && reasoningType !== 'none') {
776
+ const reasoningOpts = buildReasoningProviderOptions(reasoningEffort, reasoningType, reasoningBudgetMap);
641
777
  effectiveProviderOptions = { ...baseProviderOptions, ...reasoningOpts };
642
778
  }
643
779
  // Prepend recipe context as a system message if present
@@ -696,14 +832,32 @@ export class DMLRunner {
696
832
  // Build available tools for the agent: the harness's DML tool/2 predicates
697
833
  // plus registered TS tools (reachable via exec/2).
698
834
  const selfReportingTools = undefined;
699
- 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
+ }
700
854
  return this.executeToolIsolated(toolName, args, augmentedTools, onToolOutput, {
701
855
  workspacePath: options.workspacePath,
702
856
  signal: options.signal,
703
857
  toolPolicy: options.toolPolicy,
704
858
  onModelCall: options.onModelCall,
705
- }, [], emitToolCall, emitToolEvent);
706
- });
859
+ }, withheld, emitToolCall, emitToolEvent);
860
+ }, withheld);
707
861
  // Run agent loop with streaming support
708
862
  const resultPromise = runAgentLoop({
709
863
  taskDescription,
@@ -711,14 +865,16 @@ export class DMLRunner {
711
865
  memory: effectiveMemory,
712
866
  tools: availableTools,
713
867
  workspacePath: options.workspacePath,
868
+ environment: this.options.environment,
714
869
  resolveMedia: (refs) => this.readMediaRefs(options.workspacePath, refs),
715
870
  modelOptions: {
716
- model: this.options.model,
717
- provider: this.options.provider,
718
- temperature: this.options.temperature,
719
- maxOutputTokens: this.options.maxTokens,
871
+ model: scopedModel?.id ?? this.options.model,
872
+ provider: scopedModel?.provider ?? this.options.provider,
873
+ temperature: scopedModel?.temperature ?? this.options.temperature,
874
+ maxOutputTokens: scopedModel?.maxTokens ?? this.options.maxTokens,
720
875
  baseUrl: this.options.baseUrl,
721
876
  providerOptions: effectiveProviderOptions,
877
+ target: scopedModel,
722
878
  },
723
879
  streaming: this.options.streaming,
724
880
  debug: this.options.debug,
@@ -767,7 +923,7 @@ export class DMLRunner {
767
923
  }
768
924
  },
769
925
  onBeforeModelCall: async (messages, lastInputTokens) => {
770
- const hostCompacted = await this.applyHostCompaction(messages, lastInputTokens, emitRunnerEvent);
926
+ const hostCompacted = await this.applyHostCompaction(messages, lastInputTokens, emitRunnerEvent, scopedModel?.contextWindow);
771
927
  return hostCompacted ?? messages;
772
928
  },
773
929
  onAskUser: options.onInputRequired,
@@ -870,15 +1026,17 @@ export class DMLRunner {
870
1026
  yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
871
1027
  let succeeded = false;
872
1028
  try {
1029
+ const scopedModel = await this.resolveScope(data.modelSpec);
873
1030
  const { token, usage } = await sampleSingleToken({
874
1031
  prompt,
875
1032
  allowedTokens,
876
1033
  modelOptions: {
877
- provider: this.options.provider,
878
- model: this.options.model,
879
- temperature: this.options.temperature,
1034
+ provider: scopedModel?.provider ?? this.options.provider,
1035
+ model: scopedModel?.id ?? this.options.model,
1036
+ temperature: scopedModel?.temperature ?? this.options.temperature,
880
1037
  baseUrl: this.options.baseUrl,
881
1038
  providerOptions: this.options.providerOptions,
1039
+ target: scopedModel,
882
1040
  },
883
1041
  signal: options.signal,
884
1042
  llmBackend: this.options.llmBackend,
@@ -909,15 +1067,17 @@ export class DMLRunner {
909
1067
  yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
910
1068
  let succeeded = false;
911
1069
  try {
1070
+ const scopedModel = await this.resolveScope(data.modelSpec);
912
1071
  const { text, usage } = await generateLlmReply({
913
1072
  messages,
914
1073
  modelOptions: {
915
- provider: this.options.provider,
916
- model: this.options.model,
917
- temperature: this.options.temperature,
918
- maxOutputTokens: this.options.maxTokens,
1074
+ provider: scopedModel?.provider ?? this.options.provider,
1075
+ model: scopedModel?.id ?? this.options.model,
1076
+ temperature: scopedModel?.temperature ?? this.options.temperature,
1077
+ maxOutputTokens: scopedModel?.maxTokens ?? this.options.maxTokens,
919
1078
  baseUrl: this.options.baseUrl,
920
1079
  providerOptions: this.options.providerOptions,
1080
+ target: scopedModel,
921
1081
  },
922
1082
  signal: options.signal,
923
1083
  llmBackend: this.options.llmBackend,
@@ -1065,7 +1225,11 @@ export class DMLRunner {
1065
1225
  ? this.internalAskUserTool(options)
1066
1226
  : options.tools.get(toolName);
1067
1227
  if (!tool) {
1068
- 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}`));
1069
1233
  this.postExecResult({ success: false, error: `Tool not found: ${toolName}` });
1070
1234
  return;
1071
1235
  }
@@ -1073,6 +1237,15 @@ export class DMLRunner {
1073
1237
  try {
1074
1238
  // Execute tool
1075
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
+ }
1076
1249
  // Emit tool_call event before execution
1077
1250
  yield buildToolStartEvent(toolName, argsObj);
1078
1251
  const result = await tool.execute(argsObj, { signal: options.signal });
@@ -1247,14 +1420,26 @@ export class DMLRunner {
1247
1420
  * Only includes tools defined in DML - registered TypeScript tools are NOT exposed to the LLM.
1248
1421
  * DML tools can internally call exec/2 to use registered tools.
1249
1422
  */
1250
- buildAgentTools(userTools, _policy, executeToolInline) {
1423
+ buildAgentTools(userTools, _policy, executeToolInline, excludedTools = []) {
1251
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
+ }
1252
1433
  // Add user-defined tools from DML file with schema info
1253
1434
  // These are the ONLY tools the LLM can call during task()
1254
1435
  // Do not apply the host runtime-tool policy to these DML predicates. Their
1255
1436
  // nested exec/2 calls pass through handleExec/executeToolIsolated, where
1256
1437
  // the policy still restricts actual host capabilities.
1257
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;
1258
1443
  // Build JSON Schema properties from inputs
1259
1444
  const properties = {};
1260
1445
  const required = [];
@@ -1277,12 +1462,33 @@ export class DMLRunner {
1277
1462
  required: required.length > 0 ? required : undefined,
1278
1463
  },
1279
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
+ }
1280
1474
  // Execute tool inline via main Prolog engine (shares state with task)
1281
1475
  const { result: toolResult } = await executeToolInline(toolName, args);
1282
1476
  return toolResult;
1283
1477
  },
1284
1478
  });
1285
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
+ }
1286
1492
  // NOTE: Registered TypeScript tools are NOT added here.
1287
1493
  // They are only available via exec/2 in DML code.
1288
1494
  // DML tools can call exec/2 internally to use registered tools.
@@ -1304,6 +1510,20 @@ export class DMLRunner {
1304
1510
  * @param onToolCall - Callback to emit tool call events from nested tasks
1305
1511
  */
1306
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) {
1307
1527
  const outputs = [];
1308
1528
  if (process.env.DEBUG_RUNNER) {
1309
1529
  console.log('[RUNNER] executeToolIsolated called for:', toolName);
@@ -1360,6 +1580,8 @@ export class DMLRunner {
1360
1580
  const tool = registeredTools.get(execToolName);
1361
1581
  if (!tool) {
1362
1582
  // Tool not found - post failure and continue
1583
+ onToolEvent?.(buildToolStartEvent(execToolName, {}));
1584
+ onToolEvent?.(buildToolFailureEvent(execToolName, {}, new Error(`Tool not found: ${execToolName}`)));
1363
1585
  const postGoal = `deepclause_mi:post_exec_result('${this.sessionId}', failure, "Tool not found: ${execToolName}")`;
1364
1586
  this.query(postGoal);
1365
1587
  }
@@ -1367,6 +1589,13 @@ export class DMLRunner {
1367
1589
  let argsObj = null;
1368
1590
  try {
1369
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
+ }
1370
1599
  onToolEvent?.(buildToolStartEvent(execToolName, argsObj));
1371
1600
  const execResult = await tool.execute(argsObj, { signal: runContext.signal });
1372
1601
  onToolEvent?.(buildToolCompletionEvent(execToolName, argsObj, execResult));
@@ -1561,19 +1790,24 @@ export class DMLRunner {
1561
1790
  }
1562
1791
  }
1563
1792
  }
1564
- // Build the call stack exclusion list (current tool + previously excluded)
1565
- const nestedExcludedTools = [...excludedTools, toolName];
1566
- // Filter tools based on:
1567
- // 1. Call stack exclusion (prevent recursion)
1568
- // 2. Manual scope (if specified via with_tools/without_tools)
1569
- 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;
1570
1804
  if (toolScopeType === 'whitelist') {
1571
1805
  // Only include tools in the whitelist
1572
- filteredTools = filteredTools.filter(t => toolScopeList.includes(t.name));
1806
+ scopedTools = scopedTools.filter(t => toolScopeList.includes(t.name));
1573
1807
  }
1574
1808
  else if (toolScopeType === 'blacklist') {
1575
1809
  // Exclude tools in the blacklist
1576
- filteredTools = filteredTools.filter(t => !toolScopeList.includes(t.name));
1810
+ scopedTools = scopedTools.filter(t => !toolScopeList.includes(t.name));
1577
1811
  }
1578
1812
  // Extract memory
1579
1813
  const memory = this.extractMemoryFromPayload(payload);
@@ -1601,7 +1835,7 @@ export class DMLRunner {
1601
1835
  console.log('[RUNNER] Tool agent outputVars:', outputVars);
1602
1836
  console.log('[RUNNER] Call stack excluded tools:', nestedExcludedTools);
1603
1837
  console.log('[RUNNER] Tool scope:', toolScopeType, toolScopeList);
1604
- 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));
1605
1839
  console.log('[RUNNER] Memory passed to nested agent:', memory.length, 'messages');
1606
1840
  if (memory.length > 0) {
1607
1841
  console.log('[RUNNER] Memory contents:', JSON.stringify(memory, null, 2));
@@ -1611,8 +1845,8 @@ export class DMLRunner {
1611
1845
  const nestedToolCallback = async (nestedToolName, nestedArgs) => {
1612
1846
  return this.executeToolIsolated(nestedToolName, nestedArgs, registeredTools, onOutput, runContext, nestedExcludedTools, onToolCall, onToolEvent);
1613
1847
  };
1614
- const availableTools = this.buildAgentTools(filteredTools, null, // No policy override for nested
1615
- nestedToolCallback);
1848
+ const availableTools = this.buildAgentTools(scopedTools, null, // No policy override for nested
1849
+ nestedToolCallback, nestedExcludedTools);
1616
1850
  const nestedStepId = `step_${this.stepCounter++}`;
1617
1851
  onToolEvent?.({ type: 'task_activity', taskState: 'started', taskDescription, taskId: nestedStepId });
1618
1852
  try {
@@ -1622,6 +1856,9 @@ export class DMLRunner {
1622
1856
  outputVars,
1623
1857
  memory: nestedEffectiveMemory,
1624
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,
1625
1862
  modelOptions: {
1626
1863
  model: this.options.model,
1627
1864
  provider: this.options.provider,
@@ -1722,12 +1959,12 @@ export class DMLRunner {
1722
1959
  }
1723
1960
  }
1724
1961
  }
1725
- async applyHostCompaction(messages, lastInputTokens, emit) {
1962
+ async applyHostCompaction(messages, lastInputTokens, emit, contextWindow) {
1726
1963
  const host = this.options.compactMessages;
1727
1964
  if (!host) {
1728
1965
  return null;
1729
1966
  }
1730
- const result = await host(messages, lastInputTokens);
1967
+ const result = await host(messages, lastInputTokens, contextWindow);
1731
1968
  if (result.event) {
1732
1969
  emit?.(result.event);
1733
1970
  }
@@ -1887,6 +2124,29 @@ export class DMLRunner {
1887
2124
  /**
1888
2125
  * Execute a Prolog query
1889
2126
  */
2127
+ scopedModels = new Map();
2128
+ /**
2129
+ * Resolve a `with_model/2` spec to a model profile (cached). Returns undefined
2130
+ * for a `default`/absent scope (use the session model).
2131
+ */
2132
+ async resolveScope(rawSpec) {
2133
+ const spec = parseModelSpec(rawSpec);
2134
+ if (spec === 'default') {
2135
+ return undefined;
2136
+ }
2137
+ const key = JSON.stringify(spec);
2138
+ const cached = this.scopedModels.get(key);
2139
+ if (cached) {
2140
+ return cached;
2141
+ }
2142
+ const resolver = this.options.modelResolver;
2143
+ if (!resolver) {
2144
+ throw new Error('with_model is unavailable: no model resolver is configured');
2145
+ }
2146
+ const resolved = await resolver.resolve(spec);
2147
+ this.scopedModels.set(key, resolved);
2148
+ return resolved;
2149
+ }
1890
2150
  query(goal) {
1891
2151
  try {
1892
2152
  const q = this.swipl.prolog.query(goal);