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.
- package/README.md +83 -12
- package/dist/agent.d.ts +14 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +117 -9
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +86 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +15 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.d.ts +27 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +191 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +16 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/visualize.d.ts +4 -0
- package/dist/cli/visualize.d.ts.map +1 -1
- package/dist/cli/visualize.js +29 -4
- package/dist/cli/visualize.js.map +1 -1
- package/dist/prolog/bridge.d.ts +3 -1
- package/dist/prolog/bridge.d.ts.map +1 -1
- package/dist/prolog/bridge.js +2 -0
- package/dist/prolog/bridge.js.map +1 -1
- package/dist/prolog/loader.d.ts +32 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +87 -0
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +1217 -120
- package/dist/repl/agent.d.ts +33 -1
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +91 -8
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/backend.d.ts +31 -0
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +122 -77
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +49 -2
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +87 -7
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/imagegen.d.ts +29 -0
- package/dist/repl/imagegen.d.ts.map +1 -0
- package/dist/repl/imagegen.js +84 -0
- package/dist/repl/imagegen.js.map +1 -0
- package/dist/repl/index.d.ts +20 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +6 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/model-resolver.d.ts +59 -0
- package/dist/repl/model-resolver.d.ts.map +1 -0
- package/dist/repl/model-resolver.js +156 -0
- package/dist/repl/model-resolver.js.map +1 -0
- package/dist/repl/render.d.ts +36 -2
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +175 -17
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +129 -4
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +380 -17
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tool-arguments.d.ts +36 -0
- package/dist/repl/tool-arguments.d.ts.map +1 -0
- package/dist/repl/tool-arguments.js +123 -0
- package/dist/repl/tool-arguments.js.map +1 -0
- package/dist/repl/toolset.d.ts +28 -1
- package/dist/repl/toolset.d.ts.map +1 -1
- package/dist/repl/toolset.js +52 -4
- package/dist/repl/toolset.js.map +1 -1
- package/dist/repl/tui/transcript.d.ts +2 -0
- package/dist/repl/tui/transcript.d.ts.map +1 -1
- package/dist/repl/tui/transcript.js +16 -0
- package/dist/repl/tui/transcript.js.map +1 -1
- package/dist/runner.d.ts +44 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +293 -33
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +1000 -44
- package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
- package/dist/system/assets/docs/DML_REFERENCE.md +652 -39
- package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
- package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
- package/dist/system/environment.d.ts +36 -0
- package/dist/system/environment.d.ts.map +1 -0
- package/dist/system/environment.js +81 -0
- package/dist/system/environment.js.map +1 -0
- package/dist/system/runtime/shell-manager.d.ts +40 -2
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +82 -16
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/shell-tool-events.js +25 -1
- package/dist/system/runtime/shell-tool-events.js.map +1 -1
- package/dist/types.d.ts +76 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/visualize/ascii.d.ts +55 -0
- package/dist/visualize/ascii.d.ts.map +1 -0
- package/dist/visualize/ascii.js +242 -0
- package/dist/visualize/ascii.js.map +1 -0
- package/dist/visualize/display.d.ts +24 -0
- package/dist/visualize/display.d.ts.map +1 -0
- package/dist/visualize/display.js +29 -0
- package/dist/visualize/display.js.map +1 -0
- package/dist/visualize/graphics.d.ts +35 -0
- package/dist/visualize/graphics.d.ts.map +1 -0
- package/dist/visualize/graphics.js +96 -0
- package/dist/visualize/graphics.js.map +1 -0
- package/dist/visualize/image.d.ts +38 -0
- package/dist/visualize/image.d.ts.map +1 -0
- package/dist/visualize/image.js +95 -0
- package/dist/visualize/image.js.map +1 -0
- package/dist/visualize/index.d.ts +7 -0
- package/dist/visualize/index.d.ts.map +1 -1
- package/dist/visualize/index.js +9 -0
- package/dist/visualize/index.js.map +1 -1
- package/dist/visualize/raster.d.ts +23 -0
- package/dist/visualize/raster.d.ts.map +1 -0
- package/dist/visualize/raster.js +47 -0
- package/dist/visualize/raster.js.map +1 -0
- package/dist/visualize/svg.d.ts +39 -0
- package/dist/visualize/svg.d.ts.map +1 -0
- package/dist/visualize/svg.js +148 -0
- package/dist/visualize/svg.js.map +1 -0
- package/dist/visualize/transcode.d.ts +13 -0
- package/dist/visualize/transcode.d.ts.map +1 -0
- package/dist/visualize/transcode.js +40 -0
- package/dist/visualize/transcode.js.map +1 -0
- package/docs/images/repl.png +0 -0
- package/package.json +10 -6
- package/src/prolog-src/deepclause_mi.pl +1217 -120
- package/dist/compaction.d.ts +0 -98
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js +0 -471
- package/dist/compaction.js.map +0 -1
- package/dist/repl/tools.d.ts +0 -53
- package/dist/repl/tools.d.ts.map +0 -1
- package/dist/repl/tools.js +0 -307
- package/dist/repl/tools.js.map +0 -1
- package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
- package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
- 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
|
-
|
|
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
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
//
|
|
1565
|
-
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:',
|
|
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(
|
|
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);
|