deepclause-sdk 0.0.89 → 0.1.1
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 +481 -941
- package/dist/agent.d.ts +19 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +75 -21
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +385 -178
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +161 -188
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.d.ts +3 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +85 -411
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/login.d.ts +29 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/cli/login.js +184 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/readline.d.ts +6 -0
- package/dist/cli/readline.d.ts.map +1 -0
- package/dist/cli/readline.js +15 -0
- package/dist/cli/readline.js.map +1 -0
- package/dist/cli/repl-bin.d.ts +9 -0
- package/dist/cli/repl-bin.d.ts.map +1 -0
- package/dist/cli/repl-bin.js +16 -0
- package/dist/cli/repl-bin.js.map +1 -0
- package/dist/cli/repl.d.ts +216 -0
- package/dist/cli/repl.d.ts.map +1 -0
- package/dist/cli/repl.js +1392 -0
- package/dist/cli/repl.js.map +1 -0
- package/dist/cli/run.d.ts +17 -18
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +202 -183
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/set-backend.d.ts +53 -0
- package/dist/cli/set-backend.d.ts.map +1 -0
- package/dist/cli/set-backend.js +259 -0
- package/dist/cli/set-backend.js.map +1 -0
- package/dist/cli/visualize.d.ts +42 -0
- package/dist/cli/visualize.d.ts.map +1 -0
- package/dist/cli/visualize.js +150 -0
- package/dist/cli/visualize.js.map +1 -0
- package/dist/compiler.d.ts +5 -52
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +4 -768
- package/dist/compiler.js.map +1 -1
- package/dist/index.d.ts +27 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/prolog/bridge.d.ts +5 -1
- package/dist/prolog/bridge.d.ts.map +1 -1
- package/dist/prolog/bridge.js +10 -1
- package/dist/prolog/bridge.js.map +1 -1
- package/dist/prolog-src/deepclause_analysis.pl +10 -2
- package/dist/prolog-src/deepclause_mi.pl +2440 -49
- package/dist/repl/agent.d.ts +90 -0
- package/dist/repl/agent.d.ts.map +1 -0
- package/dist/repl/agent.js +410 -0
- package/dist/repl/agent.js.map +1 -0
- package/dist/repl/auth.d.ts +45 -0
- package/dist/repl/auth.d.ts.map +1 -0
- package/dist/repl/auth.js +116 -0
- package/dist/repl/auth.js.map +1 -0
- package/dist/repl/backend.d.ts +321 -0
- package/dist/repl/backend.d.ts.map +1 -0
- package/dist/repl/backend.js +415 -0
- package/dist/repl/backend.js.map +1 -0
- package/dist/repl/compaction.d.ts +122 -0
- package/dist/repl/compaction.d.ts.map +1 -0
- package/dist/repl/compaction.js +318 -0
- package/dist/repl/compaction.js.map +1 -0
- package/dist/repl/export.d.ts +30 -0
- package/dist/repl/export.d.ts.map +1 -0
- package/dist/repl/export.js +23 -0
- package/dist/repl/export.js.map +1 -0
- package/dist/repl/frames.d.ts +21 -0
- package/dist/repl/frames.d.ts.map +1 -0
- package/dist/repl/frames.js +79 -0
- package/dist/repl/frames.js.map +1 -0
- package/dist/repl/harness-tools.d.ts +99 -0
- package/dist/repl/harness-tools.d.ts.map +1 -0
- package/dist/repl/harness-tools.js +192 -0
- package/dist/repl/harness-tools.js.map +1 -0
- package/dist/repl/highlight.d.ts +30 -0
- package/dist/repl/highlight.d.ts.map +1 -0
- package/dist/repl/highlight.js +285 -0
- package/dist/repl/highlight.js.map +1 -0
- package/dist/repl/history.d.ts +234 -0
- package/dist/repl/history.d.ts.map +1 -0
- package/dist/repl/history.js +661 -0
- package/dist/repl/history.js.map +1 -0
- 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 +130 -0
- package/dist/repl/index.d.ts.map +1 -0
- package/dist/repl/index.js +112 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/repl/judge.d.ts +51 -0
- package/dist/repl/judge.d.ts.map +1 -0
- package/dist/repl/judge.js +58 -0
- package/dist/repl/judge.js.map +1 -0
- package/dist/repl/markdown.d.ts +25 -0
- package/dist/repl/markdown.d.ts.map +1 -0
- package/dist/repl/markdown.js +103 -0
- package/dist/repl/markdown.js.map +1 -0
- 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/picker.d.ts +34 -0
- package/dist/repl/picker.d.ts.map +1 -0
- package/dist/repl/picker.js +81 -0
- package/dist/repl/picker.js.map +1 -0
- package/dist/repl/project-context.d.ts +16 -0
- package/dist/repl/project-context.d.ts.map +1 -0
- package/dist/repl/project-context.js +23 -0
- package/dist/repl/project-context.js.map +1 -0
- package/dist/repl/render.d.ts +187 -0
- package/dist/repl/render.d.ts.map +1 -0
- package/dist/repl/render.js +808 -0
- package/dist/repl/render.js.map +1 -0
- package/dist/repl/session.d.ts +424 -0
- package/dist/repl/session.d.ts.map +1 -0
- package/dist/repl/session.js +1473 -0
- package/dist/repl/session.js.map +1 -0
- package/dist/repl/status.d.ts +100 -0
- package/dist/repl/status.d.ts.map +1 -0
- package/dist/repl/status.js +218 -0
- package/dist/repl/status.js.map +1 -0
- package/dist/repl/tools.d.ts +53 -0
- package/dist/repl/tools.d.ts.map +1 -0
- package/dist/repl/tools.js +307 -0
- package/dist/repl/tools.js.map +1 -0
- package/dist/repl/toolset.d.ts +32 -0
- package/dist/repl/toolset.d.ts.map +1 -0
- package/dist/repl/toolset.js +82 -0
- package/dist/repl/toolset.js.map +1 -0
- package/dist/repl/tui/editor.d.ts +17 -0
- package/dist/repl/tui/editor.d.ts.map +1 -0
- package/dist/repl/tui/editor.js +39 -0
- package/dist/repl/tui/editor.js.map +1 -0
- package/dist/repl/tui/status-bar.d.ts +26 -0
- package/dist/repl/tui/status-bar.d.ts.map +1 -0
- package/dist/repl/tui/status-bar.js +47 -0
- package/dist/repl/tui/status-bar.js.map +1 -0
- package/dist/repl/tui/theme.d.ts +30 -0
- package/dist/repl/tui/theme.d.ts.map +1 -0
- package/dist/repl/tui/theme.js +81 -0
- package/dist/repl/tui/theme.js.map +1 -0
- package/dist/repl/tui/transcript.d.ts +41 -0
- package/dist/repl/tui/transcript.d.ts.map +1 -0
- package/dist/repl/tui/transcript.js +139 -0
- package/dist/repl/tui/transcript.js.map +1 -0
- package/dist/runner.d.ts +143 -13
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +532 -290
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +0 -32
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +1447 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +195 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +344 -72
- package/dist/system/assets/docs/HARNESS_EXAMPLES.md +172 -0
- package/dist/system/assets/docs/HARNESS_PATTERNS.md +562 -0
- package/dist/system/assets/docs/META_PROMPT.md +68 -0
- package/dist/system/assets/docs/PROLOG_SNIPPETS.md +117 -0
- package/dist/system/assets/index.d.ts +9 -17
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +26 -85
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/runtime/agentvm-manager.d.ts +9 -2
- package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
- package/dist/system/runtime/agentvm-manager.js +70 -44
- package/dist/system/runtime/agentvm-manager.js.map +1 -1
- package/dist/system/runtime/media.d.ts +23 -0
- package/dist/system/runtime/media.d.ts.map +1 -0
- package/dist/system/runtime/media.js +55 -0
- package/dist/system/runtime/media.js.map +1 -0
- package/dist/system/runtime/shell-limits.d.ts +21 -0
- package/dist/system/runtime/shell-limits.d.ts.map +1 -0
- package/dist/system/runtime/shell-limits.js +39 -0
- package/dist/system/runtime/shell-limits.js.map +1 -0
- package/dist/system/runtime/shell-manager.d.ts +8 -3
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +56 -39
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/shell-tool-events.js +5 -0
- package/dist/system/runtime/shell-tool-events.js.map +1 -1
- package/dist/system/runtime/token-usage.d.ts +2 -0
- package/dist/system/runtime/token-usage.d.ts.map +1 -1
- package/dist/system/runtime/token-usage.js +7 -0
- package/dist/system/runtime/token-usage.js.map +1 -1
- package/dist/tools.d.ts +3 -21
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +14 -168
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +136 -119
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +19 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +338 -0
- package/dist/validate.js.map +1 -0
- 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 +59 -0
- package/dist/visualize/index.d.ts.map +1 -0
- package/dist/visualize/index.js +198 -0
- package/dist/visualize/index.js.map +1 -0
- 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 +19 -15
- package/src/prolog-src/deepclause_analysis.pl +10 -2
- package/src/prolog-src/deepclause_mi.pl +2440 -49
- package/dist/cli/commands.d.ts +0 -42
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -246
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/compile.d.ts +0 -99
- package/dist/cli/compile.d.ts.map +0 -1
- package/dist/cli/compile.js +0 -363
- package/dist/cli/compile.js.map +0 -1
- package/dist/cli/default-skills.d.ts +0 -10
- package/dist/cli/default-skills.d.ts.map +0 -1
- package/dist/cli/default-skills.js +0 -206
- package/dist/cli/default-skills.js.map +0 -1
- package/dist/cli/interactive.d.ts +0 -2
- package/dist/cli/interactive.d.ts.map +0 -1
- package/dist/cli/interactive.js +0 -20
- package/dist/cli/interactive.js.map +0 -1
- package/dist/cli/mcp.d.ts +0 -56
- package/dist/cli/mcp.d.ts.map +0 -1
- package/dist/cli/mcp.js +0 -138
- package/dist/cli/mcp.js.map +0 -1
- package/dist/cli/prompt.d.ts +0 -2
- package/dist/cli/prompt.d.ts.map +0 -1
- package/dist/cli/prompt.js +0 -2
- package/dist/cli/prompt.js.map +0 -1
- package/dist/cli/search.d.ts +0 -19
- package/dist/cli/search.d.ts.map +0 -1
- package/dist/cli/search.js +0 -189
- package/dist/cli/search.js.map +0 -1
- package/dist/cli/tool-args.d.ts +0 -2
- package/dist/cli/tool-args.d.ts.map +0 -1
- package/dist/cli/tool-args.js +0 -39
- package/dist/cli/tool-args.js.map +0 -1
- package/dist/cli/tools.d.ts +0 -36
- package/dist/cli/tools.d.ts.map +0 -1
- package/dist/cli/tools.js +0 -229
- package/dist/cli/tools.js.map +0 -1
- package/dist/cli/tui/index.d.ts +0 -22
- package/dist/cli/tui/index.d.ts.map +0 -1
- package/dist/cli/tui/index.js +0 -29
- package/dist/cli/tui/index.js.map +0 -1
- package/dist/cli/tui.d.ts +0 -167
- package/dist/cli/tui.d.ts.map +0 -1
- package/dist/cli/tui.js +0 -5228
- package/dist/cli/tui.js.map +0 -1
- package/dist/compiler_prompt.d.ts +0 -20
- package/dist/compiler_prompt.d.ts.map +0 -1
- package/dist/compiler_prompt.js +0 -870
- package/dist/compiler_prompt.js.map +0 -1
- package/dist/deepclause-cli.skill +0 -0
- package/dist/prolog-src/deepclause_mi.pl.bak +0 -570
- package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +0 -105
- package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +0 -700
- package/dist/system/assets/docs/TUI.md +0 -150
- package/dist/system/assets/recipes/deepclause-coding-workflow/SKILL.md +0 -100
- package/dist/system/assets/skills/conductor.dml +0 -76
- package/dist/system/assets/skills/deep-planner.dml +0 -412
- package/dist/system/assets/skills/plan.dml +0 -429
- package/dist/system/assets/skills/security-planner.dml +0 -337
- package/dist/system/assets/skills/skill-creator.dml +0 -187
- package/dist/system/runtime/catalog-recipes.d.ts +0 -24
- package/dist/system/runtime/catalog-recipes.d.ts.map +0 -1
- package/dist/system/runtime/catalog-recipes.js +0 -273
- package/dist/system/runtime/catalog-recipes.js.map +0 -1
- package/dist/system/runtime/catalog-skills.d.ts +0 -50
- package/dist/system/runtime/catalog-skills.d.ts.map +0 -1
- package/dist/system/runtime/catalog-skills.js +0 -152
- package/dist/system/runtime/catalog-skills.js.map +0 -1
- package/dist/system/runtime/conductor.d.ts +0 -105
- package/dist/system/runtime/conductor.d.ts.map +0 -1
- package/dist/system/runtime/conductor.js +0 -732
- package/dist/system/runtime/conductor.js.map +0 -1
- package/dist/system/runtime/console-capture.d.ts +0 -9
- package/dist/system/runtime/console-capture.d.ts.map +0 -1
- package/dist/system/runtime/console-capture.js +0 -67
- package/dist/system/runtime/console-capture.js.map +0 -1
- package/dist/system/runtime/dml-executor.d.ts +0 -52
- package/dist/system/runtime/dml-executor.d.ts.map +0 -1
- package/dist/system/runtime/dml-executor.js +0 -203
- package/dist/system/runtime/dml-executor.js.map +0 -1
- package/dist/system/runtime/runtime-tools.d.ts +0 -31
- package/dist/system/runtime/runtime-tools.d.ts.map +0 -1
- package/dist/system/runtime/runtime-tools.js +0 -312
- package/dist/system/runtime/runtime-tools.js.map +0 -1
- package/dist/system/runtime/skill-creator.d.ts +0 -38
- package/dist/system/runtime/skill-creator.d.ts.map +0 -1
- package/dist/system/runtime/skill-creator.js +0 -763
- package/dist/system/runtime/skill-creator.js.map +0 -1
- package/src/prolog-src/deepclause_mi.pl.bak +0 -570
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DMLSession — a persistent REPL living module.
|
|
3
|
+
*
|
|
4
|
+
* Owns one Prolog session (the "living module"), one `DMLRunner` bound to it,
|
|
5
|
+
* and the harness memory slot. Goal-mode queries create a `query_start/5`
|
|
6
|
+
* frame, register it with the runner, and drive it with `runFrame`.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
|
|
12
|
+
import { loadProlog, mountWorkspace } from '../prolog/loader.js';
|
|
13
|
+
import { getWorkspaceDocAssetPath } from '../system/assets/index.js';
|
|
14
|
+
import { DMLRunner, toJsValue } from '../runner.js';
|
|
15
|
+
import { compactMemory, estimateMemoryTokens, shouldCompact, toCompactionEvent, } from './compaction.js';
|
|
16
|
+
import { HarnessAgent } from './agent.js';
|
|
17
|
+
import { createHarnessTools } from './harness-tools.js';
|
|
18
|
+
import { buildParamsTerm, plainValue, quotePrologString } from './frames.js';
|
|
19
|
+
import { buildHarnessHeader, } from './export.js';
|
|
20
|
+
import { HistoryStore, harnessHash, } from './history.js';
|
|
21
|
+
import { buildDiagramPrompt, extractMermaid, parseMermaidFlowchart, rasterizeSvg, renderFlowchartSvg, renderMermaidAscii, resolveMedia, validateMermaid, } from '../visualize/index.js';
|
|
22
|
+
import { generateLlmReply } from '../prolog/bridge.js';
|
|
23
|
+
import { formatModelCatalogEntry } from './model-resolver.js';
|
|
24
|
+
let sessionCounter = 0;
|
|
25
|
+
function nextSessionId() {
|
|
26
|
+
sessionCounter += 1;
|
|
27
|
+
return `repl_${Date.now().toString(36)}_${sessionCounter}`;
|
|
28
|
+
}
|
|
29
|
+
export class DMLSession {
|
|
30
|
+
swipl;
|
|
31
|
+
runner;
|
|
32
|
+
sessionId;
|
|
33
|
+
workspacePath;
|
|
34
|
+
gasLimit;
|
|
35
|
+
model;
|
|
36
|
+
provider;
|
|
37
|
+
tools;
|
|
38
|
+
toolPolicy;
|
|
39
|
+
inputHandler;
|
|
40
|
+
maxDepth;
|
|
41
|
+
maxModelCalls;
|
|
42
|
+
hostToolsCatalog;
|
|
43
|
+
/** TypeScript compaction (ported from pi). */
|
|
44
|
+
compactionSettings;
|
|
45
|
+
compactionAuto = false;
|
|
46
|
+
contextWindow;
|
|
47
|
+
llmBackend;
|
|
48
|
+
imageBackend;
|
|
49
|
+
modelResolver;
|
|
50
|
+
temperature = 0.7;
|
|
51
|
+
maxTokens = 65536;
|
|
52
|
+
/** Persisted authoring-agent conversation (agent mode). */
|
|
53
|
+
agentMessages = [];
|
|
54
|
+
harnessAgent;
|
|
55
|
+
/** Open edit transaction (agent mode): predicate list at snapshot. */
|
|
56
|
+
transactionBefore = null;
|
|
57
|
+
transactionDirty = false;
|
|
58
|
+
/** Whether the last agent turn committed an edit (for the history record). */
|
|
59
|
+
lastTurnCommitted = false;
|
|
60
|
+
/** Version recorded by the last commit, when history is enabled. */
|
|
61
|
+
lastCommitVersion;
|
|
62
|
+
/** Current nested frame depth (used for the depth budget). */
|
|
63
|
+
frameDepth = 0;
|
|
64
|
+
/** Model calls made in the current turn (shared across all frames). */
|
|
65
|
+
modelCallsThisTurn = 0;
|
|
66
|
+
/** Current budget; grows by maxModelCalls when the user extends it. */
|
|
67
|
+
modelCallBudget;
|
|
68
|
+
dmlReference;
|
|
69
|
+
dmlAuthoring;
|
|
70
|
+
harnessExamples;
|
|
71
|
+
harnessPatterns;
|
|
72
|
+
prologSnippets;
|
|
73
|
+
disposed = false;
|
|
74
|
+
/** History recorder; present only when the caller enables it. */
|
|
75
|
+
history;
|
|
76
|
+
historyOptions;
|
|
77
|
+
resume;
|
|
78
|
+
/** Harness this session edits. */
|
|
79
|
+
harnessName;
|
|
80
|
+
autoLoadHarness;
|
|
81
|
+
/** Monotonic per-session turn counter used by history records. */
|
|
82
|
+
turnCounter = 0;
|
|
83
|
+
historyWarned = false;
|
|
84
|
+
constructor(swipl, options) {
|
|
85
|
+
this.swipl = swipl;
|
|
86
|
+
this.sessionId = options.sessionId ?? nextSessionId();
|
|
87
|
+
this.workspacePath = options.workspacePath;
|
|
88
|
+
this.gasLimit = options.gasLimit;
|
|
89
|
+
this.model = options.runnerOptions.model;
|
|
90
|
+
this.provider = String(options.runnerOptions.provider);
|
|
91
|
+
this.tools = options.tools ?? new Map();
|
|
92
|
+
this.toolPolicy = options.toolPolicy ?? null;
|
|
93
|
+
this.inputHandler = options.onInput
|
|
94
|
+
?? (options.askUser ? (prompt, choices) => options.askUser(prompt, choices).then((r) => ('cancelled' in r ? undefined : r.answer)) : undefined)
|
|
95
|
+
?? options.onInputRequired
|
|
96
|
+
?? undefined;
|
|
97
|
+
this.maxDepth = options.maxDepth ?? 3;
|
|
98
|
+
this.maxModelCalls = options.maxModelCalls ?? 100;
|
|
99
|
+
this.modelCallBudget = this.maxModelCalls;
|
|
100
|
+
this.hostToolsCatalog = options.hostToolsCatalog;
|
|
101
|
+
this.historyOptions = options.history;
|
|
102
|
+
this.resume = options.resume ?? false;
|
|
103
|
+
this.harnessName = options.harnessName ?? 'main';
|
|
104
|
+
this.autoLoadHarness = options.autoLoadHarness ?? true;
|
|
105
|
+
this.compactionSettings = options.compaction?.settings;
|
|
106
|
+
this.compactionAuto = options.compaction?.auto ?? false;
|
|
107
|
+
this.contextWindow = options.contextWindow;
|
|
108
|
+
this.llmBackend = options.runnerOptions.llmBackend;
|
|
109
|
+
this.imageBackend = options.runnerOptions.imageBackend;
|
|
110
|
+
this.modelResolver = options.runnerOptions.modelResolver;
|
|
111
|
+
this.temperature = options.runnerOptions.temperature;
|
|
112
|
+
this.maxTokens = options.runnerOptions.maxTokens;
|
|
113
|
+
this.runner = new DMLRunner(swipl, {
|
|
114
|
+
...options.runnerOptions,
|
|
115
|
+
compactMessages: options.compaction
|
|
116
|
+
? (messages, lastInputTokens, contextWindow) => this.compactLoopMessages(messages, lastInputTokens, contextWindow)
|
|
117
|
+
: undefined,
|
|
118
|
+
});
|
|
119
|
+
this.runner.bindSession(this.sessionId);
|
|
120
|
+
}
|
|
121
|
+
static async open(options) {
|
|
122
|
+
const swipl = await loadProlog();
|
|
123
|
+
const session = new DMLSession(swipl, options);
|
|
124
|
+
// Mount the workspace so DML file I/O and `[file].` consult resolve under
|
|
125
|
+
// /workspace exactly as they do for runDML.
|
|
126
|
+
if (options.workspacePath) {
|
|
127
|
+
mountWorkspace(swipl, options.workspacePath);
|
|
128
|
+
}
|
|
129
|
+
// Resuming: read the saved session snapshot up-front so the restored
|
|
130
|
+
// conversation memory can seed create_session; the harness itself is
|
|
131
|
+
// loaded once the session exists.
|
|
132
|
+
let snapshot = null;
|
|
133
|
+
if (options.resume) {
|
|
134
|
+
if (!options.history) {
|
|
135
|
+
throw new Error('resume requires history to be enabled');
|
|
136
|
+
}
|
|
137
|
+
const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
|
|
138
|
+
snapshot = await store.readSessionHarness();
|
|
139
|
+
if (!snapshot) {
|
|
140
|
+
throw new Error(`session not found or has no saved harness: ${session.sessionId}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const sessionParams = {
|
|
144
|
+
workspace_path: options.workspacePath ?? './',
|
|
145
|
+
trace: options.trace ?? false,
|
|
146
|
+
...options.params,
|
|
147
|
+
};
|
|
148
|
+
if (options.gasLimit !== undefined) {
|
|
149
|
+
sessionParams.gas_limit = options.gasLimit;
|
|
150
|
+
}
|
|
151
|
+
const harnessMemory = snapshot?.memory?.harness?.trim()
|
|
152
|
+
? snapshot.memory.harness
|
|
153
|
+
: `[${session.serializeMemory(options.initialHarnessMemory ?? [])}]`;
|
|
154
|
+
const metaMemory = snapshot?.memory?.meta?.trim() ? snapshot.memory.meta : '[]';
|
|
155
|
+
const result = session.prologQuery(`deepclause_mi:create_session('${session.sessionId}', ${buildParamsTerm(sessionParams)}, ${harnessMemory}, ${metaMemory})`);
|
|
156
|
+
if (!result) {
|
|
157
|
+
throw new Error('Failed to create REPL session');
|
|
158
|
+
}
|
|
159
|
+
await session.initHistory();
|
|
160
|
+
if (snapshot) {
|
|
161
|
+
if (snapshot.memory?.agent) {
|
|
162
|
+
session.agentMessages = snapshot.memory.agent;
|
|
163
|
+
}
|
|
164
|
+
await session.restoreHarnessFromHistory(snapshot);
|
|
165
|
+
}
|
|
166
|
+
else if (session.autoLoadHarness) {
|
|
167
|
+
// One harness per name: a fresh session starts from the harness's current
|
|
168
|
+
// content file (`harness.dml`), not an empty module.
|
|
169
|
+
const head = await session.history?.readHeadModule();
|
|
170
|
+
if (head?.module.trim()) {
|
|
171
|
+
const loaded = await session.loadCode(head.module);
|
|
172
|
+
if (loaded.error) {
|
|
173
|
+
session.warnHistory(new Error(`could not load harness '${session.harnessName}': ${loaded.error}`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return session;
|
|
178
|
+
}
|
|
179
|
+
getSessionId() {
|
|
180
|
+
return this.sessionId;
|
|
181
|
+
}
|
|
182
|
+
/** Parse and load DML source into the living module. */
|
|
183
|
+
async loadCode(code) {
|
|
184
|
+
if (this.disposed) {
|
|
185
|
+
return { error: 'Session has been disposed' };
|
|
186
|
+
}
|
|
187
|
+
const result = this.prologQuery(`deepclause_mi:load_code('${this.sessionId}', ${quoteString(code)}, Error)`);
|
|
188
|
+
const error = plainValue(result?.Error);
|
|
189
|
+
if (error === undefined || error === null) {
|
|
190
|
+
return { error: 'Failed to load code' };
|
|
191
|
+
}
|
|
192
|
+
return error === 'none' ? {} : { error: String(error) };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load a DML file (or a `.zip` bundle) into the living module.
|
|
196
|
+
*
|
|
197
|
+
* A `.zip` bundle extracts its script files into the workspace and loads
|
|
198
|
+
* `harness.dml`; a single-file `.dml` export materializes any inlined
|
|
199
|
+
* `embedded_file/2` facts back into the workspace.
|
|
200
|
+
*/
|
|
201
|
+
async load(file) {
|
|
202
|
+
const before = this.predicateList();
|
|
203
|
+
// On a fresh harness the loaded file *is* the baseline, not a version.
|
|
204
|
+
const recordLoad = this.history?.hasCommits ?? false;
|
|
205
|
+
const target = path.isAbsolute(file)
|
|
206
|
+
? file
|
|
207
|
+
: path.resolve(this.workspacePath ?? process.cwd(), file);
|
|
208
|
+
try {
|
|
209
|
+
if (target.toLowerCase().endsWith('.zip')) {
|
|
210
|
+
const result = await this.loadBundle(target);
|
|
211
|
+
if (!result.error && recordLoad)
|
|
212
|
+
await this.recordHistoryChange('load', this.turnCounter, before);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
const code = await fs.readFile(target, 'utf8');
|
|
216
|
+
const result = await this.loadCode(code);
|
|
217
|
+
if (!result.error) {
|
|
218
|
+
this.prologQuery(`deepclause_mi:repl_materialize_embedded('${this.sessionId}')`);
|
|
219
|
+
if (recordLoad)
|
|
220
|
+
await this.recordHistoryChange('load', this.turnCounter, before);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
+
return { error: `Failed to read ${file}: ${message}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Establish a fresh harness's baseline from its current module (called after
|
|
231
|
+
* any `-l` files are loaded). A no-op once the harness has a baseline.
|
|
232
|
+
*/
|
|
233
|
+
async finalizeHarnessBaseline() {
|
|
234
|
+
if (!this.history)
|
|
235
|
+
return;
|
|
236
|
+
await this.history.setInitialBaseline(this.moduleSource());
|
|
237
|
+
}
|
|
238
|
+
/** Workspace-relative files linked by `script_file/2` facts. */
|
|
239
|
+
scriptFiles() {
|
|
240
|
+
const result = this.prologQuery(`deepclause_mi:repl_script_files('${this.sessionId}', Paths)`);
|
|
241
|
+
const paths = plainValue(result?.Paths);
|
|
242
|
+
if (!Array.isArray(paths))
|
|
243
|
+
return [];
|
|
244
|
+
return paths.map((value) => String(plainValue(value) ?? '')).filter(Boolean);
|
|
245
|
+
}
|
|
246
|
+
// ---- History (docs/DML_REPL_SESSION_HISTORY.md) ----
|
|
247
|
+
async initHistory() {
|
|
248
|
+
if (!this.historyOptions)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
const store = new HistoryStore(this.workspacePath ?? process.cwd(), this.sessionId, this.historyOptions, this.harnessName);
|
|
252
|
+
await store.init();
|
|
253
|
+
const existing = await store.readFacts();
|
|
254
|
+
if (existing.trim()) {
|
|
255
|
+
this.prologQuery(`deepclause_mi:repl_history_load('${this.sessionId}', ${quoteString(existing)})`);
|
|
256
|
+
}
|
|
257
|
+
this.history = store;
|
|
258
|
+
this.assertHistoryFacts(await store.startSession({ model: this.model, backend: this.provider }, { resume: this.resume }));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.warnHistory(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Restore a resumed session's harness from its durable snapshot and continue
|
|
266
|
+
* its turn numbering. The saved module is a restore, not a user load, so it
|
|
267
|
+
* is not recorded as a new change.
|
|
268
|
+
*/
|
|
269
|
+
async restoreHarnessFromHistory(snapshot) {
|
|
270
|
+
this.turnCounter = (await this.history?.lastTurn(this.sessionId)) ?? 0;
|
|
271
|
+
const module = snapshot.module.trim();
|
|
272
|
+
if (module) {
|
|
273
|
+
const result = await this.loadCode(snapshot.module);
|
|
274
|
+
if (result.error) {
|
|
275
|
+
this.warnHistory(new Error(`could not restore harness: ${result.error}`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Consistency check: the saved hash covers the module *and* its scripts;
|
|
279
|
+
// scripts live in the workspace and may have been edited since.
|
|
280
|
+
const hash = harnessHash(snapshot.module, await this.scriptHashes());
|
|
281
|
+
if (snapshot.hash && hash !== snapshot.hash) {
|
|
282
|
+
this.warnHistory(new Error('harness or its scripts differ from the saved session state'));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Write the durable harness snapshot beside the session so `--session` can
|
|
287
|
+
* restore it even after the process exits. Captures the *current* module
|
|
288
|
+
* (including goal-mode drift) plus the serialized conversation memory.
|
|
289
|
+
*/
|
|
290
|
+
async persistSessionHarness(version, clean) {
|
|
291
|
+
if (!this.history)
|
|
292
|
+
return;
|
|
293
|
+
try {
|
|
294
|
+
const module = this.moduleSource();
|
|
295
|
+
const scripts = await this.scriptHashes();
|
|
296
|
+
const hash = harnessHash(module, scripts);
|
|
297
|
+
const isClean = clean
|
|
298
|
+
?? (this.history.currentHash ? hash === this.history.currentHash : version === 0);
|
|
299
|
+
await this.history.writeSessionHarness({
|
|
300
|
+
sessionId: this.sessionId,
|
|
301
|
+
version,
|
|
302
|
+
hash,
|
|
303
|
+
clean: isClean,
|
|
304
|
+
savedAt: new Date().toISOString(),
|
|
305
|
+
scripts,
|
|
306
|
+
memory: {
|
|
307
|
+
harness: this.dumpMemoryTerm('harness'),
|
|
308
|
+
meta: this.dumpMemoryTerm('meta'),
|
|
309
|
+
...(this.agentMessages.length > 0 ? { agent: this.agentMessages } : {}),
|
|
310
|
+
},
|
|
311
|
+
module,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
this.warnHistory(error);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Serialize a live memory slot back to a Prolog term (for resume). */
|
|
319
|
+
dumpMemoryTerm(slot) {
|
|
320
|
+
const result = this.prologQuery(`deepclause_mi:repl_memory_term('${this.sessionId}', ${slot}, Text)`);
|
|
321
|
+
const text = plainValue(result?.Text);
|
|
322
|
+
return typeof text === 'string' && text.trim() ? text : '[]';
|
|
323
|
+
}
|
|
324
|
+
assertHistoryFacts(facts) {
|
|
325
|
+
if (!this.history || facts.length === 0)
|
|
326
|
+
return;
|
|
327
|
+
try {
|
|
328
|
+
this.prologQuery(`deepclause_mi:repl_history_record('${this.sessionId}', ${quoteString(facts.join('\n'))})`);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
this.warnHistory(error);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
recordHistoryEvent(turn, event) {
|
|
335
|
+
this.history?.recordEvent({ turn, frame: event.frameKind }, event);
|
|
336
|
+
}
|
|
337
|
+
manifestText() {
|
|
338
|
+
const result = this.prologQuery(`deepclause_mi:repl_manifest('${this.sessionId}', Text)`);
|
|
339
|
+
return String(plainValue(result?.Text) ?? '');
|
|
340
|
+
}
|
|
341
|
+
predicateList() {
|
|
342
|
+
const result = this.prologQuery(`deepclause_mi:repl_user_predicates_text('${this.sessionId}', Text)`);
|
|
343
|
+
return String(plainValue(result?.Text) ?? '')
|
|
344
|
+
.split(',')
|
|
345
|
+
.map((entry) => entry.trim())
|
|
346
|
+
.filter(Boolean);
|
|
347
|
+
}
|
|
348
|
+
moduleSource() {
|
|
349
|
+
const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', none, Text)`);
|
|
350
|
+
return String(plainValue(result?.Text) ?? '');
|
|
351
|
+
}
|
|
352
|
+
async scriptHashes() {
|
|
353
|
+
const { createHash } = await import('crypto');
|
|
354
|
+
const out = [];
|
|
355
|
+
for (const rel of this.scriptFiles()) {
|
|
356
|
+
try {
|
|
357
|
+
const content = await fs.readFile(this.resolveWorkspacePath(rel));
|
|
358
|
+
out.push({ path: rel, sha256: createHash('sha256').update(content).digest('hex') });
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
out.push({ path: rel, sha256: 'missing' });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
diffPredicates(before, after, touched) {
|
|
367
|
+
const beforeSet = new Set(before);
|
|
368
|
+
const afterSet = new Set(after);
|
|
369
|
+
const byPredicate = new Map();
|
|
370
|
+
for (const predicate of touched) {
|
|
371
|
+
if (!afterSet.has(predicate))
|
|
372
|
+
continue;
|
|
373
|
+
byPredicate.set(predicate, { predicate, action: beforeSet.has(predicate) ? 'replaced' : 'added' });
|
|
374
|
+
}
|
|
375
|
+
for (const predicate of before) {
|
|
376
|
+
if (!afterSet.has(predicate))
|
|
377
|
+
byPredicate.set(predicate, { predicate, action: 'removed' });
|
|
378
|
+
}
|
|
379
|
+
return [...byPredicate.values()].sort((a, b) => a.predicate.localeCompare(b.predicate));
|
|
380
|
+
}
|
|
381
|
+
touchedPredicates() {
|
|
382
|
+
return this.lastCommitPredicates().split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
383
|
+
}
|
|
384
|
+
/** Record a harness version; returns the new version number. */
|
|
385
|
+
async recordHistoryChange(kind, turn, before, overrides = {}) {
|
|
386
|
+
if (!this.history)
|
|
387
|
+
return undefined;
|
|
388
|
+
try {
|
|
389
|
+
const after = this.predicateList();
|
|
390
|
+
const touched = overrides.touched ?? (kind === 'commit' ? this.touchedPredicates() : after);
|
|
391
|
+
const changed = this.diffPredicates(before, after, touched);
|
|
392
|
+
const { facts, version } = await this.history.recordChange({
|
|
393
|
+
kind,
|
|
394
|
+
sessionId: this.sessionId,
|
|
395
|
+
turn,
|
|
396
|
+
changed,
|
|
397
|
+
scripts: await this.scriptHashes(),
|
|
398
|
+
module: this.moduleSource(),
|
|
399
|
+
manifest: this.manifestText(),
|
|
400
|
+
});
|
|
401
|
+
this.assertHistoryFacts(facts);
|
|
402
|
+
await this.persistSessionHarness(version, true);
|
|
403
|
+
return version;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
this.warnHistory(error);
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async finishHistoryTurn(record) {
|
|
411
|
+
if (!this.history)
|
|
412
|
+
return;
|
|
413
|
+
try {
|
|
414
|
+
this.assertHistoryFacts(await this.history.recordTurn(record));
|
|
415
|
+
await this.history.flushEvents();
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.warnHistory(error);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
warnHistory(error) {
|
|
422
|
+
if (this.historyWarned)
|
|
423
|
+
return;
|
|
424
|
+
this.historyWarned = true;
|
|
425
|
+
if (process.env.DEBUG_REPL) {
|
|
426
|
+
console.error('[repl] history recording failed:', error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** Extract a `.zip` bundle and load its harness.dml. */
|
|
430
|
+
async loadBundle(zipPath) {
|
|
431
|
+
const data = new Uint8Array(await fs.readFile(zipPath));
|
|
432
|
+
const entries = unzipSync(data);
|
|
433
|
+
const harnessName = Object.keys(entries).find((name) => name === 'harness.dml' || name.endsWith('/harness.dml'));
|
|
434
|
+
if (!harnessName) {
|
|
435
|
+
return { error: 'bundle does not contain harness.dml' };
|
|
436
|
+
}
|
|
437
|
+
const files = [];
|
|
438
|
+
for (const [name, bytes] of Object.entries(entries)) {
|
|
439
|
+
if (name === harnessName || name === 'manifest.json')
|
|
440
|
+
continue;
|
|
441
|
+
const rel = name.replace(/^\.\//, '');
|
|
442
|
+
if (path.isAbsolute(rel) || rel.split('/').includes('..')) {
|
|
443
|
+
return { error: `unsafe bundle entry: ${name}` };
|
|
444
|
+
}
|
|
445
|
+
this.resolveWorkspacePath(rel);
|
|
446
|
+
files.push({ rel, bytes });
|
|
447
|
+
}
|
|
448
|
+
for (const file of files) {
|
|
449
|
+
const abs = this.resolveWorkspacePath(file.rel);
|
|
450
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
451
|
+
await fs.writeFile(abs, Buffer.from(file.bytes));
|
|
452
|
+
}
|
|
453
|
+
return this.loadCode(strFromU8(entries[harnessName]));
|
|
454
|
+
}
|
|
455
|
+
/** Read-only listing of the living module (`all`, `Name/Arity`, or `tools`). */
|
|
456
|
+
async listing(filter = 'all') {
|
|
457
|
+
const filterTerm = filter === 'all' || filter === 'tools'
|
|
458
|
+
? filter
|
|
459
|
+
: toListingFilter(filter);
|
|
460
|
+
const result = this.prologQuery(`deepclause_mi:repl_listing('${this.sessionId}', ${filterTerm}, Text)`);
|
|
461
|
+
const text = plainValue(result?.Text);
|
|
462
|
+
return typeof text === 'string' ? text : '';
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Run a goal and enumerate its solutions.
|
|
466
|
+
*
|
|
467
|
+
* Yields `solution` events (one per answer), interleaved with `output`/`log`/
|
|
468
|
+
* `answer` events from the goal, and finally `finished` or `error`.
|
|
469
|
+
*/
|
|
470
|
+
async *query(goal, options = {}) {
|
|
471
|
+
if (this.disposed) {
|
|
472
|
+
yield { type: 'error', content: 'Session has been disposed' };
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.modelCallsThisTurn = 0;
|
|
476
|
+
this.modelCallBudget = this.maxModelCalls;
|
|
477
|
+
const frameKind = options.frameKind ?? 'object';
|
|
478
|
+
const start = this.prologQuery(`deepclause_mi:repl_initial_state('${this.sessionId}', ${frameKind}, State), `
|
|
479
|
+
+ `deepclause_mi:query_start('${this.sessionId}', ${quoteString(goal)}, ${frameKind}, State, Error)`);
|
|
480
|
+
const error = plainValue(start?.Error);
|
|
481
|
+
if (error === undefined || error === null) {
|
|
482
|
+
yield { type: 'error', content: 'Failed to start query' };
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (error !== 'none') {
|
|
486
|
+
yield { type: 'error', content: String(error) };
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.turnCounter += 1;
|
|
490
|
+
const historyTurn = this.turnCounter;
|
|
491
|
+
const historyStart = Date.now();
|
|
492
|
+
const frame = this.runner.registerDefaultFrame(frameKind);
|
|
493
|
+
if (!frame) {
|
|
494
|
+
yield { type: 'error', content: 'Failed to create query frame' };
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
let historyOutcome = 'query';
|
|
498
|
+
try {
|
|
499
|
+
for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions(options))) {
|
|
500
|
+
this.recordHistoryEvent(historyTurn, event);
|
|
501
|
+
if (event.type === 'error')
|
|
502
|
+
historyOutcome = 'failed';
|
|
503
|
+
yield event;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
this.runner.endFrame(frame.id);
|
|
508
|
+
}
|
|
509
|
+
// An aborted goal may have asserted scratch state; preserve it.
|
|
510
|
+
if (options.signal?.aborted) {
|
|
511
|
+
historyOutcome = 'aborted';
|
|
512
|
+
const saved = await this.saveCurrentHarness('cancelled');
|
|
513
|
+
yield {
|
|
514
|
+
type: 'error',
|
|
515
|
+
content: saved ? `turn aborted; current harness saved to ${saved}` : 'turn aborted',
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
await this.finishHistoryTurn({
|
|
519
|
+
turn: historyTurn,
|
|
520
|
+
mode: 'goal',
|
|
521
|
+
request: goal,
|
|
522
|
+
outcome: historyOutcome,
|
|
523
|
+
durationMs: Date.now() - historyStart,
|
|
524
|
+
modelCalls: this.modelCallsThisTurn,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/** Load the bundled DML reference once, for inclusion in the meta prompt. */
|
|
528
|
+
async loadDmlReference() {
|
|
529
|
+
if (this.dmlReference !== undefined) {
|
|
530
|
+
return this.dmlReference;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
this.dmlReference = await fs.readFile(getWorkspaceDocAssetPath('dml-reference'), 'utf8');
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
this.dmlReference = '';
|
|
537
|
+
}
|
|
538
|
+
return this.dmlReference;
|
|
539
|
+
}
|
|
540
|
+
/** Load the curated authoring patterns once, for the meta prompt. */
|
|
541
|
+
async loadDmlAuthoring() {
|
|
542
|
+
if (this.dmlAuthoring !== undefined) {
|
|
543
|
+
return this.dmlAuthoring;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
this.dmlAuthoring = await fs.readFile(getWorkspaceDocAssetPath('dml-authoring'), 'utf8');
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
this.dmlAuthoring = '';
|
|
550
|
+
}
|
|
551
|
+
return this.dmlAuthoring;
|
|
552
|
+
}
|
|
553
|
+
/** Load a bundled doc asset once (cached by field). */
|
|
554
|
+
async loadDocAsset(name) {
|
|
555
|
+
const cached = name === 'harness-examples'
|
|
556
|
+
? this.harnessExamples
|
|
557
|
+
: name === 'harness-patterns'
|
|
558
|
+
? this.harnessPatterns
|
|
559
|
+
: this.prologSnippets;
|
|
560
|
+
if (cached !== undefined)
|
|
561
|
+
return cached;
|
|
562
|
+
let text = '';
|
|
563
|
+
try {
|
|
564
|
+
text = await fs.readFile(getWorkspaceDocAssetPath(name), 'utf8');
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
text = '';
|
|
568
|
+
}
|
|
569
|
+
if (name === 'harness-examples')
|
|
570
|
+
this.harnessExamples = text;
|
|
571
|
+
else if (name === 'harness-patterns')
|
|
572
|
+
this.harnessPatterns = text;
|
|
573
|
+
else
|
|
574
|
+
this.prologSnippets = text;
|
|
575
|
+
return text;
|
|
576
|
+
}
|
|
577
|
+
/** Predicates replaced by the most recent successful commit ("name/arity, …"). */
|
|
578
|
+
lastCommitPredicates() {
|
|
579
|
+
const result = this.prologQuery(`deepclause_mi:repl_last_commit_summary('${this.sessionId}', Text)`);
|
|
580
|
+
return String(plainValue(result?.Text) ?? '');
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Serialize the *current* living module (including any scratch edits) to a
|
|
584
|
+
* temp .dml file. Used as an abort safety net so a cancelled turn's work is
|
|
585
|
+
* not lost.
|
|
586
|
+
*/
|
|
587
|
+
/** Sorted arities of a user predicate in the living module (e.g. `agent_main`). */
|
|
588
|
+
predicateArities(name) {
|
|
589
|
+
const safe = /^[a-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
|
|
590
|
+
const result = this.prologQuery(`deepclause_mi:repl_predicate_arities('${this.sessionId}', ${safe}, Arities)`);
|
|
591
|
+
const arities = plainValue(result?.Arities);
|
|
592
|
+
if (!Array.isArray(arities))
|
|
593
|
+
return [];
|
|
594
|
+
return arities
|
|
595
|
+
.map((arity) => Number(plainValue(arity)))
|
|
596
|
+
.filter((arity) => Number.isFinite(arity));
|
|
597
|
+
}
|
|
598
|
+
/** True when the living module has no user predicates (nothing worth saving). */
|
|
599
|
+
isEmpty() {
|
|
600
|
+
const result = this.prologQuery(`deepclause_mi:repl_user_predicates_text('${this.sessionId}', Text)`);
|
|
601
|
+
return String(plainValue(result?.Text) ?? '').trim() === '';
|
|
602
|
+
}
|
|
603
|
+
/** Number of user predicates (`Name/Arity`) currently in the harness. */
|
|
604
|
+
predicateCount() {
|
|
605
|
+
return this.predicateList().length;
|
|
606
|
+
}
|
|
607
|
+
async saveCurrentHarness(reason, header = `aborted turn: ${reason}`) {
|
|
608
|
+
const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', ${quoteString(header)}, Text)`);
|
|
609
|
+
const text = String(plainValue(result?.Text) ?? '');
|
|
610
|
+
if (!text.trim()) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
// A bundle (module + current scripts) so an aborted
|
|
614
|
+
// turn's work is fully recoverable, not just the module source.
|
|
615
|
+
const file = path.join(os.tmpdir(), `deepclause-repl-${this.sessionId}-${Date.now()}.zip`);
|
|
616
|
+
await this.writeBundle(file, text, this.scriptFiles(), header);
|
|
617
|
+
return file;
|
|
618
|
+
}
|
|
619
|
+
/** Clear the living module and memories (the `reset.` command). */
|
|
620
|
+
async reset() {
|
|
621
|
+
if (this.isFrozen()) {
|
|
622
|
+
throw new Error('harness is frozen; reset is disabled');
|
|
623
|
+
}
|
|
624
|
+
const before = this.predicateList();
|
|
625
|
+
this.prologQuery(`deepclause_mi:clear_session('${this.sessionId}')`);
|
|
626
|
+
this.runner.clearFrames();
|
|
627
|
+
await this.recordHistoryChange('reset', this.turnCounter, before);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Undo the most recent harness transaction by restoring the previous history
|
|
631
|
+
* version. No-op when there is nothing to undo; refused when frozen. The
|
|
632
|
+
* rollback itself is recorded as a change (harness_rollback/5).
|
|
633
|
+
*/
|
|
634
|
+
async rollback() {
|
|
635
|
+
if (this.disposed)
|
|
636
|
+
return { ok: false, error: 'Session has been disposed' };
|
|
637
|
+
if (!this.history)
|
|
638
|
+
return { ok: false, error: 'history is not enabled; rollback is unavailable' };
|
|
639
|
+
if (this.isFrozen())
|
|
640
|
+
return { ok: false, error: 'harness is frozen; rollback is disabled' };
|
|
641
|
+
const plan = await this.history.prepareRollback();
|
|
642
|
+
if (!plan)
|
|
643
|
+
return { ok: false, error: 'no harness changes to roll back' };
|
|
644
|
+
const result = this.prologQuery(`deepclause_mi:repl_rollback('${this.sessionId}', ${quoteString(plan.module)}, Result)`);
|
|
645
|
+
const outcome = plainValue(result?.Result);
|
|
646
|
+
if (outcome !== 'ok') {
|
|
647
|
+
return { ok: false, error: String(outcome ?? 'rollback failed') };
|
|
648
|
+
}
|
|
649
|
+
this.assertHistoryFacts(await this.history.recordRollback(plan.from, plan.to, this.turnCounter, this.sessionId, plan.module));
|
|
650
|
+
return { ok: true, from: plan.from, to: plan.to };
|
|
651
|
+
}
|
|
652
|
+
// ---- Freeze lock (M2) ----
|
|
653
|
+
/** Make the living module read-only (assert/retract/load/commit refused). */
|
|
654
|
+
async freeze() {
|
|
655
|
+
this.prologQuery(`deepclause_mi:repl_freeze('${this.sessionId}')`);
|
|
656
|
+
}
|
|
657
|
+
/** Like freeze(), but `unfreeze.` is refused for the rest of the process. */
|
|
658
|
+
async lock() {
|
|
659
|
+
this.prologQuery(`deepclause_mi:repl_lock('${this.sessionId}')`);
|
|
660
|
+
}
|
|
661
|
+
/** Clear the lock list. Throws when the harness is irreversibly locked. */
|
|
662
|
+
async unfreeze() {
|
|
663
|
+
const result = this.prologQuery(`deepclause_mi:repl_try_unfreeze('${this.sessionId}', Error)`);
|
|
664
|
+
const error = plainValue(result?.Error);
|
|
665
|
+
if (error !== undefined && error !== null && error !== 'none') {
|
|
666
|
+
throw new Error(String(error));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
isFrozen() {
|
|
670
|
+
const result = this.prologQuery(`deepclause_mi:repl_is_frozen('${this.sessionId}')`);
|
|
671
|
+
return result?.success === true;
|
|
672
|
+
}
|
|
673
|
+
// ---- Export (M2) ----
|
|
674
|
+
/**
|
|
675
|
+
* Serialize the living module.
|
|
676
|
+
*
|
|
677
|
+
* `.zip` produces the self-contained bundle (harness.dml + linked script
|
|
678
|
+
* files + manifest.json). Any other extension produces a single `.dml` with
|
|
679
|
+
* the linked scripts auto-inlined as `embedded_file/2` facts.
|
|
680
|
+
*/
|
|
681
|
+
async export(file, options = {}) {
|
|
682
|
+
const header = options.header ?? buildHarnessHeader({
|
|
683
|
+
timestamp: new Date().toISOString(),
|
|
684
|
+
backend: options.skill ? 'skill' : this.provider,
|
|
685
|
+
model: this.model,
|
|
686
|
+
});
|
|
687
|
+
const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', ${quoteString(header)}, Text)`);
|
|
688
|
+
const moduleText = String(plainValue(result?.Text) ?? '');
|
|
689
|
+
const target = path.resolve(this.workspacePath ?? process.cwd(), file ?? options.path ?? 'harness.dml');
|
|
690
|
+
const scripts = this.scriptFiles();
|
|
691
|
+
if (target.toLowerCase().endsWith('.zip')) {
|
|
692
|
+
await this.writeBundle(target, moduleText, scripts, header);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
await fs.writeFile(target, await this.inlineScripts(moduleText, scripts), 'utf8');
|
|
696
|
+
}
|
|
697
|
+
const stats = this.prologQuery(`deepclause_mi:export_stats('${this.sessionId}', Clauses, Tools, Scripts)`);
|
|
698
|
+
return {
|
|
699
|
+
path: target,
|
|
700
|
+
clauses: Number(plainValue(stats?.Clauses) ?? 0),
|
|
701
|
+
tools: Number(plainValue(stats?.Tools) ?? 0),
|
|
702
|
+
scripts: Number(plainValue(stats?.Scripts) ?? 0),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/** Inline linked script files as `embedded_file/2` facts on a single .dml. */
|
|
706
|
+
async inlineScripts(moduleText, scripts) {
|
|
707
|
+
const facts = [];
|
|
708
|
+
for (const rel of [...scripts].sort()) {
|
|
709
|
+
const content = await this.readWorkspaceFile(rel);
|
|
710
|
+
if (content === null)
|
|
711
|
+
continue;
|
|
712
|
+
facts.push(`embedded_file(${quotePrologString(rel)}, ${quotePrologString(content)}).`);
|
|
713
|
+
}
|
|
714
|
+
if (facts.length === 0)
|
|
715
|
+
return moduleText;
|
|
716
|
+
return `% inlined script files (materialized on load)\n${facts.join('\n')}\n\n${moduleText}`;
|
|
717
|
+
}
|
|
718
|
+
/** Write a `.zip` bundle: harness.dml + linked files + manifest.json. */
|
|
719
|
+
async writeBundle(target, moduleText, scripts, header) {
|
|
720
|
+
const entries = { 'harness.dml': strToU8(moduleText) };
|
|
721
|
+
const manifestFiles = [];
|
|
722
|
+
for (const rel of [...scripts].sort()) {
|
|
723
|
+
const content = await this.readWorkspaceFile(rel);
|
|
724
|
+
if (content === null)
|
|
725
|
+
continue;
|
|
726
|
+
const bytes = strToU8(content);
|
|
727
|
+
entries[rel] = bytes;
|
|
728
|
+
manifestFiles.push({ path: rel, bytes: bytes.length });
|
|
729
|
+
}
|
|
730
|
+
entries['manifest.json'] = strToU8(`${JSON.stringify({
|
|
731
|
+
version: 1,
|
|
732
|
+
header,
|
|
733
|
+
model: this.model,
|
|
734
|
+
provider: this.provider,
|
|
735
|
+
timestamp: new Date().toISOString(),
|
|
736
|
+
files: manifestFiles,
|
|
737
|
+
}, null, 2)}\n`);
|
|
738
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
739
|
+
await fs.writeFile(target, Buffer.from(zipSync(entries, { level: 6 })));
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Run a goal in an isolated `execute` child frame and collect its result.
|
|
743
|
+
* The child frame uses the living scratch module and may itself call task()
|
|
744
|
+
* (RLM-style recursion); its harness-memory writes are discarded afterward.
|
|
745
|
+
*/
|
|
746
|
+
async executeGoal(goal, limit = 5, onEvent, signal) {
|
|
747
|
+
const startedAt = Date.now();
|
|
748
|
+
const elapsed = () => Date.now() - startedAt;
|
|
749
|
+
const childDepth = this.frameDepth + 1;
|
|
750
|
+
if (childDepth > this.maxDepth) {
|
|
751
|
+
return {
|
|
752
|
+
bindings: [],
|
|
753
|
+
solutions: [],
|
|
754
|
+
output: '',
|
|
755
|
+
error: `maximum frame depth (${this.maxDepth}) exceeded`,
|
|
756
|
+
elapsedMs: elapsed(),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const start = this.prologQuery(`deepclause_mi:repl_execute_frame('${this.sessionId}', ${quoteString(goal)}, FrameId, Error)`);
|
|
760
|
+
const error = plainValue(start?.Error);
|
|
761
|
+
if (error === undefined || error === null) {
|
|
762
|
+
return { bindings: [], solutions: [], output: '', error: 'Failed to start execute frame', elapsedMs: elapsed() };
|
|
763
|
+
}
|
|
764
|
+
if (error !== 'none') {
|
|
765
|
+
return { bindings: [], solutions: [], output: '', error: String(error), elapsedMs: elapsed() };
|
|
766
|
+
}
|
|
767
|
+
const frameId = String(plainValue(start?.FrameId));
|
|
768
|
+
const frame = this.runner.registerFrame(frameId, 'nested', 1);
|
|
769
|
+
// `execute` is a scratch test: capture harness memory and restore it after,
|
|
770
|
+
// so a goal's `system/1`/`user/1` writes do not leak into the live harness.
|
|
771
|
+
const memoryBefore = this.getMemory('harness');
|
|
772
|
+
const solutions = [];
|
|
773
|
+
let output = '';
|
|
774
|
+
let runError;
|
|
775
|
+
this.frameDepth = childDepth;
|
|
776
|
+
try {
|
|
777
|
+
for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal }))) {
|
|
778
|
+
onEvent?.(event);
|
|
779
|
+
if (event.type === 'solution') {
|
|
780
|
+
if (solutions.length < limit) {
|
|
781
|
+
solutions.push(event.solution?.bindings ?? []);
|
|
782
|
+
}
|
|
783
|
+
// Stop enumerating once the limit is reached: a nondeterministic goal
|
|
784
|
+
// (e.g. an unlabelled CLP model) can otherwise produce unbounded
|
|
785
|
+
// solutions and flood the session before the turn timeout.
|
|
786
|
+
if (solutions.length >= limit) {
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else if (event.type === 'output' || event.type === 'answer') {
|
|
791
|
+
output += `${event.content ?? ''}\n`;
|
|
792
|
+
}
|
|
793
|
+
else if (event.type === 'error') {
|
|
794
|
+
runError = event.content;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
this.runner.endFrame(frame.id);
|
|
800
|
+
this.frameDepth = childDepth - 1;
|
|
801
|
+
this.setMemory('harness', memoryBefore);
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
bindings: solutions[0] ?? [],
|
|
805
|
+
solutions,
|
|
806
|
+
output: output.slice(0, 4000),
|
|
807
|
+
error: runError,
|
|
808
|
+
elapsedMs: elapsed(),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// ---- Authoring agent (TS) ----------------------------------------------
|
|
812
|
+
/**
|
|
813
|
+
* The TypeScript authoring agent: a normal `LLMBackend` agent with harness
|
|
814
|
+
* tools. It runs one or more committed turns in a loop until the model calls
|
|
815
|
+
* `finish`. It runs one or more committed turns per request.
|
|
816
|
+
*/
|
|
817
|
+
async *agentPrompt(request, options = {}) {
|
|
818
|
+
if (this.disposed) {
|
|
819
|
+
yield { type: 'error', content: 'Session has been disposed' };
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!this.llmBackend) {
|
|
823
|
+
yield { type: 'error', content: 'authoring agent requires an injected LLM backend' };
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
this.turnCounter += 1;
|
|
827
|
+
const turn = this.turnCounter;
|
|
828
|
+
const historyStart = Date.now();
|
|
829
|
+
this.lastTurnCommitted = false;
|
|
830
|
+
this.lastCommitVersion = undefined;
|
|
831
|
+
// No automatic transaction: `define`/`assert`/… are refused until the agent
|
|
832
|
+
// calls `begin_turn`, so the transaction is explicit and visible.
|
|
833
|
+
const agent = await this.getHarnessAgent();
|
|
834
|
+
let failed = false;
|
|
835
|
+
let nudges = 0;
|
|
836
|
+
const maxNudges = 3;
|
|
837
|
+
let requestText = request;
|
|
838
|
+
try {
|
|
839
|
+
for (;;) {
|
|
840
|
+
for await (const event of agent.run(requestText, { signal: options.signal })) {
|
|
841
|
+
// A nested (`execute` child) error is a tool result the agent can
|
|
842
|
+
// react to; only the agent's own errors fail the turn.
|
|
843
|
+
if (event.type === 'error' && event.frameKind !== 'nested')
|
|
844
|
+
failed = true;
|
|
845
|
+
this.recordHistoryEvent(turn, event);
|
|
846
|
+
yield event;
|
|
847
|
+
}
|
|
848
|
+
if (options.signal?.aborted || failed)
|
|
849
|
+
break;
|
|
850
|
+
// The loop ended with an open transaction: nudge the agent to commit or
|
|
851
|
+
// discard it rather than auto-committing silently.
|
|
852
|
+
if (this.transactionBefore === null)
|
|
853
|
+
break;
|
|
854
|
+
if (nudges >= maxNudges)
|
|
855
|
+
break;
|
|
856
|
+
nudges += 1;
|
|
857
|
+
requestText = 'NUDGE: you still have an OPEN edit transaction with uncommitted changes. '
|
|
858
|
+
+ 'Call `end_turn` to commit it (or `rollback` to discard it), then call `finish`.';
|
|
859
|
+
this.recordHistoryEvent(turn, { type: 'output', content: requestText });
|
|
860
|
+
yield { type: 'output', content: requestText };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
failed = true;
|
|
865
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
866
|
+
this.recordHistoryEvent(turn, { type: 'error', content: message });
|
|
867
|
+
yield { type: 'error', content: message };
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
this.agentMessages = agent.getMessages();
|
|
871
|
+
}
|
|
872
|
+
if (options.signal?.aborted || failed) {
|
|
873
|
+
await this.rollbackTurn();
|
|
874
|
+
}
|
|
875
|
+
else if (this.transactionBefore !== null) {
|
|
876
|
+
// Last resort after the nudges: commit so nothing is lost, and show it.
|
|
877
|
+
await this.endTurn();
|
|
878
|
+
const marker = {
|
|
879
|
+
type: 'tool_call',
|
|
880
|
+
toolName: 'end_turn',
|
|
881
|
+
toolArgs: {},
|
|
882
|
+
toolState: 'completed',
|
|
883
|
+
toolSummary: 'commit transaction (host; agent did not end_turn)',
|
|
884
|
+
};
|
|
885
|
+
this.recordHistoryEvent(turn, marker);
|
|
886
|
+
yield marker;
|
|
887
|
+
}
|
|
888
|
+
await this.persistSessionHarness(this.history?.currentVersion ?? 0);
|
|
889
|
+
const outcome = options.signal?.aborted
|
|
890
|
+
? 'aborted'
|
|
891
|
+
: failed
|
|
892
|
+
? 'failed'
|
|
893
|
+
: this.lastTurnCommitted
|
|
894
|
+
? 'committed'
|
|
895
|
+
: 'answered';
|
|
896
|
+
await this.finishHistoryTurn({
|
|
897
|
+
turn,
|
|
898
|
+
mode: 'prompt',
|
|
899
|
+
request,
|
|
900
|
+
outcome,
|
|
901
|
+
commitVersion: this.lastCommitVersion,
|
|
902
|
+
durationMs: Date.now() - historyStart,
|
|
903
|
+
modelCalls: this.modelCallsThisTurn,
|
|
904
|
+
error: failed ? 'authoring turn failed' : undefined,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Deliver a steering message into the running authoring turn (agent mode).
|
|
909
|
+
* Ignored when no agent turn is active.
|
|
910
|
+
*/
|
|
911
|
+
steer(text) {
|
|
912
|
+
this.harnessAgent?.steer(text);
|
|
913
|
+
}
|
|
914
|
+
async getHarnessAgent() {
|
|
915
|
+
if (this.harnessAgent)
|
|
916
|
+
return this.harnessAgent;
|
|
917
|
+
this.harnessAgent = new HarnessAgent({
|
|
918
|
+
tools: createHarnessTools(this),
|
|
919
|
+
llmBackend: this.llmBackend,
|
|
920
|
+
model: this.model,
|
|
921
|
+
temperature: this.temperature,
|
|
922
|
+
maxTokens: this.maxTokens,
|
|
923
|
+
streaming: true,
|
|
924
|
+
maxModelCalls: this.maxModelCalls,
|
|
925
|
+
askInput: (prompt, options) => this.requireInput(prompt, options),
|
|
926
|
+
contextWindow: this.contextWindow,
|
|
927
|
+
compaction: this.compactionSettings
|
|
928
|
+
? { settings: this.compactionSettings, auto: this.compactionAuto }
|
|
929
|
+
: undefined,
|
|
930
|
+
workspacePath: this.workspacePath,
|
|
931
|
+
currentModel: `${this.provider}:${this.model}`,
|
|
932
|
+
modelCatalog: this.modelResolver?.catalog(),
|
|
933
|
+
knownMessages: this.agentMessages,
|
|
934
|
+
});
|
|
935
|
+
return this.harnessAgent;
|
|
936
|
+
}
|
|
937
|
+
/** Read the current harness manifest for the agent system prompt. */
|
|
938
|
+
async manifest() {
|
|
939
|
+
return this.manifestText();
|
|
940
|
+
}
|
|
941
|
+
/** The models available in this session (agent `models` tool). */
|
|
942
|
+
async listModels() {
|
|
943
|
+
const current = `${this.provider}:${this.model}`;
|
|
944
|
+
const resolver = this.modelResolver;
|
|
945
|
+
if (!resolver) {
|
|
946
|
+
return `current model: ${current}\n(no model catalog is configured; with_model is unavailable)`;
|
|
947
|
+
}
|
|
948
|
+
const models = resolver.catalog();
|
|
949
|
+
const body = models.length > 0
|
|
950
|
+
? models.map((model) => ` ${formatModelCatalogEntry(model)}`).join('\n')
|
|
951
|
+
: ' (none)';
|
|
952
|
+
return `current model: ${current}\n\navailable models (price = USD per 1M tokens, input/output):\n${body}`;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Generate a diagram of one entry predicate: ask the active model for Mermaid,
|
|
956
|
+
* then render it deterministically to a Unicode tree (always) and an SVG (for
|
|
957
|
+
* the terminal-image path). One model call.
|
|
958
|
+
*/
|
|
959
|
+
async diagram(entry, mode = 'presentation', options = {}) {
|
|
960
|
+
if (!this.llmBackend) {
|
|
961
|
+
throw new Error('diagram generation needs a model backend');
|
|
962
|
+
}
|
|
963
|
+
const listing = await this.listing('all');
|
|
964
|
+
const resolved = (entry ?? '').trim() || this.defaultDiagramEntry(listing);
|
|
965
|
+
if (!resolved) {
|
|
966
|
+
throw new Error('the harness has no predicate to diagram yet');
|
|
967
|
+
}
|
|
968
|
+
const prompt = buildDiagramPrompt({ entry: resolved, mode, listing });
|
|
969
|
+
const { text } = await generateLlmReply({
|
|
970
|
+
messages: [{ role: 'user', content: prompt }],
|
|
971
|
+
modelOptions: { provider: this.provider, model: this.model, temperature: this.temperature },
|
|
972
|
+
llmBackend: this.llmBackend,
|
|
973
|
+
signal: options.signal,
|
|
974
|
+
});
|
|
975
|
+
const mermaid = extractMermaid(text);
|
|
976
|
+
const warnings = validateMermaid(mermaid);
|
|
977
|
+
const svg = renderFlowchartSvg(parseMermaidFlowchart(mermaid));
|
|
978
|
+
return {
|
|
979
|
+
entry: resolved,
|
|
980
|
+
mode,
|
|
981
|
+
mermaid,
|
|
982
|
+
ascii: renderMermaidAscii(mermaid),
|
|
983
|
+
svg: svg.svg,
|
|
984
|
+
width: svg.width,
|
|
985
|
+
height: svg.height,
|
|
986
|
+
warnings,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Diagram of a skill for the authoring agent's `visualize` tool. */
|
|
990
|
+
async visualize(entry, mode = 'presentation') {
|
|
991
|
+
const result = await this.diagram(entry, mode);
|
|
992
|
+
const png = await rasterizeSvg(result.svg, { width: result.width, height: result.height });
|
|
993
|
+
if (png) {
|
|
994
|
+
return {
|
|
995
|
+
message: `Diagram of ${result.entry} (${result.mode}) is displayed to the user as an image. Do not repeat the diagram text.`,
|
|
996
|
+
event: { type: 'media', media: { mimeType: 'image/svg+xml', data: png.toString('base64') } },
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
return { message: result.ascii };
|
|
1000
|
+
}
|
|
1001
|
+
/** Display a workspace image (agent `show_image` tool). */
|
|
1002
|
+
async showImage(relPath) {
|
|
1003
|
+
return this.mediaEventFor(relPath);
|
|
1004
|
+
}
|
|
1005
|
+
/** Generate an image and display it (agent `generate_image` tool). */
|
|
1006
|
+
async generateImage(prompt) {
|
|
1007
|
+
if (!this.imageBackend) {
|
|
1008
|
+
return { message: 'Image generation is unavailable: no image backend is configured.' };
|
|
1009
|
+
}
|
|
1010
|
+
const result = await this.imageBackend.generate({ prompt });
|
|
1011
|
+
const shown = await this.mediaEventFor(result.path);
|
|
1012
|
+
return { message: `Generated ${result.path} (${result.mimeType}). ${shown.message}`, event: shown.event };
|
|
1013
|
+
}
|
|
1014
|
+
/** Write a workspace file (agent `write_file` tool); creates parent dirs. */
|
|
1015
|
+
async writeFile(relPath, content) {
|
|
1016
|
+
try {
|
|
1017
|
+
const abs = this.resolveWorkspacePath(relPath);
|
|
1018
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
1019
|
+
await fs.writeFile(abs, content, 'utf8');
|
|
1020
|
+
return { ok: true };
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
return { ok: false, error: error.message };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Build a `media` event for a workspace image (resolved to a PNG for inline
|
|
1028
|
+
* display). Returning an event lets any host render it through its normal
|
|
1029
|
+
* event pipeline — no session-specific image channel needed.
|
|
1030
|
+
*/
|
|
1031
|
+
async mediaEventFor(relPath) {
|
|
1032
|
+
const resolved = await resolveMedia(this.workspacePath ?? process.cwd(), { path: relPath });
|
|
1033
|
+
if (!resolved) {
|
|
1034
|
+
return { message: `Cannot read image ${relPath}.` };
|
|
1035
|
+
}
|
|
1036
|
+
const event = {
|
|
1037
|
+
type: 'media',
|
|
1038
|
+
media: { path: relPath, mimeType: resolved.mimeType, data: resolved.pngBase64 },
|
|
1039
|
+
};
|
|
1040
|
+
const message = resolved.pngBase64
|
|
1041
|
+
? `Image ${relPath} (${resolved.mimeType}) is displayed to the user.`
|
|
1042
|
+
: `Image ${relPath} (${resolved.mimeType}) is at workspace path ${relPath} `
|
|
1043
|
+
+ '(not rasterized for inline display: SVG needs @resvg/resvg-js, rasters need sharp).';
|
|
1044
|
+
return { message, event };
|
|
1045
|
+
}
|
|
1046
|
+
/** Pick a default diagram entry: `agent_main/N`, else the first predicate. */
|
|
1047
|
+
defaultDiagramEntry(listing) {
|
|
1048
|
+
const mains = this.predicateArities('agent_main');
|
|
1049
|
+
if (mains.length > 0) {
|
|
1050
|
+
return `agent_main/${mains[0]}`;
|
|
1051
|
+
}
|
|
1052
|
+
const reserved = new Set(['doc', 'module_doc', 'tool', 'system', 'user', 'param', 'parameters', 'answer']);
|
|
1053
|
+
for (const line of listing.split('\n')) {
|
|
1054
|
+
const match = /^([a-z][A-Za-z0-9_]*)\/(\d+)\s*$/.exec(line.trim());
|
|
1055
|
+
if (match && !reserved.has(match[1])) {
|
|
1056
|
+
return `${match[1]}/${match[2]}`;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
async beginTurn() {
|
|
1062
|
+
this.prologQuery(`deepclause_mi:repl_snapshot_save('${this.sessionId}')`);
|
|
1063
|
+
this.prologQuery(`deepclause_mi:repl_edit_touched_clear('${this.sessionId}')`);
|
|
1064
|
+
this.transactionBefore = this.predicateList();
|
|
1065
|
+
this.transactionDirty = false;
|
|
1066
|
+
}
|
|
1067
|
+
async endTurn() {
|
|
1068
|
+
if (this.transactionBefore === null) {
|
|
1069
|
+
return { committed: false, changed: [], error: 'no open transaction' };
|
|
1070
|
+
}
|
|
1071
|
+
const before = this.transactionBefore;
|
|
1072
|
+
const after = this.predicateList();
|
|
1073
|
+
const changed = this.simplePredicateDiff(before, after);
|
|
1074
|
+
const touched = this.editTouched();
|
|
1075
|
+
this.transactionBefore = null;
|
|
1076
|
+
const committed = this.transactionDirty;
|
|
1077
|
+
let version;
|
|
1078
|
+
if (committed && this.history) {
|
|
1079
|
+
version = await this.recordHistoryChange('commit', this.turnCounter, before, { touched });
|
|
1080
|
+
}
|
|
1081
|
+
this.lastTurnCommitted = committed;
|
|
1082
|
+
this.lastCommitVersion = version;
|
|
1083
|
+
this.transactionDirty = false;
|
|
1084
|
+
this.prologQuery(`deepclause_mi:repl_edit_touched_clear('${this.sessionId}')`);
|
|
1085
|
+
await this.persistSessionHarness(this.history?.currentVersion ?? 0);
|
|
1086
|
+
// Advisory: probe changed pure predicates for accidental nondeterminism.
|
|
1087
|
+
let warnings = [];
|
|
1088
|
+
if (committed) {
|
|
1089
|
+
const candidates = new Set();
|
|
1090
|
+
for (const entry of touched)
|
|
1091
|
+
candidates.add(entry.trim());
|
|
1092
|
+
for (const entry of changed)
|
|
1093
|
+
if (entry.startsWith('+'))
|
|
1094
|
+
candidates.add(entry.slice(1));
|
|
1095
|
+
warnings = await this.probeNondeterminism([...candidates]);
|
|
1096
|
+
}
|
|
1097
|
+
return { committed: true, changed, warnings };
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Probe changed predicates for accidental nondeterminism: a pure predicate
|
|
1101
|
+
* that yields more than `threshold` solutions on fresh variables is very likely
|
|
1102
|
+
* missing an `once/1`/`!` and would flood an `execute`. Fact tables and
|
|
1103
|
+
* side-effecting predicates are skipped by the Prolog probe.
|
|
1104
|
+
*/
|
|
1105
|
+
async probeNondeterminism(indicators) {
|
|
1106
|
+
const reserved = new Set(['doc', 'module_doc', 'tool']);
|
|
1107
|
+
const warnings = [];
|
|
1108
|
+
const threshold = 100;
|
|
1109
|
+
const bound = 200000;
|
|
1110
|
+
for (const indicator of indicators.slice(0, 8)) {
|
|
1111
|
+
const match = /^([a-z][A-Za-z0-9_]*)\/(\d+)$/.exec(indicator);
|
|
1112
|
+
if (!match || reserved.has(match[1]))
|
|
1113
|
+
continue;
|
|
1114
|
+
const name = match[1];
|
|
1115
|
+
const arity = match[2];
|
|
1116
|
+
try {
|
|
1117
|
+
const result = this.prologQuery(`deepclause_mi:repl_probe_nondeterminism('${this.sessionId}', ${name}, ${arity}, ${threshold}, ${bound}, Result)`);
|
|
1118
|
+
const verdict = String(plainValue(result?.Result) ?? '');
|
|
1119
|
+
if (verdict === 'many') {
|
|
1120
|
+
warnings.push(`${name}/${arity} yields more than ${threshold} solutions when called with fresh variables. `
|
|
1121
|
+
+ 'If a single answer is intended, make it deterministic with once/1 or !.');
|
|
1122
|
+
}
|
|
1123
|
+
else if (verdict === 'timeout') {
|
|
1124
|
+
warnings.push(`${name}/${arity} did not settle within the inference limit when probed `
|
|
1125
|
+
+ '(possible unbounded search, e.g. label/1 without once/1).');
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
// A probe failure must never fail the commit.
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return warnings;
|
|
1133
|
+
}
|
|
1134
|
+
/** Predicates touched by edit tools in the current transaction. */
|
|
1135
|
+
editTouched() {
|
|
1136
|
+
const result = this.prologQuery(`deepclause_mi:repl_edit_touched_text('${this.sessionId}', Text)`);
|
|
1137
|
+
return String(plainValue(result?.Text) ?? '')
|
|
1138
|
+
.split(',')
|
|
1139
|
+
.map((entry) => entry.trim())
|
|
1140
|
+
.filter(Boolean);
|
|
1141
|
+
}
|
|
1142
|
+
async rollbackTurn() {
|
|
1143
|
+
if (this.transactionBefore === null)
|
|
1144
|
+
return;
|
|
1145
|
+
this.prologQuery(`deepclause_mi:repl_snapshot_restore('${this.sessionId}')`);
|
|
1146
|
+
this.transactionBefore = null;
|
|
1147
|
+
this.lastTurnCommitted = false;
|
|
1148
|
+
this.lastCommitVersion = undefined;
|
|
1149
|
+
this.transactionDirty = false;
|
|
1150
|
+
}
|
|
1151
|
+
simplePredicateDiff(before, after) {
|
|
1152
|
+
const beforeSet = new Set(before);
|
|
1153
|
+
const afterSet = new Set(after);
|
|
1154
|
+
const changed = [];
|
|
1155
|
+
for (const predicate of after)
|
|
1156
|
+
if (!beforeSet.has(predicate))
|
|
1157
|
+
changed.push(`+${predicate}`);
|
|
1158
|
+
for (const predicate of before)
|
|
1159
|
+
if (!afterSet.has(predicate))
|
|
1160
|
+
changed.push(`-${predicate}`);
|
|
1161
|
+
return changed.sort();
|
|
1162
|
+
}
|
|
1163
|
+
/** Edit tools require an open transaction (opened by `begin_turn`). */
|
|
1164
|
+
requireTransaction() {
|
|
1165
|
+
if (this.transactionBefore === null) {
|
|
1166
|
+
return { ok: false, error: 'no open transaction; call begin_turn before editing' };
|
|
1167
|
+
}
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
editResult(result) {
|
|
1171
|
+
if (result === 'ok') {
|
|
1172
|
+
this.transactionDirty = true;
|
|
1173
|
+
return { ok: true };
|
|
1174
|
+
}
|
|
1175
|
+
return { ok: false, error: String(result) };
|
|
1176
|
+
}
|
|
1177
|
+
async define(head, clauses) {
|
|
1178
|
+
const closed = this.requireTransaction();
|
|
1179
|
+
if (closed)
|
|
1180
|
+
return closed;
|
|
1181
|
+
const result = this.prologQuery(`deepclause_mi:repl_define('${this.sessionId}', ${quoteString(head)}, ${quoteString(clauses)}, Result)`);
|
|
1182
|
+
return this.editResult(plainValue(result?.Result));
|
|
1183
|
+
}
|
|
1184
|
+
async assertClause(clause) {
|
|
1185
|
+
const closed = this.requireTransaction();
|
|
1186
|
+
if (closed)
|
|
1187
|
+
return closed;
|
|
1188
|
+
const result = this.prologQuery(`deepclause_mi:repl_assert('${this.sessionId}', ${quoteString(clause)}, Result)`);
|
|
1189
|
+
return this.editResult(plainValue(result?.Result));
|
|
1190
|
+
}
|
|
1191
|
+
async assertMany(clauses) {
|
|
1192
|
+
const closed = this.requireTransaction();
|
|
1193
|
+
if (closed)
|
|
1194
|
+
return closed;
|
|
1195
|
+
const result = this.prologQuery(`deepclause_mi:repl_assert_many('${this.sessionId}', ${quoteString(clauses)}, Result)`);
|
|
1196
|
+
return this.editResult(plainValue(result?.Result));
|
|
1197
|
+
}
|
|
1198
|
+
async retractClause(clause) {
|
|
1199
|
+
const closed = this.requireTransaction();
|
|
1200
|
+
if (closed)
|
|
1201
|
+
return closed;
|
|
1202
|
+
const result = this.prologQuery(`deepclause_mi:repl_retract('${this.sessionId}', ${quoteString(clause)}, Result)`);
|
|
1203
|
+
return this.editResult(plainValue(result?.Result));
|
|
1204
|
+
}
|
|
1205
|
+
async deletePredicate(nameArity) {
|
|
1206
|
+
const closed = this.requireTransaction();
|
|
1207
|
+
if (closed)
|
|
1208
|
+
return closed;
|
|
1209
|
+
const match = /^\s*([^\s/]+)\s*\/\s*(\d+)\s*$/.exec(nameArity);
|
|
1210
|
+
if (!match) {
|
|
1211
|
+
return { ok: false, error: `expected Name/Arity, got '${nameArity}'` };
|
|
1212
|
+
}
|
|
1213
|
+
const [, name, arityText] = match;
|
|
1214
|
+
const atom = /^[a-z][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
|
|
1215
|
+
const result = this.prologQuery(`deepclause_mi:repl_delete_predicate('${this.sessionId}', ${atom}, ${Number(arityText)}, Result)`);
|
|
1216
|
+
return this.editResult(plainValue(result?.Result));
|
|
1217
|
+
}
|
|
1218
|
+
async reference(topic) {
|
|
1219
|
+
switch (topic.trim().toLowerCase()) {
|
|
1220
|
+
case 'language':
|
|
1221
|
+
case 'dml':
|
|
1222
|
+
return this.loadDmlReference();
|
|
1223
|
+
case 'authoring':
|
|
1224
|
+
return this.loadDmlAuthoring();
|
|
1225
|
+
case 'examples':
|
|
1226
|
+
return this.loadDocAsset('harness-examples');
|
|
1227
|
+
case 'patterns':
|
|
1228
|
+
case 'harness-patterns':
|
|
1229
|
+
return this.loadDocAsset('harness-patterns');
|
|
1230
|
+
case 'snippets':
|
|
1231
|
+
return this.loadDocAsset('prolog-snippets');
|
|
1232
|
+
case 'tools':
|
|
1233
|
+
return this.hostToolsCatalog ?? '(no host capability tools registered)';
|
|
1234
|
+
default:
|
|
1235
|
+
return `unknown reference topic '${topic}'. Topics: language, authoring, examples, patterns, snippets, tools.`;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async execute(goal, limit, signal, onEvent) {
|
|
1239
|
+
return this.executeGoal(goal, limit ?? 5, onEvent, signal);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Prompt mode: English request -> meta task -> DML -> commit -> entry run.
|
|
1243
|
+
*
|
|
1244
|
+
* The meta task runs in a `meta` frame with exactly the host meta tools.
|
|
1245
|
+
* Its scratch edits are discarded; the returned DML source is the single
|
|
1246
|
+
* source of truth and is committed replace-by-predicate. A failed commit
|
|
1247
|
+
* triggers at most one repair round; any failure leaves the harness
|
|
1248
|
+
* byte-identical.
|
|
1249
|
+
*/
|
|
1250
|
+
/** Prompt mode: author the harness through the TypeScript agent. */
|
|
1251
|
+
async *prompt(request, options = {}) {
|
|
1252
|
+
yield* this.agentPrompt(request, options);
|
|
1253
|
+
}
|
|
1254
|
+
async dispose() {
|
|
1255
|
+
if (this.disposed)
|
|
1256
|
+
return;
|
|
1257
|
+
this.disposed = true;
|
|
1258
|
+
if (this.history) {
|
|
1259
|
+
try {
|
|
1260
|
+
await this.persistSessionHarness(this.history.currentVersion);
|
|
1261
|
+
this.assertHistoryFacts(await this.history.endSession('dispose'));
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
this.warnHistory(error);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
this.prologQuery(`deepclause_mi:destroy_session('${this.sessionId}')`);
|
|
1268
|
+
this.runner.clearFrames();
|
|
1269
|
+
await this.runner.dispose();
|
|
1270
|
+
}
|
|
1271
|
+
buildRunOptions(options) {
|
|
1272
|
+
const inputHandler = options.onInput ?? this.inputHandler;
|
|
1273
|
+
return {
|
|
1274
|
+
workspacePath: this.workspacePath,
|
|
1275
|
+
gasLimit: options.gasLimit ?? this.gasLimit,
|
|
1276
|
+
tools: options.tools ?? this.tools,
|
|
1277
|
+
toolPolicy: options.toolPolicy ?? this.toolPolicy,
|
|
1278
|
+
onInputRequired: inputHandler
|
|
1279
|
+
? (prompt) => this.requireInput(prompt)
|
|
1280
|
+
: async () => undefined,
|
|
1281
|
+
signal: options.signal,
|
|
1282
|
+
onModelCall: () => this.reserveModelCall(),
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
// ---- TypeScript compaction (ported from pi) ----
|
|
1286
|
+
/** Read a live memory slot as `MemoryMessage[]`. */
|
|
1287
|
+
getMemory(slot) {
|
|
1288
|
+
const result = this.prologQuery(`deepclause_mi:repl_get_memory('${this.sessionId}', ${slot}, Messages)`);
|
|
1289
|
+
const raw = toJsValue(result?.Messages);
|
|
1290
|
+
if (!Array.isArray(raw))
|
|
1291
|
+
return [];
|
|
1292
|
+
const messages = [];
|
|
1293
|
+
for (const entry of raw) {
|
|
1294
|
+
const value = toJsValue(entry);
|
|
1295
|
+
if (!value || typeof value !== 'object')
|
|
1296
|
+
continue;
|
|
1297
|
+
const role = String(toJsValue(value.role) ?? 'user');
|
|
1298
|
+
const content = String(toJsValue(value.content) ?? '');
|
|
1299
|
+
if (role !== 'system' && role !== 'user' && role !== 'assistant')
|
|
1300
|
+
continue;
|
|
1301
|
+
messages.push({ role, content });
|
|
1302
|
+
}
|
|
1303
|
+
return messages;
|
|
1304
|
+
}
|
|
1305
|
+
/** Replace a live memory slot. */
|
|
1306
|
+
setMemory(slot, messages) {
|
|
1307
|
+
const term = messages
|
|
1308
|
+
.map((message) => `message{role: ${message.role}, content: ${quoteString(message.content)}}`)
|
|
1309
|
+
.join(', ');
|
|
1310
|
+
this.prologQuery(`deepclause_mi:repl_set_memory('${this.sessionId}', ${slot}, [${term}])`);
|
|
1311
|
+
}
|
|
1312
|
+
/** Build the `memory_compaction` event for a compaction result. */
|
|
1313
|
+
compactionEvent(scope, trigger, result) {
|
|
1314
|
+
const event = toCompactionEvent(scope, trigger, result);
|
|
1315
|
+
const suffix = event.error ? ` (${event.error})` : '';
|
|
1316
|
+
return {
|
|
1317
|
+
type: 'memory_compaction',
|
|
1318
|
+
content: `compact ${scope}.${trigger} ${event.action} ${event.beforeTokens} -> ${event.afterTokens} tokens${suffix}`,
|
|
1319
|
+
compactionScope: scope,
|
|
1320
|
+
compactionTrigger: trigger,
|
|
1321
|
+
compactionAction: event.action,
|
|
1322
|
+
beforeTokens: event.beforeTokens,
|
|
1323
|
+
afterTokens: event.afterTokens,
|
|
1324
|
+
compactionError: event.error,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
/** Loop compaction hook: run before each model call inside a frame. */
|
|
1328
|
+
async compactLoopMessages(messages, lastInputTokens, contextWindow) {
|
|
1329
|
+
const settings = this.compactionSettings;
|
|
1330
|
+
if (!settings?.enabled || !this.llmBackend) {
|
|
1331
|
+
return { messages };
|
|
1332
|
+
}
|
|
1333
|
+
const contextTokens = lastInputTokens ?? estimateMemoryTokens(messages);
|
|
1334
|
+
if (!shouldCompact(contextTokens, contextWindow ?? this.contextWindow ?? 0, settings)) {
|
|
1335
|
+
return { messages };
|
|
1336
|
+
}
|
|
1337
|
+
const result = await compactMemory({
|
|
1338
|
+
messages,
|
|
1339
|
+
backend: this.llmBackend,
|
|
1340
|
+
model: this.model,
|
|
1341
|
+
settings,
|
|
1342
|
+
force: true,
|
|
1343
|
+
});
|
|
1344
|
+
return result.applied
|
|
1345
|
+
? { messages: result.messages, event: this.compactionEvent('loop', 'before_model_call', result) }
|
|
1346
|
+
: { messages };
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Explicitly compact the authoring conversation. Used by `compact.`; returns
|
|
1350
|
+
* the before/after token counts.
|
|
1351
|
+
*/
|
|
1352
|
+
async compact(customInstructions) {
|
|
1353
|
+
if (!this.harnessAgent) {
|
|
1354
|
+
return { applied: false, beforeTokens: 0, afterTokens: 0, error: 'compaction is disabled' };
|
|
1355
|
+
}
|
|
1356
|
+
const result = await this.harnessAgent.compactMemory(true, undefined, customInstructions);
|
|
1357
|
+
return {
|
|
1358
|
+
applied: result.applied,
|
|
1359
|
+
beforeTokens: result.tokensBefore,
|
|
1360
|
+
afterTokens: result.tokensAfter,
|
|
1361
|
+
error: result.error,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
// ---- Workspace file tools (meta task) ----
|
|
1365
|
+
/** Resolve a workspace-relative path, rejecting traversal/absolute paths. */
|
|
1366
|
+
resolveWorkspacePath(relPath) {
|
|
1367
|
+
const workspace = path.resolve(this.workspacePath ?? process.cwd());
|
|
1368
|
+
if (path.isAbsolute(relPath)) {
|
|
1369
|
+
throw new Error(`path must be workspace-relative: ${relPath}`);
|
|
1370
|
+
}
|
|
1371
|
+
const abs = path.resolve(workspace, relPath);
|
|
1372
|
+
if (abs !== workspace && !abs.startsWith(workspace + path.sep)) {
|
|
1373
|
+
throw new Error(`path escapes the workspace: ${relPath}`);
|
|
1374
|
+
}
|
|
1375
|
+
return abs;
|
|
1376
|
+
}
|
|
1377
|
+
normalizeRel(relPath) {
|
|
1378
|
+
return path.relative(path.resolve(this.workspacePath ?? process.cwd()), this.resolveWorkspacePath(relPath))
|
|
1379
|
+
.split(path.sep).join('/');
|
|
1380
|
+
}
|
|
1381
|
+
/** Read a workspace file. */
|
|
1382
|
+
async readWorkspaceFile(relPath) {
|
|
1383
|
+
try {
|
|
1384
|
+
return await fs.readFile(this.resolveWorkspacePath(this.normalizeRel(relPath)), 'utf8');
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Replace the human-in-the-loop channel after construction. The CLI uses
|
|
1392
|
+
* this to route input through the REPL's own readline interface instead of a
|
|
1393
|
+
* second interface on the same stdin.
|
|
1394
|
+
*/
|
|
1395
|
+
setInputHandler(handler) {
|
|
1396
|
+
this.inputHandler = handler;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* @deprecated Terminal images now flow as `media` events (rendered by the
|
|
1400
|
+
* host), so this is a no-op kept for backwards compatibility.
|
|
1401
|
+
*/
|
|
1402
|
+
setImageHandler(_handler) {
|
|
1403
|
+
// no-op
|
|
1404
|
+
}
|
|
1405
|
+
/** Resolve a human answer, or undefined when no channel is configured. */
|
|
1406
|
+
async requireInput(prompt, options) {
|
|
1407
|
+
if (!this.inputHandler) {
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1410
|
+
return this.inputHandler(prompt, options);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Reserve one model call for the current turn. When the budget is reached,
|
|
1414
|
+
* the human is asked whether to extend it by another `maxModelCalls`
|
|
1415
|
+
* (interactive runs); autonomous runs abort. `0` means unlimited.
|
|
1416
|
+
*/
|
|
1417
|
+
async reserveModelCall() {
|
|
1418
|
+
if (this.maxModelCalls <= 0) {
|
|
1419
|
+
this.modelCallsThisTurn += 1;
|
|
1420
|
+
return true;
|
|
1421
|
+
}
|
|
1422
|
+
if (this.modelCallsThisTurn >= this.modelCallBudget) {
|
|
1423
|
+
const extend = await this.askBudgetExtension();
|
|
1424
|
+
if (!extend) {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
this.modelCallBudget += this.maxModelCalls;
|
|
1428
|
+
}
|
|
1429
|
+
this.modelCallsThisTurn += 1;
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
async askBudgetExtension() {
|
|
1433
|
+
if (!this.inputHandler) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
const answer = await this.inputHandler(`Max model-call budget reached (${this.modelCallBudget}). Continue for another ${this.maxModelCalls} calls? (yes/no)\n`
|
|
1437
|
+
+ 'change the limit: repl.maxModelCalls in .deepclause/config.json '
|
|
1438
|
+
+ '("repl": { "maxModelCalls": N }), or pass --max-model-calls N; 0 means unlimited', ['yes', 'no']);
|
|
1439
|
+
return /^(y|yes)$/i.test((answer ?? '').trim());
|
|
1440
|
+
}
|
|
1441
|
+
prologQuery(goal) {
|
|
1442
|
+
try {
|
|
1443
|
+
const result = this.swipl.prolog.query(goal).once();
|
|
1444
|
+
return (result ?? null);
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
if (process.env.DEBUG_REPL) {
|
|
1448
|
+
console.error('[repl] Prolog query failed:', goal, error);
|
|
1449
|
+
}
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
serializeMemory(messages) {
|
|
1454
|
+
return messages
|
|
1455
|
+
.map((message) => `message{role: ${message.role}, content: ${quoteString(message.content)}}`)
|
|
1456
|
+
.join(', ');
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/** Quote a JS string as a Prolog double-quoted string. */
|
|
1460
|
+
export function quoteString(value) {
|
|
1461
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
1462
|
+
}
|
|
1463
|
+
/** Turn `fact/2` into the Prolog term `fact/2`; other filters become atoms. */
|
|
1464
|
+
function toListingFilter(filter) {
|
|
1465
|
+
if (/^[a-z][a-zA-Z0-9_]*\/\d+$/.test(filter)) {
|
|
1466
|
+
return filter;
|
|
1467
|
+
}
|
|
1468
|
+
if (/^[a-z][a-zA-Z0-9_]*$/.test(filter)) {
|
|
1469
|
+
return filter;
|
|
1470
|
+
}
|
|
1471
|
+
return `'${filter.replace(/'/g, "''")}'`;
|
|
1472
|
+
}
|
|
1473
|
+
//# sourceMappingURL=session.js.map
|