deepclause-sdk 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -3
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +148 -42
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +12 -12
- package/dist/cli/repl.d.ts +13 -1
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +177 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +119 -0
- package/dist/forms/registry.d.ts.map +1 -0
- package/dist/forms/registry.js +256 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/spec.d.ts +193 -0
- package/dist/forms/spec.d.ts.map +1 -0
- package/dist/forms/spec.js +295 -0
- package/dist/forms/spec.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog/loader.d.ts +18 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +26 -1
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +786 -40
- package/dist/repl/agent.d.ts +11 -0
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +23 -9
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/form-widget.d.ts +56 -0
- package/dist/repl/form-widget.d.ts.map +1 -0
- package/dist/repl/form-widget.js +289 -0
- package/dist/repl/form-widget.js.map +1 -0
- package/dist/repl/harness-tools.d.ts +4 -1
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +15 -58
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +24 -1
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +76 -8
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +20 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/process-backend.d.ts +2 -2
- package/dist/repl/process-backend.d.ts.map +1 -1
- package/dist/repl/process-executor.d.ts +11 -2
- package/dist/repl/process-executor.d.ts.map +1 -1
- package/dist/repl/process-executor.js +67 -18
- package/dist/repl/process-executor.js.map +1 -1
- package/dist/repl/process-worker.d.ts +35 -4
- package/dist/repl/process-worker.d.ts.map +1 -1
- package/dist/repl/process-worker.js +78 -38
- package/dist/repl/process-worker.js.map +1 -1
- package/dist/repl/processes.d.ts +4 -0
- package/dist/repl/processes.d.ts.map +1 -1
- package/dist/repl/processes.js +20 -0
- package/dist/repl/processes.js.map +1 -1
- package/dist/repl/render.d.ts +17 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +60 -0
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +143 -7
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +317 -20
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/form-view.d.ts +17 -0
- package/dist/repl/tui/form-view.d.ts.map +1 -0
- package/dist/repl/tui/form-view.js +25 -0
- package/dist/repl/tui/form-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +4 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +12 -0
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/worker-host.d.ts +16 -2
- package/dist/repl/worker-host.d.ts.map +1 -1
- package/dist/repl/worker-host.js +38 -6
- package/dist/repl/worker-host.js.map +1 -1
- package/dist/repl/worker-protocol.d.ts +14 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -1
- package/dist/repl/worker-protocol.js +8 -1
- package/dist/repl/worker-protocol.js.map +1 -1
- package/dist/runner.d.ts +69 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +381 -44
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +12 -0
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +452 -18
- package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
- package/dist/system/assets/docs/DML_FORMS.md +740 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +239 -4
- package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
- package/dist/system/assets/index.d.ts +2 -2
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +4 -0
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/environment.d.ts +6 -0
- package/dist/system/environment.d.ts.map +1 -1
- package/dist/system/environment.js +5 -4
- package/dist/system/environment.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/prolog-src/deepclause_mi.pl +786 -40
package/dist/repl/session.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadProlog, mountWorkspace, takePrologMessages, } from '../prolog/loade
|
|
|
13
13
|
import { describeShellExecutionBackend, } from '../system/runtime/shell-manager.js';
|
|
14
14
|
import { getWorkspaceDocAssetPath } from '../system/assets/index.js';
|
|
15
15
|
import { DMLRunner, toJsValue } from '../runner.js';
|
|
16
|
+
import { resolveReplJudgeSelection } from './judge.js';
|
|
16
17
|
import { DmlProcessExecutor } from './process-executor.js';
|
|
17
18
|
import { pidKey } from './processes.js';
|
|
18
19
|
import { compactMemory, estimateMemoryTokens, shouldCompact, toCompactionEvent, } from './compaction.js';
|
|
@@ -23,6 +24,7 @@ import { buildHarnessHeader, } from './export.js';
|
|
|
23
24
|
import { HistoryStore, harnessHash, moduleVersion, } from './history.js';
|
|
24
25
|
import { buildDiagramPrompt, extractMermaid, parseMermaidFlowchart, rasterizeSvg, renderFlowchartSvg, renderMermaidAscii, resolveMedia, validateMermaid, } from '../visualize/index.js';
|
|
25
26
|
import { generateLlmReply } from '../prolog/bridge.js';
|
|
27
|
+
import { FormRegistry } from '../forms/registry.js';
|
|
26
28
|
import { formatModelCatalogEntry } from './model-resolver.js';
|
|
27
29
|
/**
|
|
28
30
|
* Context note added when a saved session is reloaded. The opening words are
|
|
@@ -52,6 +54,16 @@ function historyEventFromRecord(record) {
|
|
|
52
54
|
case 'answer':
|
|
53
55
|
case 'error':
|
|
54
56
|
return { type, frameKind, content };
|
|
57
|
+
case 'html':
|
|
58
|
+
return {
|
|
59
|
+
type,
|
|
60
|
+
frameKind,
|
|
61
|
+
html: {
|
|
62
|
+
path: typeof record.html?.path === 'string'
|
|
63
|
+
? record.html.path
|
|
64
|
+
: '',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
55
67
|
case 'log':
|
|
56
68
|
return { type, frameKind, content, level: record.level };
|
|
57
69
|
case 'tool_call':
|
|
@@ -62,6 +74,10 @@ function historyEventFromRecord(record) {
|
|
|
62
74
|
toolState: record.toolState,
|
|
63
75
|
toolSummary: typeof record.toolSummary === 'string' ? record.toolSummary : undefined,
|
|
64
76
|
toolArgs: record.toolArgs,
|
|
77
|
+
toolResult: record.toolResult,
|
|
78
|
+
toolError: typeof record.toolError === 'string' ? record.toolError : undefined,
|
|
79
|
+
toolDurationMs: typeof record.toolDurationMs === 'number' ? record.toolDurationMs : undefined,
|
|
80
|
+
toolExitCode: typeof record.toolExitCode === 'number' ? record.toolExitCode : undefined,
|
|
65
81
|
};
|
|
66
82
|
case 'solution':
|
|
67
83
|
return {
|
|
@@ -123,8 +139,11 @@ export class DMLSession {
|
|
|
123
139
|
model;
|
|
124
140
|
provider;
|
|
125
141
|
tools;
|
|
142
|
+
authoringTools;
|
|
126
143
|
toolPolicy;
|
|
127
144
|
inputHandler;
|
|
145
|
+
/** Host-side interaction for an opened form (docs/DML_FORMS.md). */
|
|
146
|
+
formHandler;
|
|
128
147
|
maxDepth;
|
|
129
148
|
maxModelCalls;
|
|
130
149
|
hostToolsCatalog;
|
|
@@ -155,6 +174,7 @@ export class DMLSession {
|
|
|
155
174
|
modelCallBudget;
|
|
156
175
|
dmlReference;
|
|
157
176
|
dmlAuthoring;
|
|
177
|
+
dmlForms;
|
|
158
178
|
harnessExamples;
|
|
159
179
|
harnessPatterns;
|
|
160
180
|
prologSnippets;
|
|
@@ -175,6 +195,8 @@ export class DMLSession {
|
|
|
175
195
|
/** Session-scoped subagents (spec §2). */
|
|
176
196
|
processBackend;
|
|
177
197
|
processBackendPromise;
|
|
198
|
+
/** Form host state (docs/DML_FORMS.md); active only when the harness opts in. */
|
|
199
|
+
forms = new FormRegistry();
|
|
178
200
|
/**
|
|
179
201
|
* The harness snapshot new processes should run. On the main session this is
|
|
180
202
|
* unset (children snapshot the live module); a worker host sets it to the
|
|
@@ -187,6 +209,7 @@ export class DMLSession {
|
|
|
187
209
|
static PROCESS_LOG_LIMIT = 500;
|
|
188
210
|
/** Service declarations replayed from the session record (resume only). */
|
|
189
211
|
recordedServices = [];
|
|
212
|
+
pendingSuspensionValue;
|
|
190
213
|
/** Monotonic per-session turn counter used by history records. */
|
|
191
214
|
turnCounter = 0;
|
|
192
215
|
historyWarned = false;
|
|
@@ -198,11 +221,13 @@ export class DMLSession {
|
|
|
198
221
|
this.model = options.runnerOptions.model;
|
|
199
222
|
this.provider = String(options.runnerOptions.provider);
|
|
200
223
|
this.tools = options.tools ?? new Map();
|
|
224
|
+
this.authoringTools = options.authoringTools ?? new Map();
|
|
201
225
|
this.toolPolicy = options.toolPolicy ?? null;
|
|
202
226
|
this.inputHandler = options.onInput
|
|
203
227
|
?? (options.askUser ? (prompt, choices) => options.askUser(prompt, choices).then((r) => ('cancelled' in r ? undefined : r.answer)) : undefined)
|
|
204
228
|
?? options.onInputRequired
|
|
205
229
|
?? undefined;
|
|
230
|
+
this.formHandler = options.onForm;
|
|
206
231
|
this.maxDepth = options.maxDepth ?? 3;
|
|
207
232
|
this.maxModelCalls = options.maxModelCalls ?? 100;
|
|
208
233
|
this.modelCallBudget = this.maxModelCalls;
|
|
@@ -222,9 +247,26 @@ export class DMLSession {
|
|
|
222
247
|
this.modelResolver = options.runnerOptions.modelResolver;
|
|
223
248
|
this.temperature = options.runnerOptions.temperature;
|
|
224
249
|
this.maxTokens = options.runnerOptions.maxTokens;
|
|
250
|
+
// Direct DMLSession users (notably the browser child workers) do not go
|
|
251
|
+
// through createHarnessRepl's judge wiring. Without a default selection,
|
|
252
|
+
// judge/2 fails with "Unknown judgment backend: llm".
|
|
253
|
+
const judgeSelection = options.runnerOptions.judgeSelection
|
|
254
|
+
?? resolveReplJudgeSelection({
|
|
255
|
+
llmBackend: options.runnerOptions.llmBackend,
|
|
256
|
+
model: options.runnerOptions.model,
|
|
257
|
+
provider: String(options.runnerOptions.provider),
|
|
258
|
+
temperature: options.runnerOptions.temperature,
|
|
259
|
+
maxTokens: options.runnerOptions.maxTokens,
|
|
260
|
+
baseUrl: options.runnerOptions.baseUrl,
|
|
261
|
+
providerOptions: options.runnerOptions.providerOptions,
|
|
262
|
+
defaultJudge: options.defaultJudge,
|
|
263
|
+
jev: options.jev,
|
|
264
|
+
}).selection;
|
|
225
265
|
this.runner = new DMLRunner(swipl, {
|
|
226
266
|
...options.runnerOptions,
|
|
267
|
+
judgeSelection,
|
|
227
268
|
environment: this.executionEnvironment,
|
|
269
|
+
capabilityTool: options.capabilityTool,
|
|
228
270
|
compactMessages: options.compaction
|
|
229
271
|
? (messages, lastInputTokens, contextWindow) => this.compactLoopMessages(messages, lastInputTokens, contextWindow)
|
|
230
272
|
: undefined,
|
|
@@ -250,9 +292,11 @@ export class DMLSession {
|
|
|
250
292
|
const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
|
|
251
293
|
snapshot = await store.readSessionHarness();
|
|
252
294
|
session.recordedServices = await store.readServiceRecords();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
session.pendingSuspensionValue = (await store.readSuspendRecord()) ?? undefined;
|
|
296
|
+
// A session can have a transcript but no harness snapshot (older builds,
|
|
297
|
+
// or a turn interrupted before its first commit). Resume it from the
|
|
298
|
+
// current harness instead of refusing to open it; the transcript reader
|
|
299
|
+
// still restores every turn and event it has.
|
|
256
300
|
}
|
|
257
301
|
const sessionParams = {
|
|
258
302
|
workspace_path: options.workspacePath ?? './',
|
|
@@ -292,6 +336,27 @@ export class DMLSession {
|
|
|
292
336
|
session.warnHistory(new Error(`could not load harness '${session.harnessName}': ${loaded.error}`));
|
|
293
337
|
}
|
|
294
338
|
}
|
|
339
|
+
if (options.resume)
|
|
340
|
+
session.noteSessionResumed();
|
|
341
|
+
}
|
|
342
|
+
// Creating a session (or loading a harness) can move SWI's cwd away from the
|
|
343
|
+
// workspace, so reassert it before any goal runs — with the **mounted** path.
|
|
344
|
+
// `mountWorkspace` puts every workspace at `/workspace` and points both
|
|
345
|
+
// Emscripten's cwd and SWI's there; the host path is not in the WASM filesystem,
|
|
346
|
+
// so asserting *that* throws (harmlessly in the main thread, but it means the
|
|
347
|
+
// reassert never happened where it was needed).
|
|
348
|
+
//
|
|
349
|
+
// `working_directory/2` is process-global, so this is only meaningful while a
|
|
350
|
+
// process has one mount — true for every session today, since the mount itself
|
|
351
|
+
// is process-global. Query with `.once()`: `prolog.call()` returns an
|
|
352
|
+
// unexecuted query and wedges the next one.
|
|
353
|
+
if (options.workspacePath) {
|
|
354
|
+
try {
|
|
355
|
+
swipl.prolog.query("working_directory(_, '/workspace').").once();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
/* non-fatal: absolute paths still resolve */
|
|
359
|
+
}
|
|
295
360
|
}
|
|
296
361
|
return session;
|
|
297
362
|
}
|
|
@@ -354,7 +419,12 @@ export class DMLSession {
|
|
|
354
419
|
};
|
|
355
420
|
if (mode === 'worker') {
|
|
356
421
|
const { RemoteProcessHost } = await import('./worker-host.js');
|
|
357
|
-
const host = new RemoteProcessHost(this, {
|
|
422
|
+
const host = new RemoteProcessHost(this, {
|
|
423
|
+
onEvent,
|
|
424
|
+
workerFactory: this.openOptions.processWorkerFactory,
|
|
425
|
+
beforeStart: this.openOptions.beforeProcessStart,
|
|
426
|
+
beforeSpawn: this.openOptions.beforeProcessSpawn,
|
|
427
|
+
});
|
|
358
428
|
await host.start();
|
|
359
429
|
this.processBackend = host;
|
|
360
430
|
return host;
|
|
@@ -398,6 +468,47 @@ export class DMLSession {
|
|
|
398
468
|
recordedProcesses() {
|
|
399
469
|
return this.recordedServices;
|
|
400
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* A turn stopped by `suspend/1`: where it stopped and the goal that continues
|
|
473
|
+
* it. Set on resume, so a host can run the continuation once the harness and
|
|
474
|
+
* memory are back.
|
|
475
|
+
*/
|
|
476
|
+
pendingSuspension() {
|
|
477
|
+
return this.pendingSuspensionValue;
|
|
478
|
+
}
|
|
479
|
+
/** Forget the pending suspension (after resuming it, or deliberately dropping it). */
|
|
480
|
+
async clearSuspension() {
|
|
481
|
+
this.pendingSuspensionValue = undefined;
|
|
482
|
+
await this.history?.clearSuspendRecord();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Persist a suspension: the continuation, the memory it stopped with, and the
|
|
486
|
+
* module version it was running. The harness snapshot is written too, so the
|
|
487
|
+
* resumed session starts from exactly this module + memory.
|
|
488
|
+
*/
|
|
489
|
+
async handleSuspend(payload) {
|
|
490
|
+
if (!this.history)
|
|
491
|
+
return;
|
|
492
|
+
try {
|
|
493
|
+
if (payload.memory) {
|
|
494
|
+
// The turn's threaded memory is the authoritative one at the stop; make it
|
|
495
|
+
// the session's, so `harness.json` and the resumed state agree.
|
|
496
|
+
this.prologQuery(`deepclause_mi:repl_set_memory('${this.sessionId}', harness, ${payload.memory})`);
|
|
497
|
+
}
|
|
498
|
+
await this.history.writeSuspendRecord({
|
|
499
|
+
continuation: payload.continuation,
|
|
500
|
+
memory: payload.memory,
|
|
501
|
+
turn: this.turnCounter,
|
|
502
|
+
moduleVersion: this.history.currentVersion ? String(this.history.currentVersion) : undefined,
|
|
503
|
+
moduleHash: this.history.currentHash,
|
|
504
|
+
});
|
|
505
|
+
await this.persistSessionHarness(this.history.currentVersion);
|
|
506
|
+
this.pendingSuspensionValue = { continuation: payload.continuation, memory: payload.memory };
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
this.warnHistory(error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
401
512
|
/**
|
|
402
513
|
* Recent subagent output for the log view, oldest first. Answers and exits are
|
|
403
514
|
* included; per-tick state/activity updates are not (the summary shows those).
|
|
@@ -410,7 +521,7 @@ export class DMLSession {
|
|
|
410
521
|
* Events from the resumed session, to render it on load. Empty unless this
|
|
411
522
|
* session was opened with `resume` and history is enabled.
|
|
412
523
|
*/
|
|
413
|
-
async resumedTranscript(limit =
|
|
524
|
+
async resumedTranscript(limit = Number.POSITIVE_INFINITY) {
|
|
414
525
|
if (!this.resume || !this.history)
|
|
415
526
|
return [];
|
|
416
527
|
const records = await this.history.readEvents(this.sessionId, limit);
|
|
@@ -481,11 +592,40 @@ export class DMLSession {
|
|
|
481
592
|
sendProcess(to, messageTerm) {
|
|
482
593
|
this.processBackend?.send(to, messageTerm);
|
|
483
594
|
}
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
|
|
595
|
+
/**
|
|
596
|
+
* Request/reply against a service process (library(service) protocol).
|
|
597
|
+
*
|
|
598
|
+
* The message is normalized first: prose ("hello from the REPL") is not a
|
|
599
|
+
* Prolog term, and interpolating it into `request(From, Ref, …)` used to leave
|
|
600
|
+
* the caller waiting out the timeout while the service failed to parse the goal.
|
|
601
|
+
*/
|
|
602
|
+
async callProcess(to, messageTerm, timeoutMs = 30_000, signal) {
|
|
603
|
+
return (await this.ensureProcessBackend()).callTerm(to, this.normalizeMessage(messageTerm), timeoutMs, signal);
|
|
604
|
+
}
|
|
605
|
+
/** Quote a `call` message that does not parse as a term, so it is a payload. */
|
|
606
|
+
normalizeMessage(text) {
|
|
607
|
+
if (!text.trim())
|
|
608
|
+
return 'true';
|
|
609
|
+
const result = this.prologQuery(`deepclause_mi:repl_normalize_message(${quoteString(text)}, Term)`);
|
|
610
|
+
return String(plainValue(result?.Term) ?? text);
|
|
611
|
+
}
|
|
612
|
+
/** The synthetic pid shared by foreground/execute work in this session. */
|
|
613
|
+
async processHostPid() {
|
|
614
|
+
const backend = await this.ensureProcessBackend();
|
|
615
|
+
const hostPid = backend.hostPid?.();
|
|
616
|
+
if (!hostPid)
|
|
617
|
+
throw new Error('this process backend has no host pid');
|
|
618
|
+
return hostPid;
|
|
619
|
+
}
|
|
620
|
+
/** Handle a process primitive on behalf of a remote child session. */
|
|
621
|
+
async handleProcessRequest(pid, request, signal) {
|
|
622
|
+
const backend = await this.ensureProcessBackend();
|
|
623
|
+
const candidate = backend;
|
|
624
|
+
if (typeof candidate.handleRequest !== 'function') {
|
|
625
|
+
throw new Error('this process backend cannot service remote child requests');
|
|
626
|
+
}
|
|
627
|
+
return candidate.handleRequest(pid, request, signal);
|
|
487
628
|
}
|
|
488
|
-
/** Resolve a process primitive for this (non-process) foreground run. */
|
|
489
629
|
async handleForeground(request, signal) {
|
|
490
630
|
return (await this.ensureProcessBackend()).foregroundHandle(request, signal);
|
|
491
631
|
}
|
|
@@ -570,7 +710,12 @@ export class DMLSession {
|
|
|
570
710
|
* (a swarm item reports `failed`, say), whereas throwing would kill the process
|
|
571
711
|
* before it can report and leave a waiting parent hanging.
|
|
572
712
|
*/
|
|
573
|
-
async createProcessSession() {
|
|
713
|
+
async createProcessSession(pid) {
|
|
714
|
+
if (this.openOptions.createProcessSession) {
|
|
715
|
+
if (!pid)
|
|
716
|
+
throw new Error('createProcessSession requires a pid');
|
|
717
|
+
return this.openOptions.createProcessSession(pid);
|
|
718
|
+
}
|
|
574
719
|
const text = this.processSnapshotFor().text;
|
|
575
720
|
const child = await DMLSession.open({
|
|
576
721
|
...this.openOptions,
|
|
@@ -860,6 +1005,25 @@ export class DMLSession {
|
|
|
860
1005
|
return undefined;
|
|
861
1006
|
}
|
|
862
1007
|
}
|
|
1008
|
+
/** Write the user turn immediately, so an interrupted turn keeps its request. */
|
|
1009
|
+
async recordHistoryTurnStart(record) {
|
|
1010
|
+
if (!this.history)
|
|
1011
|
+
return;
|
|
1012
|
+
try {
|
|
1013
|
+
this.assertHistoryFacts(await this.history.recordTurn({
|
|
1014
|
+
...record,
|
|
1015
|
+
outcome: 'running',
|
|
1016
|
+
durationMs: 0,
|
|
1017
|
+
modelCalls: 0,
|
|
1018
|
+
}));
|
|
1019
|
+
// Snapshot before the turn as well: an interrupted turn then still has a
|
|
1020
|
+
// resumable harness and transcript, not just the previous turn's files.
|
|
1021
|
+
await this.persistSessionHarness(this.history.currentVersion);
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
this.warnHistory(error);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
863
1027
|
async finishHistoryTurn(record) {
|
|
864
1028
|
if (!this.history)
|
|
865
1029
|
return;
|
|
@@ -942,6 +1106,7 @@ export class DMLSession {
|
|
|
942
1106
|
this.turnCounter += 1;
|
|
943
1107
|
const historyTurn = this.turnCounter;
|
|
944
1108
|
const historyStart = Date.now();
|
|
1109
|
+
await this.recordHistoryTurnStart({ turn: historyTurn, mode: 'goal', request: goal });
|
|
945
1110
|
const frame = this.runner.registerDefaultFrame(frameKind);
|
|
946
1111
|
if (!frame) {
|
|
947
1112
|
yield { type: 'error', content: 'Failed to create query frame' };
|
|
@@ -976,6 +1141,7 @@ export class DMLSession {
|
|
|
976
1141
|
durationMs: Date.now() - historyStart,
|
|
977
1142
|
modelCalls: this.modelCallsThisTurn,
|
|
978
1143
|
});
|
|
1144
|
+
await this.persistSessionHarness(this.history?.currentVersion ?? 0);
|
|
979
1145
|
}
|
|
980
1146
|
/** Load the bundled DML reference once, for inclusion in the meta prompt. */
|
|
981
1147
|
async loadDmlReference() {
|
|
@@ -1005,6 +1171,17 @@ export class DMLSession {
|
|
|
1005
1171
|
}
|
|
1006
1172
|
/** Load a bundled doc asset once (cached by field). */
|
|
1007
1173
|
async loadDocAsset(name) {
|
|
1174
|
+
if (name === 'dml-forms') {
|
|
1175
|
+
if (this.dmlForms !== undefined)
|
|
1176
|
+
return this.dmlForms;
|
|
1177
|
+
try {
|
|
1178
|
+
this.dmlForms = await fs.readFile(getWorkspaceDocAssetPath('dml-forms'), 'utf8');
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
this.dmlForms = '';
|
|
1182
|
+
}
|
|
1183
|
+
return this.dmlForms;
|
|
1184
|
+
}
|
|
1008
1185
|
const cached = name === 'harness-examples'
|
|
1009
1186
|
? this.harnessExamples
|
|
1010
1187
|
: name === 'harness-patterns'
|
|
@@ -1286,6 +1463,7 @@ export class DMLSession {
|
|
|
1286
1463
|
this.turnCounter += 1;
|
|
1287
1464
|
const turn = this.turnCounter;
|
|
1288
1465
|
const historyStart = Date.now();
|
|
1466
|
+
await this.recordHistoryTurnStart({ turn, mode: 'prompt', request });
|
|
1289
1467
|
this.lastTurnCommitted = false;
|
|
1290
1468
|
this.lastCommitVersion = undefined;
|
|
1291
1469
|
// No automatic transaction: `define`/`assert`/… are refused until the agent
|
|
@@ -1374,8 +1552,24 @@ export class DMLSession {
|
|
|
1374
1552
|
async getHarnessAgent() {
|
|
1375
1553
|
if (this.harnessAgent)
|
|
1376
1554
|
return this.harnessAgent;
|
|
1555
|
+
const authoringTools = createHarnessTools(this);
|
|
1556
|
+
const allowed = this.openOptions.authoringToolNames
|
|
1557
|
+
? new Set(this.openOptions.authoringToolNames)
|
|
1558
|
+
: null;
|
|
1559
|
+
if (allowed) {
|
|
1560
|
+
for (const name of [...authoringTools.keys()]) {
|
|
1561
|
+
if (!allowed.has(name))
|
|
1562
|
+
authoringTools.delete(name);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
for (const [name, tool] of this.authoringTools) {
|
|
1566
|
+
if (allowed && !allowed.has(name))
|
|
1567
|
+
continue;
|
|
1568
|
+
if (!authoringTools.has(name))
|
|
1569
|
+
authoringTools.set(name, tool);
|
|
1570
|
+
}
|
|
1377
1571
|
this.harnessAgent = new HarnessAgent({
|
|
1378
|
-
tools:
|
|
1572
|
+
tools: authoringTools,
|
|
1379
1573
|
llmBackend: this.llmBackend,
|
|
1380
1574
|
model: this.model,
|
|
1381
1575
|
temperature: this.temperature,
|
|
@@ -1389,9 +1583,11 @@ export class DMLSession {
|
|
|
1389
1583
|
: undefined,
|
|
1390
1584
|
workspacePath: this.workspacePath,
|
|
1391
1585
|
environment: this.executionEnvironment,
|
|
1586
|
+
capabilityTool: this.openOptions.capabilityTool,
|
|
1392
1587
|
currentModel: `${this.provider}:${this.model}`,
|
|
1393
1588
|
modelCatalog: this.modelResolver?.catalog(),
|
|
1394
1589
|
knownMessages: this.agentMessages,
|
|
1590
|
+
agentSystemPrompt: this.openOptions.agentSystemPrompt,
|
|
1395
1591
|
});
|
|
1396
1592
|
return this.harnessAgent;
|
|
1397
1593
|
}
|
|
@@ -1520,13 +1716,14 @@ export class DMLSession {
|
|
|
1520
1716
|
return undefined;
|
|
1521
1717
|
}
|
|
1522
1718
|
async beginTurn() {
|
|
1523
|
-
// A second begin_turn
|
|
1719
|
+
// A second begin_turn must not re-snapshot the *mid-transaction* state:
|
|
1524
1720
|
// `rollback` would stop meaning "the last committed harness" and the
|
|
1525
|
-
// recorded diff would lose
|
|
1721
|
+
// recorded diff would lose edits made before it. Treat it as an idempotent
|
|
1722
|
+
// reminder instead of an error so agents do not get stuck retrying it.
|
|
1526
1723
|
if (this.transactionBefore !== null) {
|
|
1527
1724
|
return {
|
|
1528
|
-
ok:
|
|
1529
|
-
|
|
1725
|
+
ok: true,
|
|
1726
|
+
message: 'transaction is already open; continue editing, then call end_turn to commit or rollback to discard',
|
|
1530
1727
|
};
|
|
1531
1728
|
}
|
|
1532
1729
|
this.prologQuery(`deepclause_mi:repl_snapshot_save('${this.sessionId}')`);
|
|
@@ -1541,8 +1738,8 @@ export class DMLSession {
|
|
|
1541
1738
|
}
|
|
1542
1739
|
const before = this.transactionBefore;
|
|
1543
1740
|
const after = this.predicateList();
|
|
1544
|
-
const changed = this.simplePredicateDiff(before, after);
|
|
1545
1741
|
const touched = this.editTouched();
|
|
1742
|
+
const changed = this.simplePredicateDiff(before, after, touched);
|
|
1546
1743
|
this.transactionBefore = null;
|
|
1547
1744
|
const committed = this.transactionDirty;
|
|
1548
1745
|
let version;
|
|
@@ -1567,6 +1764,61 @@ export class DMLSession {
|
|
|
1567
1764
|
warnings.push(...this.probeToolContracts());
|
|
1568
1765
|
warnings.push(...this.probeDocContracts());
|
|
1569
1766
|
}
|
|
1767
|
+
// `committed` means a version was recorded. It used to be hard-coded `true`,
|
|
1768
|
+
// so a turn whose every edit was refused reported `{committed: true,
|
|
1769
|
+
// changed: []}` — which reads as "committed, nothing changed" and hides that
|
|
1770
|
+
// the edit never landed.
|
|
1771
|
+
return { committed, changed, warnings };
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* The living module serialized as loadable DML (the editable harness source).
|
|
1775
|
+
* `header: none` keeps it free of the export banner, so it round-trips through
|
|
1776
|
+
* `replaceHarness` without accumulating comments.
|
|
1777
|
+
*/
|
|
1778
|
+
async harnessSource() {
|
|
1779
|
+
const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', none, Text)`);
|
|
1780
|
+
return String(plainValue(result?.Text) ?? '');
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Replace the whole living module with `source` and commit it as a new
|
|
1784
|
+
* version.
|
|
1785
|
+
*
|
|
1786
|
+
* `loadCode` only adds/replaces clauses, so a predicate deleted from the text
|
|
1787
|
+
* would survive. This abolishes the user predicates (and their directives,
|
|
1788
|
+
* tools and sources) and reloads the text, preserving conversation memory, so
|
|
1789
|
+
* the editor's text is the source of truth. The result is an ordinary commit:
|
|
1790
|
+
* a history version, the persisted harness, and the same contract warnings.
|
|
1791
|
+
*/
|
|
1792
|
+
async replaceHarness(source) {
|
|
1793
|
+
if (this.disposed)
|
|
1794
|
+
return { committed: false, error: 'Session has been disposed' };
|
|
1795
|
+
if (this.isFrozen())
|
|
1796
|
+
return { committed: false, error: 'harness is frozen; editing is disabled' };
|
|
1797
|
+
// `repl_rollback` abolishes the predicates *before* loading, so a syntax error
|
|
1798
|
+
// would otherwise leave the session empty. Keep the previous text and put it
|
|
1799
|
+
// back if the new source does not load.
|
|
1800
|
+
const previous = await this.harnessSource();
|
|
1801
|
+
const before = this.predicateList();
|
|
1802
|
+
const result = this.prologQuery(`deepclause_mi:repl_rollback('${this.sessionId}', ${quoteString(source)}, Result)`);
|
|
1803
|
+
const outcome = plainValue(result?.Result);
|
|
1804
|
+
if (outcome !== 'ok') {
|
|
1805
|
+
this.prologQuery(`deepclause_mi:repl_rollback('${this.sessionId}', ${quoteString(previous)}, _)`);
|
|
1806
|
+
return { committed: false, error: String(outcome ?? 'replace failed') };
|
|
1807
|
+
}
|
|
1808
|
+
const after = this.predicateList();
|
|
1809
|
+
const changed = this.simplePredicateDiff(before, after);
|
|
1810
|
+
let version;
|
|
1811
|
+
if (this.history) {
|
|
1812
|
+
version = await this.recordHistoryChange('commit', this.turnCounter, before, { touched: after });
|
|
1813
|
+
}
|
|
1814
|
+
this.lastTurnCommitted = true;
|
|
1815
|
+
this.lastCommitVersion = version;
|
|
1816
|
+
await this.persistSessionHarness(this.history?.currentVersion ?? 0);
|
|
1817
|
+
const warnings = [
|
|
1818
|
+
...(await this.probeNondeterminism(after)),
|
|
1819
|
+
...this.probeToolContracts(),
|
|
1820
|
+
...this.probeDocContracts(),
|
|
1821
|
+
];
|
|
1570
1822
|
return { committed: true, changed, warnings };
|
|
1571
1823
|
}
|
|
1572
1824
|
/**
|
|
@@ -1658,13 +1910,27 @@ export class DMLSession {
|
|
|
1658
1910
|
this.lastCommitVersion = undefined;
|
|
1659
1911
|
this.transactionDirty = false;
|
|
1660
1912
|
}
|
|
1661
|
-
|
|
1913
|
+
/**
|
|
1914
|
+
* `+f/1` added, `-f/1` removed, `~f/1` rewritten in place.
|
|
1915
|
+
*
|
|
1916
|
+
* A set diff alone reports `[]` when a predicate is replaced by a new definition
|
|
1917
|
+
* with the same name and arity — the most common edit there is. An agent that
|
|
1918
|
+
* reads `changed` as "did my edit land?" was then told nothing had happened
|
|
1919
|
+
* while the clause block had in fact been swapped. `touched` is what makes the
|
|
1920
|
+
* rewrite visible: the edit tools replace by abolish-and-reassert, so anything
|
|
1921
|
+
* they touched and that still exists was rewritten.
|
|
1922
|
+
*/
|
|
1923
|
+
simplePredicateDiff(before, after, touched = []) {
|
|
1662
1924
|
const beforeSet = new Set(before);
|
|
1663
1925
|
const afterSet = new Set(after);
|
|
1926
|
+
const touchedSet = new Set(touched);
|
|
1664
1927
|
const changed = [];
|
|
1665
|
-
for (const predicate of after)
|
|
1928
|
+
for (const predicate of after) {
|
|
1666
1929
|
if (!beforeSet.has(predicate))
|
|
1667
1930
|
changed.push(`+${predicate}`);
|
|
1931
|
+
else if (touchedSet.has(predicate))
|
|
1932
|
+
changed.push(`~${predicate}`);
|
|
1933
|
+
}
|
|
1668
1934
|
for (const predicate of before)
|
|
1669
1935
|
if (!afterSet.has(predicate))
|
|
1670
1936
|
changed.push(`-${predicate}`);
|
|
@@ -1746,13 +2012,16 @@ export class DMLSession {
|
|
|
1746
2012
|
return this.loadDocAsset('harness-patterns');
|
|
1747
2013
|
case 'snippets':
|
|
1748
2014
|
return this.loadDocAsset('prolog-snippets');
|
|
2015
|
+
case 'forms':
|
|
2016
|
+
case 'dml-forms':
|
|
2017
|
+
return this.loadDocAsset('dml-forms');
|
|
1749
2018
|
case 'tools':
|
|
1750
2019
|
return this.hostToolsCatalog ?? '(no host capability tools registered)';
|
|
1751
2020
|
default:
|
|
1752
|
-
return `unknown reference topic '${topic}'. Topics: language, authoring, examples, patterns, snippets, tools.`;
|
|
2021
|
+
return `unknown reference topic '${topic}'. Topics: language, authoring, examples, patterns, snippets, forms, tools.`;
|
|
1753
2022
|
}
|
|
1754
2023
|
}
|
|
1755
|
-
async execute(goal, limit, signal, onEvent, bindingMode = 'full') {
|
|
2024
|
+
async execute(goal, limit, signal, onEvent, bindingMode = 'full', _timeoutMs) {
|
|
1756
2025
|
return this.executeGoal(goal, limit ?? 5, onEvent, signal, bindingMode);
|
|
1757
2026
|
}
|
|
1758
2027
|
/**
|
|
@@ -1803,6 +2072,10 @@ export class DMLSession {
|
|
|
1803
2072
|
onInputRequired: inputHandler
|
|
1804
2073
|
? (prompt) => this.requireInput(prompt)
|
|
1805
2074
|
: async () => undefined,
|
|
2075
|
+
onForm: this.formHandler,
|
|
2076
|
+
// A turn that called suspend/1 stops here; the record is what --session
|
|
2077
|
+
// resumes from (docs/DML_REPL_SUSPEND_RESUME.md).
|
|
2078
|
+
onSuspend: (payload) => this.handleSuspend(payload),
|
|
1806
2079
|
signal: options.signal,
|
|
1807
2080
|
onModelCall: () => this.reserveModelCall(),
|
|
1808
2081
|
// A run that is not itself a process resolves primitives through the
|
|
@@ -1810,9 +2083,33 @@ export class DMLSession {
|
|
|
1810
2083
|
onProcessRequest: options.process
|
|
1811
2084
|
? (request, signal) => options.process.handle(request, signal)
|
|
1812
2085
|
: (request, signal) => this.handleForeground(request, signal),
|
|
2086
|
+
forms: this.forms,
|
|
1813
2087
|
};
|
|
1814
2088
|
}
|
|
2089
|
+
/** The session's form host (docs/DML_FORMS.md). */
|
|
2090
|
+
formRegistry() {
|
|
2091
|
+
return this.forms;
|
|
2092
|
+
}
|
|
2093
|
+
/** Install the host-side interaction for opened forms. */
|
|
2094
|
+
setFormHandler(handler) {
|
|
2095
|
+
this.formHandler = handler;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Send a form action from the UI (the REPL's half of the form protocol).
|
|
2099
|
+
* Resolves an `await_form`/`wait_for_form_change` blocked in the session.
|
|
2100
|
+
*/
|
|
2101
|
+
submitForm(formId, change) {
|
|
2102
|
+
const record = this.forms.get(formId);
|
|
2103
|
+
if (!record || record.state !== 'open')
|
|
2104
|
+
return false;
|
|
2105
|
+
this.forms.submit(formId, change.action, change.values ?? {}, change.errors ?? []);
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
1815
2108
|
// ---- TypeScript compaction (ported from pi) ----
|
|
2109
|
+
/** Read the live harness memory (for disposable execute workers). */
|
|
2110
|
+
harnessMemory() {
|
|
2111
|
+
return this.getMemory('harness');
|
|
2112
|
+
}
|
|
1816
2113
|
/** Read a live memory slot as `MemoryMessage[]`. */
|
|
1817
2114
|
getMemory(slot) {
|
|
1818
2115
|
const result = this.prologQuery(`deepclause_mi:repl_get_memory('${this.sessionId}', ${slot}, Messages)`);
|