deepclause-sdk 0.1.3 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -6
- package/dist/cli/config.d.ts +2 -2
- package/dist/cli/repl.d.ts +36 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +414 -14
- package/dist/cli/repl.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +547 -2
- package/dist/repl/backend.d.ts +9 -0
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +44 -0
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +36 -0
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +173 -0
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +29 -0
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +71 -0
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +6 -0
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -1
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/picker.d.ts +3 -0
- package/dist/repl/picker.d.ts.map +1 -1
- package/dist/repl/picker.js +132 -0
- package/dist/repl/picker.js.map +1 -1
- package/dist/repl/process-backend.d.ts +31 -0
- package/dist/repl/process-backend.d.ts.map +1 -0
- package/dist/repl/process-backend.js +10 -0
- package/dist/repl/process-backend.js.map +1 -0
- package/dist/repl/process-executor.d.ts +67 -0
- package/dist/repl/process-executor.d.ts.map +1 -0
- package/dist/repl/process-executor.js +371 -0
- package/dist/repl/process-executor.js.map +1 -0
- package/dist/repl/process-worker.d.ts +10 -0
- package/dist/repl/process-worker.d.ts.map +1 -0
- package/dist/repl/process-worker.js +210 -0
- package/dist/repl/process-worker.js.map +1 -0
- package/dist/repl/processes.d.ts +282 -0
- package/dist/repl/processes.d.ts.map +1 -0
- package/dist/repl/processes.js +557 -0
- package/dist/repl/processes.js.map +1 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +19 -5
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +142 -3
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +435 -4
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/autocomplete.d.ts.map +1 -1
- package/dist/repl/tui/autocomplete.js +7 -0
- package/dist/repl/tui/autocomplete.js.map +1 -1
- package/dist/repl/tui/harness-view.d.ts +102 -0
- package/dist/repl/tui/harness-view.d.ts.map +1 -0
- package/dist/repl/tui/harness-view.js +323 -0
- package/dist/repl/tui/harness-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +6 -1
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/tui/services-panel.d.ts +35 -0
- package/dist/repl/tui/services-panel.d.ts.map +1 -0
- package/dist/repl/tui/services-panel.js +108 -0
- package/dist/repl/tui/services-panel.js.map +1 -0
- package/dist/repl/tui/status-bar.d.ts +2 -0
- package/dist/repl/tui/status-bar.d.ts.map +1 -1
- package/dist/repl/tui/status-bar.js +2 -0
- package/dist/repl/tui/status-bar.js.map +1 -1
- package/dist/repl/tui/tool-pane.d.ts.map +1 -1
- package/dist/repl/tui/tool-pane.js +6 -2
- package/dist/repl/tui/tool-pane.js.map +1 -1
- package/dist/repl/worker-host.d.ts +80 -0
- package/dist/repl/worker-host.d.ts.map +1 -0
- package/dist/repl/worker-host.js +305 -0
- package/dist/repl/worker-host.js.map +1 -0
- package/dist/repl/worker-protocol.d.ts +123 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -0
- package/dist/repl/worker-protocol.js +9 -0
- package/dist/repl/worker-protocol.js.map +1 -0
- package/dist/runner.d.ts +24 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +58 -0
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
- package/package.json +7 -6
- package/src/prolog-src/deepclause_mi.pl +547 -2
package/dist/repl/session.js
CHANGED
|
@@ -13,16 +13,103 @@ 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 { DmlProcessExecutor } from './process-executor.js';
|
|
17
|
+
import { pidKey } from './processes.js';
|
|
16
18
|
import { compactMemory, estimateMemoryTokens, shouldCompact, toCompactionEvent, } from './compaction.js';
|
|
17
19
|
import { HarnessAgent } from './agent.js';
|
|
18
20
|
import { BRIEF_VALUE_CHARS, createHarnessTools } from './harness-tools.js';
|
|
19
21
|
import { buildParamsTerm, plainValue, quotePrologString } from './frames.js';
|
|
20
22
|
import { buildHarnessHeader, } from './export.js';
|
|
21
|
-
import { HistoryStore, harnessHash, } from './history.js';
|
|
23
|
+
import { HistoryStore, harnessHash, moduleVersion, } from './history.js';
|
|
22
24
|
import { buildDiagramPrompt, extractMermaid, parseMermaidFlowchart, rasterizeSvg, renderFlowchartSvg, renderMermaidAscii, resolveMedia, validateMermaid, } from '../visualize/index.js';
|
|
23
25
|
import { generateLlmReply } from '../prolog/bridge.js';
|
|
24
26
|
import { formatModelCatalogEntry } from './model-resolver.js';
|
|
27
|
+
/**
|
|
28
|
+
* Context note added when a saved session is reloaded. The opening words are
|
|
29
|
+
* also the de-duplication marker in `repl_note_resumed/2` — keep them in sync.
|
|
30
|
+
*/
|
|
31
|
+
const RESUME_NOTE = 'This session was reloaded from a saved snapshot and its transient state was reset: '
|
|
32
|
+
+ 'the harness and conversation memory were restored, but any subagents/services running '
|
|
33
|
+
+ 'before are gone. Re-create anything you still need.';
|
|
34
|
+
const RESUME_NOTE_PREFIX = 'This session was reloaded';
|
|
25
35
|
let sessionCounter = 0;
|
|
36
|
+
/**
|
|
37
|
+
* One human, one editor: human-channel requests from the foreground and from any
|
|
38
|
+
* subagent are served strictly in order (see `requireInput`).
|
|
39
|
+
*/
|
|
40
|
+
let sharedInputQueue = Promise.resolve();
|
|
41
|
+
/** Rebuild a `DMLEvent` from a persisted history record, or undefined. */
|
|
42
|
+
function historyEventFromRecord(record) {
|
|
43
|
+
const type = typeof record.type === 'string' ? record.type : undefined;
|
|
44
|
+
if (!type)
|
|
45
|
+
return undefined;
|
|
46
|
+
const frameKind = typeof record.frame === 'string' ? record.frame : undefined;
|
|
47
|
+
const content = typeof record.content === 'string' ? record.content : undefined;
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'stream':
|
|
50
|
+
return content ? { type, frameKind, content } : undefined;
|
|
51
|
+
case 'output':
|
|
52
|
+
case 'answer':
|
|
53
|
+
case 'error':
|
|
54
|
+
return { type, frameKind, content };
|
|
55
|
+
case 'log':
|
|
56
|
+
return { type, frameKind, content, level: record.level };
|
|
57
|
+
case 'tool_call':
|
|
58
|
+
return {
|
|
59
|
+
type,
|
|
60
|
+
frameKind,
|
|
61
|
+
toolName: typeof record.toolName === 'string' ? record.toolName : undefined,
|
|
62
|
+
toolState: record.toolState,
|
|
63
|
+
toolSummary: typeof record.toolSummary === 'string' ? record.toolSummary : undefined,
|
|
64
|
+
toolArgs: record.toolArgs,
|
|
65
|
+
};
|
|
66
|
+
case 'solution':
|
|
67
|
+
return {
|
|
68
|
+
type,
|
|
69
|
+
frameKind,
|
|
70
|
+
solution: {
|
|
71
|
+
bindings: (Array.isArray(record.bindings) ? record.bindings : []),
|
|
72
|
+
deterministic: false,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
case 'task_activity':
|
|
76
|
+
return {
|
|
77
|
+
type,
|
|
78
|
+
frameKind,
|
|
79
|
+
taskState: record.taskState,
|
|
80
|
+
taskDescription: typeof record.taskDescription === 'string' ? record.taskDescription : undefined,
|
|
81
|
+
};
|
|
82
|
+
default:
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The log-view line for an event, or undefined for per-tick noise. */
|
|
87
|
+
function processLogText(event) {
|
|
88
|
+
switch (event.type) {
|
|
89
|
+
case 'started':
|
|
90
|
+
return 'started';
|
|
91
|
+
case 'output':
|
|
92
|
+
case 'stream':
|
|
93
|
+
case 'reasoning':
|
|
94
|
+
case 'tool':
|
|
95
|
+
case 'model':
|
|
96
|
+
return event.content ?? '';
|
|
97
|
+
case 'answer': {
|
|
98
|
+
// A process's terminal result is its accumulated output; the actual
|
|
99
|
+
// answer/1 value is the last non-empty line, so use that (and avoid
|
|
100
|
+
// duplicating the whole transcript in one log row).
|
|
101
|
+
const text = String(event.answer ?? 'ok');
|
|
102
|
+
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
103
|
+
return `› ${lines.length > 0 ? lines[lines.length - 1] : text}`;
|
|
104
|
+
}
|
|
105
|
+
case 'exited':
|
|
106
|
+
if (event.state === 'done')
|
|
107
|
+
return 'finished';
|
|
108
|
+
return `${event.state}${event.error ? ` (${event.error})` : event.reason && event.reason !== 'normal' ? ` (${String(event.reason)})` : ''}`;
|
|
109
|
+
default:
|
|
110
|
+
return undefined; // state/activity are shown in the summary view
|
|
111
|
+
}
|
|
112
|
+
}
|
|
26
113
|
function nextSessionId() {
|
|
27
114
|
sessionCounter += 1;
|
|
28
115
|
return `repl_${Date.now().toString(36)}_${sessionCounter}`;
|
|
@@ -83,6 +170,23 @@ export class DMLSession {
|
|
|
83
170
|
/** Full resolution for the backend, for the agent's environment block. */
|
|
84
171
|
executionEnvironment;
|
|
85
172
|
autoLoadHarness;
|
|
173
|
+
/** Original options, used to create per-process child sessions. */
|
|
174
|
+
openOptions;
|
|
175
|
+
/** Session-scoped subagents (spec §2). */
|
|
176
|
+
processBackend;
|
|
177
|
+
processBackendPromise;
|
|
178
|
+
/**
|
|
179
|
+
* The harness snapshot new processes should run. On the main session this is
|
|
180
|
+
* unset (children snapshot the live module); a worker host sets it to the
|
|
181
|
+
* snapshot the main sent, so its children never run a stale module.
|
|
182
|
+
*/
|
|
183
|
+
processSnapshot;
|
|
184
|
+
processEventSink;
|
|
185
|
+
/** Bounded log of subagent output for the `/ps --watch --logs` view. */
|
|
186
|
+
processLogEntries = [];
|
|
187
|
+
static PROCESS_LOG_LIMIT = 500;
|
|
188
|
+
/** Service declarations replayed from the session record (resume only). */
|
|
189
|
+
recordedServices = [];
|
|
86
190
|
/** Monotonic per-session turn counter used by history records. */
|
|
87
191
|
turnCounter = 0;
|
|
88
192
|
historyWarned = false;
|
|
@@ -109,6 +213,7 @@ export class DMLSession {
|
|
|
109
213
|
this.executionBackend = options.executionBackend ?? 'clean-room';
|
|
110
214
|
this.executionEnvironment = describeShellExecutionBackend(this.executionBackend === 'agentvm', this.executionBackend === 'agentvm' ? {} : { wrapper: this.executionBackend });
|
|
111
215
|
this.autoLoadHarness = options.autoLoadHarness ?? true;
|
|
216
|
+
this.openOptions = options;
|
|
112
217
|
this.compactionSettings = options.compaction?.settings;
|
|
113
218
|
this.compactionAuto = options.compaction?.auto ?? false;
|
|
114
219
|
this.contextWindow = options.contextWindow;
|
|
@@ -144,6 +249,7 @@ export class DMLSession {
|
|
|
144
249
|
}
|
|
145
250
|
const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
|
|
146
251
|
snapshot = await store.readSessionHarness();
|
|
252
|
+
session.recordedServices = await store.readServiceRecords();
|
|
147
253
|
if (!snapshot) {
|
|
148
254
|
throw new Error(`session not found or has no saved harness: ${session.sessionId}`);
|
|
149
255
|
}
|
|
@@ -174,6 +280,7 @@ export class DMLSession {
|
|
|
174
280
|
session.agentMessages = snapshot.memory.agent;
|
|
175
281
|
}
|
|
176
282
|
await session.restoreHarnessFromHistory(snapshot);
|
|
283
|
+
session.noteSessionResumed();
|
|
177
284
|
}
|
|
178
285
|
else if (session.autoLoadHarness) {
|
|
179
286
|
// One harness per name: a fresh session starts from the harness's current
|
|
@@ -191,6 +298,299 @@ export class DMLSession {
|
|
|
191
298
|
getSessionId() {
|
|
192
299
|
return this.sessionId;
|
|
193
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* The live harness, for the side view: what predicates exist right now, how big
|
|
303
|
+
* each is, and what the open transaction has touched.
|
|
304
|
+
*
|
|
305
|
+
* Read from the session module rather than from the last commit, so a side view
|
|
306
|
+
* shows the harness *being built* — including uncommitted edits.
|
|
307
|
+
*/
|
|
308
|
+
harnessView() {
|
|
309
|
+
const result = this.prologQuery(`deepclause_mi:repl_harness_view('${this.sessionId}', Rows)`);
|
|
310
|
+
const raw = toJsValue(result?.Rows);
|
|
311
|
+
const predicates = Array.isArray(raw)
|
|
312
|
+
? raw
|
|
313
|
+
.map((row) => String(row).split('|'))
|
|
314
|
+
.filter((parts) => parts.length === 3)
|
|
315
|
+
.map(([indicator, clauses, flags]) => {
|
|
316
|
+
const [name, arityText] = indicator.split('/');
|
|
317
|
+
return {
|
|
318
|
+
indicator: indicator.trim(),
|
|
319
|
+
name: String(name).trim(),
|
|
320
|
+
arity: Number(arityText),
|
|
321
|
+
clauses: Number(clauses),
|
|
322
|
+
flags: String(flags).split(',').map((flag) => flag.trim()).filter(Boolean),
|
|
323
|
+
};
|
|
324
|
+
})
|
|
325
|
+
: [];
|
|
326
|
+
const versionResult = this.prologQuery(`( current_predicate(${this.sessionId}:harness_head/4)`
|
|
327
|
+
+ ` -> ${this.sessionId}:harness_head(V, _, _, _) ; V = none )`);
|
|
328
|
+
const version = plainValue(versionResult?.V);
|
|
329
|
+
return {
|
|
330
|
+
predicates: predicates.sort((a, b) => a.indicator.localeCompare(b.indicator)),
|
|
331
|
+
version: version === 'none' ? undefined : Number(version),
|
|
332
|
+
transactionOpen: this.transactionBefore !== null,
|
|
333
|
+
touched: this.editTouched(),
|
|
334
|
+
lastCommit: this.lastTurnCommitted,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// ---- Session-scoped subagents (spec §2) --------------------------------
|
|
338
|
+
/** Register the sink that receives process lifecycle and output events. */
|
|
339
|
+
setProcessEventSink(sink) {
|
|
340
|
+
this.processEventSink = sink;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The process backend: in-process (L1) or worker-backed (L2). Created lazily
|
|
344
|
+
* so a session with no subagents pays nothing.
|
|
345
|
+
*/
|
|
346
|
+
ensureProcessBackend() {
|
|
347
|
+
if (this.processBackendPromise)
|
|
348
|
+
return this.processBackendPromise;
|
|
349
|
+
const mode = this.openOptions.execution ?? 'worker';
|
|
350
|
+
this.processBackendPromise = (async () => {
|
|
351
|
+
const onEvent = (event) => {
|
|
352
|
+
this.processEventSink?.(event);
|
|
353
|
+
this.recordServiceEvent(event);
|
|
354
|
+
};
|
|
355
|
+
if (mode === 'worker') {
|
|
356
|
+
const { RemoteProcessHost } = await import('./worker-host.js');
|
|
357
|
+
const host = new RemoteProcessHost(this, { onEvent });
|
|
358
|
+
await host.start();
|
|
359
|
+
this.processBackend = host;
|
|
360
|
+
return host;
|
|
361
|
+
}
|
|
362
|
+
const executor = new DmlProcessExecutor(this, { onEvent });
|
|
363
|
+
this.processBackend = executor;
|
|
364
|
+
return executor;
|
|
365
|
+
})();
|
|
366
|
+
return this.processBackendPromise;
|
|
367
|
+
}
|
|
368
|
+
/** Record a process declaration/exit so a resumed session can replay it (§7). */
|
|
369
|
+
recordServiceEvent(event) {
|
|
370
|
+
this.recordProcessLog(event);
|
|
371
|
+
if (!this.history)
|
|
372
|
+
return;
|
|
373
|
+
if (event.type === 'started') {
|
|
374
|
+
const info = this.processBackend?.info(event.pid);
|
|
375
|
+
void this.history.appendServiceRecord({
|
|
376
|
+
type: 'declare',
|
|
377
|
+
at: event.at,
|
|
378
|
+
pid: event.pid,
|
|
379
|
+
name: event.name,
|
|
380
|
+
goal: info?.goal,
|
|
381
|
+
priority: info?.priority,
|
|
382
|
+
codeVersion: info?.codeVersion,
|
|
383
|
+
harnessVersion: info?.harnessVersion,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else if (event.type === 'exited') {
|
|
387
|
+
void this.history.appendServiceRecord({
|
|
388
|
+
type: 'exit',
|
|
389
|
+
at: event.at,
|
|
390
|
+
pid: event.pid,
|
|
391
|
+
name: event.name,
|
|
392
|
+
state: event.state,
|
|
393
|
+
reason: event.reason,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** Service declarations replayed from the session record (resume only). */
|
|
398
|
+
recordedProcesses() {
|
|
399
|
+
return this.recordedServices;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Recent subagent output for the log view, oldest first. Answers and exits are
|
|
403
|
+
* included; per-tick state/activity updates are not (the summary shows those).
|
|
404
|
+
*/
|
|
405
|
+
processLog(limit = 200) {
|
|
406
|
+
const entries = this.processLogEntries;
|
|
407
|
+
return limit >= entries.length ? [...entries] : entries.slice(entries.length - limit);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Events from the resumed session, to render it on load. Empty unless this
|
|
411
|
+
* session was opened with `resume` and history is enabled.
|
|
412
|
+
*/
|
|
413
|
+
async resumedTranscript(limit = 400) {
|
|
414
|
+
if (!this.resume || !this.history)
|
|
415
|
+
return [];
|
|
416
|
+
const records = await this.history.readEvents(this.sessionId, limit);
|
|
417
|
+
return records
|
|
418
|
+
.map((record) => historyEventFromRecord(record))
|
|
419
|
+
.filter((event) => Boolean(event));
|
|
420
|
+
}
|
|
421
|
+
recordProcessLog(event) {
|
|
422
|
+
const text = processLogText(event);
|
|
423
|
+
if (text === undefined)
|
|
424
|
+
return;
|
|
425
|
+
// Coalesce streamed chunks into one growing line per process/kind.
|
|
426
|
+
const last = this.processLogEntries[this.processLogEntries.length - 1];
|
|
427
|
+
const streamy = event.type === 'stream' || event.type === 'reasoning';
|
|
428
|
+
if (streamy && last && last.kind === event.type && pidKey(last.pid) === pidKey(event.pid) && !last.done) {
|
|
429
|
+
last.text = (last.text + text).slice(-2000);
|
|
430
|
+
last.done = event.done;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.processLogEntries.push({
|
|
434
|
+
at: event.at,
|
|
435
|
+
pid: event.pid,
|
|
436
|
+
name: event.name,
|
|
437
|
+
kind: event.type,
|
|
438
|
+
text,
|
|
439
|
+
done: event.done,
|
|
440
|
+
});
|
|
441
|
+
if (this.processLogEntries.length > DMLSession.PROCESS_LOG_LIMIT) {
|
|
442
|
+
this.processLogEntries.splice(0, this.processLogEntries.length - DMLSession.PROCESS_LOG_LIMIT);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** Start a session-scoped subagent running `goal`; returns immediately. */
|
|
446
|
+
async spawnProcess(goal, options = {}) {
|
|
447
|
+
return (await this.ensureProcessBackend()).spawn(goal, options);
|
|
448
|
+
}
|
|
449
|
+
/** The whole system state: every process this session owns. */
|
|
450
|
+
processes() {
|
|
451
|
+
return this.processBackend?.processes() ?? [];
|
|
452
|
+
}
|
|
453
|
+
processInfo(pid) {
|
|
454
|
+
return this.processBackend?.info(pid);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* The harness source a process is running, for inspection. Accepts a pid or a
|
|
458
|
+
* registered name. Undefined when the process is unknown.
|
|
459
|
+
*/
|
|
460
|
+
processCode(target) {
|
|
461
|
+
const pid = typeof target === 'string' ? this.processBackend?.whereis(target) : target;
|
|
462
|
+
return pid ? this.processBackend?.codeOf(pid) : undefined;
|
|
463
|
+
}
|
|
464
|
+
/** The version of the harness the session itself is currently editing. */
|
|
465
|
+
currentHarnessVersion() {
|
|
466
|
+
return this.processSnapshotFor().version;
|
|
467
|
+
}
|
|
468
|
+
/** Stop one process by pid or registered name. */
|
|
469
|
+
async stopProcess(target, reason = 'shutdown') {
|
|
470
|
+
await (await this.ensureProcessBackend()).stop(target, reason);
|
|
471
|
+
}
|
|
472
|
+
/** Resolve a registered process name. */
|
|
473
|
+
whereisProcess(name) {
|
|
474
|
+
return this.processBackend?.whereis(name);
|
|
475
|
+
}
|
|
476
|
+
/** Register a process under a session-scoped name. */
|
|
477
|
+
registerProcess(name, pid) {
|
|
478
|
+
this.processBackend?.register(name, pid);
|
|
479
|
+
}
|
|
480
|
+
/** Copy a message term into a process's mailbox. */
|
|
481
|
+
sendProcess(to, messageTerm) {
|
|
482
|
+
this.processBackend?.send(to, messageTerm);
|
|
483
|
+
}
|
|
484
|
+
/** Request/reply against a service process (library(service) protocol). */
|
|
485
|
+
async callProcess(to, messageTerm, timeoutMs = 300_000) {
|
|
486
|
+
return (await this.ensureProcessBackend()).callTerm(to, messageTerm, timeoutMs);
|
|
487
|
+
}
|
|
488
|
+
/** Resolve a process primitive for this (non-process) foreground run. */
|
|
489
|
+
async handleForeground(request, signal) {
|
|
490
|
+
return (await this.ensureProcessBackend()).foregroundHandle(request, signal);
|
|
491
|
+
}
|
|
492
|
+
/** Host tool descriptors, for a worker's proxy tools. */
|
|
493
|
+
toolDescriptors() {
|
|
494
|
+
return [...this.tools.entries()].map(([name, tool]) => ({
|
|
495
|
+
name,
|
|
496
|
+
description: tool.description,
|
|
497
|
+
parameters: tool.parameters,
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
/** Run a model call on behalf of a worker's proxy backend. */
|
|
501
|
+
async completeModelRequest(request) {
|
|
502
|
+
if (!this.llmBackend)
|
|
503
|
+
throw new Error('this session has no LLM backend');
|
|
504
|
+
return this.llmBackend.complete(request);
|
|
505
|
+
}
|
|
506
|
+
/** Run a host tool on behalf of a worker's proxy tool. */
|
|
507
|
+
runHostTool(name, args, signal) {
|
|
508
|
+
const tool = this.tools.get(name);
|
|
509
|
+
if (!tool)
|
|
510
|
+
throw new Error(`unknown host tool: ${name}`);
|
|
511
|
+
return tool.execute(args, { signal });
|
|
512
|
+
}
|
|
513
|
+
/** Prompt the human on behalf of a worker's `input/2`. */
|
|
514
|
+
requestHumanInput(prompt) {
|
|
515
|
+
return this.requireInput(prompt);
|
|
516
|
+
}
|
|
517
|
+
// Accessors for building a worker's init data (see worker-host.ts).
|
|
518
|
+
workspacePathValue() {
|
|
519
|
+
return this.workspacePath;
|
|
520
|
+
}
|
|
521
|
+
modelId() {
|
|
522
|
+
return this.model;
|
|
523
|
+
}
|
|
524
|
+
providerId() {
|
|
525
|
+
return this.provider;
|
|
526
|
+
}
|
|
527
|
+
temperatureValue() {
|
|
528
|
+
return this.temperature;
|
|
529
|
+
}
|
|
530
|
+
maxTokensValue() {
|
|
531
|
+
return this.maxTokens;
|
|
532
|
+
}
|
|
533
|
+
toolPolicyValue() {
|
|
534
|
+
return this.toolPolicy;
|
|
535
|
+
}
|
|
536
|
+
/** The living module as loadable DML source, for a process snapshot. */
|
|
537
|
+
async harnessText() {
|
|
538
|
+
return this.harnessTextSync();
|
|
539
|
+
}
|
|
540
|
+
/** Synchronous form of `harnessText`, for spawn-time stamping. */
|
|
541
|
+
harnessTextSync() {
|
|
542
|
+
const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', '', Text)`);
|
|
543
|
+
return String(plainValue(result?.Text) ?? '');
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The harness snapshot and its version for a process about to be spawned.
|
|
547
|
+
*
|
|
548
|
+
* A worker host holds the snapshot the main sent; the main computes it from
|
|
549
|
+
* the live module. Either way, every process carries the exact module it runs
|
|
550
|
+
* and a version that lets two processes be compared at a glance.
|
|
551
|
+
*/
|
|
552
|
+
processSnapshotFor() {
|
|
553
|
+
if (this.processSnapshot)
|
|
554
|
+
return this.processSnapshot;
|
|
555
|
+
const text = this.harnessTextSync();
|
|
556
|
+
return { text, version: moduleVersion(text) };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Adopt a process snapshot pushed by the main session (worker hosts only).
|
|
560
|
+
* Keeps a worker's children on the code the user actually committed.
|
|
561
|
+
*/
|
|
562
|
+
setProcessSnapshot(text, version) {
|
|
563
|
+
this.processSnapshot = { text, version: version ?? moduleVersion(text) };
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* A private child session carrying a snapshot of this harness (invariant C).
|
|
567
|
+
*
|
|
568
|
+
* Returns the session and, when the snapshot could not be loaded, the load
|
|
569
|
+
* error. It does **not** throw on a load failure: the child still runs its goal
|
|
570
|
+
* (a swarm item reports `failed`, say), whereas throwing would kill the process
|
|
571
|
+
* before it can report and leave a waiting parent hanging.
|
|
572
|
+
*/
|
|
573
|
+
async createProcessSession() {
|
|
574
|
+
const text = this.processSnapshotFor().text;
|
|
575
|
+
const child = await DMLSession.open({
|
|
576
|
+
...this.openOptions,
|
|
577
|
+
execution: 'inline',
|
|
578
|
+
sessionId: undefined,
|
|
579
|
+
resume: false,
|
|
580
|
+
history: undefined,
|
|
581
|
+
autoLoadHarness: false,
|
|
582
|
+
tools: this.tools,
|
|
583
|
+
toolPolicy: this.toolPolicy,
|
|
584
|
+
onInput: this.inputHandler,
|
|
585
|
+
onInputRequired: undefined,
|
|
586
|
+
});
|
|
587
|
+
let loadError;
|
|
588
|
+
if (text.trim()) {
|
|
589
|
+
const loaded = await child.loadCode(text);
|
|
590
|
+
loadError = loaded.error;
|
|
591
|
+
}
|
|
592
|
+
return { session: child, loadError };
|
|
593
|
+
}
|
|
194
594
|
/**
|
|
195
595
|
* Prolog output captured since the last drain.
|
|
196
596
|
*
|
|
@@ -301,6 +701,19 @@ export class DMLSession {
|
|
|
301
701
|
this.warnHistory(error);
|
|
302
702
|
}
|
|
303
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Note in the model's context that this session was restored. Without it an
|
|
706
|
+
* agent can assume work from a previous run is still alive — a subagent, a
|
|
707
|
+
* service, an in-flight job — when the runtime state is gone. The note is
|
|
708
|
+
* replaced (not stacked) across repeated resumes.
|
|
709
|
+
*/
|
|
710
|
+
noteSessionResumed() {
|
|
711
|
+
this.prologQuery(`deepclause_mi:repl_note_resumed('${this.sessionId}', ${quoteString(RESUME_NOTE)})`);
|
|
712
|
+
this.agentMessages = [
|
|
713
|
+
...this.agentMessages.filter((message) => !message.content.startsWith(RESUME_NOTE_PREFIX)),
|
|
714
|
+
{ role: 'system', content: RESUME_NOTE },
|
|
715
|
+
];
|
|
716
|
+
}
|
|
304
717
|
/**
|
|
305
718
|
* Restore a resumed session's harness from its durable snapshot and continue
|
|
306
719
|
* its turn numbering. The saved module is a restore, not a user load, so it
|
|
@@ -783,7 +1196,7 @@ export class DMLSession {
|
|
|
783
1196
|
* The child frame uses the living scratch module and may itself call task()
|
|
784
1197
|
* (RLM-style recursion); its harness-memory writes are discarded afterward.
|
|
785
1198
|
*/
|
|
786
|
-
async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full') {
|
|
1199
|
+
async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full', process) {
|
|
787
1200
|
const startedAt = Date.now();
|
|
788
1201
|
const elapsed = () => Date.now() - startedAt;
|
|
789
1202
|
const childDepth = this.frameDepth + 1;
|
|
@@ -814,7 +1227,7 @@ export class DMLSession {
|
|
|
814
1227
|
let runError;
|
|
815
1228
|
this.frameDepth = childDepth;
|
|
816
1229
|
try {
|
|
817
|
-
for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal }))) {
|
|
1230
|
+
for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal, process }))) {
|
|
818
1231
|
onEvent?.(event);
|
|
819
1232
|
if (event.type === 'solution') {
|
|
820
1233
|
if (solutions.length < limit) {
|
|
@@ -1359,6 +1772,14 @@ export class DMLSession {
|
|
|
1359
1772
|
if (this.disposed)
|
|
1360
1773
|
return;
|
|
1361
1774
|
this.disposed = true;
|
|
1775
|
+
if (this.processBackend) {
|
|
1776
|
+
try {
|
|
1777
|
+
await this.processBackend.shutdown();
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
// teardown is best-effort
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1362
1783
|
if (this.history) {
|
|
1363
1784
|
try {
|
|
1364
1785
|
await this.persistSessionHarness(this.history.currentVersion);
|
|
@@ -1384,6 +1805,11 @@ export class DMLSession {
|
|
|
1384
1805
|
: async () => undefined,
|
|
1385
1806
|
signal: options.signal,
|
|
1386
1807
|
onModelCall: () => this.reserveModelCall(),
|
|
1808
|
+
// A run that is not itself a process resolves primitives through the
|
|
1809
|
+
// process backend (local or worker); a process uses its own binding.
|
|
1810
|
+
onProcessRequest: options.process
|
|
1811
|
+
? (request, signal) => options.process.handle(request, signal)
|
|
1812
|
+
: (request, signal) => this.handleForeground(request, signal),
|
|
1387
1813
|
};
|
|
1388
1814
|
}
|
|
1389
1815
|
// ---- TypeScript compaction (ported from pi) ----
|
|
@@ -1511,7 +1937,12 @@ export class DMLSession {
|
|
|
1511
1937
|
if (!this.inputHandler) {
|
|
1512
1938
|
return undefined;
|
|
1513
1939
|
}
|
|
1514
|
-
|
|
1940
|
+
// Serialize human-channel requests across every session in this process: a
|
|
1941
|
+
// background subagent's input/2 must not clobber a foreground prompt (one
|
|
1942
|
+
// editor, one human). Child process sessions share this module-level queue.
|
|
1943
|
+
const value = sharedInputQueue.then(() => this.inputHandler(prompt, options));
|
|
1944
|
+
sharedInputQueue = value.then(() => undefined, () => undefined);
|
|
1945
|
+
return value;
|
|
1515
1946
|
}
|
|
1516
1947
|
/**
|
|
1517
1948
|
* Reserve one model call for the current turn. When the budget is reached,
|