deepclause-sdk 0.1.2 → 0.2.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.
Files changed (122) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/config.d.ts +2 -2
  3. package/dist/cli/repl.d.ts +55 -4
  4. package/dist/cli/repl.d.ts.map +1 -1
  5. package/dist/cli/repl.js +675 -52
  6. package/dist/cli/repl.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/prolog-src/deepclause_mi.pl +516 -2
  12. package/dist/repl/backend.d.ts +9 -0
  13. package/dist/repl/backend.d.ts.map +1 -1
  14. package/dist/repl/backend.js +62 -5
  15. package/dist/repl/backend.js.map +1 -1
  16. package/dist/repl/harness-tools.d.ts +36 -0
  17. package/dist/repl/harness-tools.d.ts.map +1 -1
  18. package/dist/repl/harness-tools.js +173 -0
  19. package/dist/repl/harness-tools.js.map +1 -1
  20. package/dist/repl/history.d.ts +29 -0
  21. package/dist/repl/history.d.ts.map +1 -1
  22. package/dist/repl/history.js +71 -0
  23. package/dist/repl/history.js.map +1 -1
  24. package/dist/repl/index.d.ts +6 -0
  25. package/dist/repl/index.d.ts.map +1 -1
  26. package/dist/repl/index.js +9 -1
  27. package/dist/repl/index.js.map +1 -1
  28. package/dist/repl/picker.d.ts +3 -0
  29. package/dist/repl/picker.d.ts.map +1 -1
  30. package/dist/repl/picker.js +132 -0
  31. package/dist/repl/picker.js.map +1 -1
  32. package/dist/repl/process-backend.d.ts +31 -0
  33. package/dist/repl/process-backend.d.ts.map +1 -0
  34. package/dist/repl/process-backend.js +10 -0
  35. package/dist/repl/process-backend.js.map +1 -0
  36. package/dist/repl/process-executor.d.ts +67 -0
  37. package/dist/repl/process-executor.d.ts.map +1 -0
  38. package/dist/repl/process-executor.js +371 -0
  39. package/dist/repl/process-executor.js.map +1 -0
  40. package/dist/repl/process-worker.d.ts +10 -0
  41. package/dist/repl/process-worker.d.ts.map +1 -0
  42. package/dist/repl/process-worker.js +210 -0
  43. package/dist/repl/process-worker.js.map +1 -0
  44. package/dist/repl/processes.d.ts +282 -0
  45. package/dist/repl/processes.d.ts.map +1 -0
  46. package/dist/repl/processes.js +557 -0
  47. package/dist/repl/processes.js.map +1 -0
  48. package/dist/repl/render.d.ts +52 -2
  49. package/dist/repl/render.d.ts.map +1 -1
  50. package/dist/repl/render.js +52 -17
  51. package/dist/repl/render.js.map +1 -1
  52. package/dist/repl/session.d.ts +135 -3
  53. package/dist/repl/session.d.ts.map +1 -1
  54. package/dist/repl/session.js +413 -4
  55. package/dist/repl/session.js.map +1 -1
  56. package/dist/repl/status.d.ts +3 -1
  57. package/dist/repl/status.d.ts.map +1 -1
  58. package/dist/repl/status.js +4 -0
  59. package/dist/repl/status.js.map +1 -1
  60. package/dist/repl/tui/autocomplete.d.ts +19 -0
  61. package/dist/repl/tui/autocomplete.d.ts.map +1 -0
  62. package/dist/repl/tui/autocomplete.js +48 -0
  63. package/dist/repl/tui/autocomplete.js.map +1 -0
  64. package/dist/repl/tui/document.d.ts +51 -0
  65. package/dist/repl/tui/document.d.ts.map +1 -0
  66. package/dist/repl/tui/document.js +160 -0
  67. package/dist/repl/tui/document.js.map +1 -0
  68. package/dist/repl/tui/harness-view.d.ts +102 -0
  69. package/dist/repl/tui/harness-view.d.ts.map +1 -0
  70. package/dist/repl/tui/harness-view.js +323 -0
  71. package/dist/repl/tui/harness-view.js.map +1 -0
  72. package/dist/repl/tui/picker.d.ts +31 -0
  73. package/dist/repl/tui/picker.d.ts.map +1 -0
  74. package/dist/repl/tui/picker.js +43 -0
  75. package/dist/repl/tui/picker.js.map +1 -0
  76. package/dist/repl/tui/renderer.d.ts +98 -0
  77. package/dist/repl/tui/renderer.d.ts.map +1 -0
  78. package/dist/repl/tui/renderer.js +289 -0
  79. package/dist/repl/tui/renderer.js.map +1 -0
  80. package/dist/repl/tui/services-panel.d.ts +35 -0
  81. package/dist/repl/tui/services-panel.d.ts.map +1 -0
  82. package/dist/repl/tui/services-panel.js +108 -0
  83. package/dist/repl/tui/services-panel.js.map +1 -0
  84. package/dist/repl/tui/status-bar.d.ts +6 -2
  85. package/dist/repl/tui/status-bar.d.ts.map +1 -1
  86. package/dist/repl/tui/status-bar.js +82 -24
  87. package/dist/repl/tui/status-bar.js.map +1 -1
  88. package/dist/repl/tui/terminal.d.ts +28 -0
  89. package/dist/repl/tui/terminal.d.ts.map +1 -0
  90. package/dist/repl/tui/terminal.js +94 -0
  91. package/dist/repl/tui/terminal.js.map +1 -0
  92. package/dist/repl/tui/theme.d.ts +60 -0
  93. package/dist/repl/tui/theme.d.ts.map +1 -1
  94. package/dist/repl/tui/theme.js +142 -0
  95. package/dist/repl/tui/theme.js.map +1 -1
  96. package/dist/repl/tui/tool-pane.d.ts +68 -0
  97. package/dist/repl/tui/tool-pane.d.ts.map +1 -0
  98. package/dist/repl/tui/tool-pane.js +261 -0
  99. package/dist/repl/tui/tool-pane.js.map +1 -0
  100. package/dist/repl/tui/working-line.d.ts +23 -0
  101. package/dist/repl/tui/working-line.d.ts.map +1 -0
  102. package/dist/repl/tui/working-line.js +58 -0
  103. package/dist/repl/tui/working-line.js.map +1 -0
  104. package/dist/repl/worker-host.d.ts +80 -0
  105. package/dist/repl/worker-host.d.ts.map +1 -0
  106. package/dist/repl/worker-host.js +305 -0
  107. package/dist/repl/worker-host.js.map +1 -0
  108. package/dist/repl/worker-protocol.d.ts +123 -0
  109. package/dist/repl/worker-protocol.d.ts.map +1 -0
  110. package/dist/repl/worker-protocol.js +9 -0
  111. package/dist/repl/worker-protocol.js.map +1 -0
  112. package/dist/runner.d.ts +24 -0
  113. package/dist/runner.d.ts.map +1 -1
  114. package/dist/runner.js +47 -0
  115. package/dist/runner.js.map +1 -1
  116. package/dist/system/assets/docs/AGENT_PROMPT.md +193 -0
  117. package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
  118. package/dist/system/assets/docs/DML_REFERENCE.md +145 -0
  119. package/dist/types.d.ts +7 -0
  120. package/dist/types.d.ts.map +1 -1
  121. package/package.json +2 -1
  122. package/src/prolog-src/deepclause_mi.pl +516 -2
@@ -13,16 +13,95 @@ 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';
25
27
  let sessionCounter = 0;
28
+ /**
29
+ * One human, one editor: human-channel requests from the foreground and from any
30
+ * subagent are served strictly in order (see `requireInput`).
31
+ */
32
+ let sharedInputQueue = Promise.resolve();
33
+ /** Rebuild a `DMLEvent` from a persisted history record, or undefined. */
34
+ function historyEventFromRecord(record) {
35
+ const type = typeof record.type === 'string' ? record.type : undefined;
36
+ if (!type)
37
+ return undefined;
38
+ const frameKind = typeof record.frame === 'string' ? record.frame : undefined;
39
+ const content = typeof record.content === 'string' ? record.content : undefined;
40
+ switch (type) {
41
+ case 'stream':
42
+ return content ? { type, frameKind, content } : undefined;
43
+ case 'output':
44
+ case 'answer':
45
+ case 'error':
46
+ return { type, frameKind, content };
47
+ case 'log':
48
+ return { type, frameKind, content, level: record.level };
49
+ case 'tool_call':
50
+ return {
51
+ type,
52
+ frameKind,
53
+ toolName: typeof record.toolName === 'string' ? record.toolName : undefined,
54
+ toolState: record.toolState,
55
+ toolSummary: typeof record.toolSummary === 'string' ? record.toolSummary : undefined,
56
+ toolArgs: record.toolArgs,
57
+ };
58
+ case 'solution':
59
+ return {
60
+ type,
61
+ frameKind,
62
+ solution: {
63
+ bindings: (Array.isArray(record.bindings) ? record.bindings : []),
64
+ deterministic: false,
65
+ },
66
+ };
67
+ case 'task_activity':
68
+ return {
69
+ type,
70
+ frameKind,
71
+ taskState: record.taskState,
72
+ taskDescription: typeof record.taskDescription === 'string' ? record.taskDescription : undefined,
73
+ };
74
+ default:
75
+ return undefined;
76
+ }
77
+ }
78
+ /** The log-view line for an event, or undefined for per-tick noise. */
79
+ function processLogText(event) {
80
+ switch (event.type) {
81
+ case 'started':
82
+ return 'started';
83
+ case 'output':
84
+ case 'stream':
85
+ case 'reasoning':
86
+ case 'tool':
87
+ case 'model':
88
+ return event.content ?? '';
89
+ case 'answer': {
90
+ // A process's terminal result is its accumulated output; the actual
91
+ // answer/1 value is the last non-empty line, so use that (and avoid
92
+ // duplicating the whole transcript in one log row).
93
+ const text = String(event.answer ?? 'ok');
94
+ const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
95
+ return `› ${lines.length > 0 ? lines[lines.length - 1] : text}`;
96
+ }
97
+ case 'exited':
98
+ if (event.state === 'done')
99
+ return 'finished';
100
+ return `${event.state}${event.error ? ` (${event.error})` : event.reason && event.reason !== 'normal' ? ` (${String(event.reason)})` : ''}`;
101
+ default:
102
+ return undefined; // state/activity are shown in the summary view
103
+ }
104
+ }
26
105
  function nextSessionId() {
27
106
  sessionCounter += 1;
28
107
  return `repl_${Date.now().toString(36)}_${sessionCounter}`;
@@ -83,6 +162,23 @@ export class DMLSession {
83
162
  /** Full resolution for the backend, for the agent's environment block. */
84
163
  executionEnvironment;
85
164
  autoLoadHarness;
165
+ /** Original options, used to create per-process child sessions. */
166
+ openOptions;
167
+ /** Session-scoped subagents (spec §2). */
168
+ processBackend;
169
+ processBackendPromise;
170
+ /**
171
+ * The harness snapshot new processes should run. On the main session this is
172
+ * unset (children snapshot the live module); a worker host sets it to the
173
+ * snapshot the main sent, so its children never run a stale module.
174
+ */
175
+ processSnapshot;
176
+ processEventSink;
177
+ /** Bounded log of subagent output for the `/ps --watch --logs` view. */
178
+ processLogEntries = [];
179
+ static PROCESS_LOG_LIMIT = 500;
180
+ /** Service declarations replayed from the session record (resume only). */
181
+ recordedServices = [];
86
182
  /** Monotonic per-session turn counter used by history records. */
87
183
  turnCounter = 0;
88
184
  historyWarned = false;
@@ -109,6 +205,7 @@ export class DMLSession {
109
205
  this.executionBackend = options.executionBackend ?? 'clean-room';
110
206
  this.executionEnvironment = describeShellExecutionBackend(this.executionBackend === 'agentvm', this.executionBackend === 'agentvm' ? {} : { wrapper: this.executionBackend });
111
207
  this.autoLoadHarness = options.autoLoadHarness ?? true;
208
+ this.openOptions = options;
112
209
  this.compactionSettings = options.compaction?.settings;
113
210
  this.compactionAuto = options.compaction?.auto ?? false;
114
211
  this.contextWindow = options.contextWindow;
@@ -144,6 +241,7 @@ export class DMLSession {
144
241
  }
145
242
  const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
146
243
  snapshot = await store.readSessionHarness();
244
+ session.recordedServices = await store.readServiceRecords();
147
245
  if (!snapshot) {
148
246
  throw new Error(`session not found or has no saved harness: ${session.sessionId}`);
149
247
  }
@@ -191,6 +289,299 @@ export class DMLSession {
191
289
  getSessionId() {
192
290
  return this.sessionId;
193
291
  }
292
+ /**
293
+ * The live harness, for the side view: what predicates exist right now, how big
294
+ * each is, and what the open transaction has touched.
295
+ *
296
+ * Read from the session module rather than from the last commit, so a side view
297
+ * shows the harness *being built* — including uncommitted edits.
298
+ */
299
+ harnessView() {
300
+ const result = this.prologQuery(`deepclause_mi:repl_harness_view('${this.sessionId}', Rows)`);
301
+ const raw = toJsValue(result?.Rows);
302
+ const predicates = Array.isArray(raw)
303
+ ? raw
304
+ .map((row) => String(row).split('|'))
305
+ .filter((parts) => parts.length === 3)
306
+ .map(([indicator, clauses, flags]) => {
307
+ const [name, arityText] = indicator.split('/');
308
+ return {
309
+ indicator: indicator.trim(),
310
+ name: String(name).trim(),
311
+ arity: Number(arityText),
312
+ clauses: Number(clauses),
313
+ flags: String(flags).split(',').map((flag) => flag.trim()).filter(Boolean),
314
+ };
315
+ })
316
+ : [];
317
+ const versionResult = this.prologQuery(`( current_predicate(${this.sessionId}:harness_head/4)`
318
+ + ` -> ${this.sessionId}:harness_head(V, _, _, _) ; V = none )`);
319
+ const version = plainValue(versionResult?.V);
320
+ return {
321
+ predicates: predicates.sort((a, b) => a.indicator.localeCompare(b.indicator)),
322
+ version: version === 'none' ? undefined : Number(version),
323
+ transactionOpen: this.transactionBefore !== null,
324
+ touched: this.editTouched(),
325
+ lastCommit: this.lastTurnCommitted,
326
+ };
327
+ }
328
+ // ---- Session-scoped subagents (spec §2) --------------------------------
329
+ /** Register the sink that receives process lifecycle and output events. */
330
+ setProcessEventSink(sink) {
331
+ this.processEventSink = sink;
332
+ }
333
+ /**
334
+ * The process backend: in-process (L1) or worker-backed (L2). Created lazily
335
+ * so a session with no subagents pays nothing.
336
+ */
337
+ ensureProcessBackend() {
338
+ if (this.processBackendPromise)
339
+ return this.processBackendPromise;
340
+ const mode = this.openOptions.execution ?? 'worker';
341
+ this.processBackendPromise = (async () => {
342
+ const onEvent = (event) => {
343
+ this.processEventSink?.(event);
344
+ this.recordServiceEvent(event);
345
+ };
346
+ if (mode === 'worker') {
347
+ const { RemoteProcessHost } = await import('./worker-host.js');
348
+ const host = new RemoteProcessHost(this, { onEvent });
349
+ await host.start();
350
+ this.processBackend = host;
351
+ return host;
352
+ }
353
+ const executor = new DmlProcessExecutor(this, { onEvent });
354
+ this.processBackend = executor;
355
+ return executor;
356
+ })();
357
+ return this.processBackendPromise;
358
+ }
359
+ /** Record a process declaration/exit so a resumed session can replay it (§7). */
360
+ recordServiceEvent(event) {
361
+ this.recordProcessLog(event);
362
+ if (!this.history)
363
+ return;
364
+ if (event.type === 'started') {
365
+ const info = this.processBackend?.info(event.pid);
366
+ void this.history.appendServiceRecord({
367
+ type: 'declare',
368
+ at: event.at,
369
+ pid: event.pid,
370
+ name: event.name,
371
+ goal: info?.goal,
372
+ priority: info?.priority,
373
+ codeVersion: info?.codeVersion,
374
+ harnessVersion: info?.harnessVersion,
375
+ });
376
+ }
377
+ else if (event.type === 'exited') {
378
+ void this.history.appendServiceRecord({
379
+ type: 'exit',
380
+ at: event.at,
381
+ pid: event.pid,
382
+ name: event.name,
383
+ state: event.state,
384
+ reason: event.reason,
385
+ });
386
+ }
387
+ }
388
+ /** Service declarations replayed from the session record (resume only). */
389
+ recordedProcesses() {
390
+ return this.recordedServices;
391
+ }
392
+ /**
393
+ * Recent subagent output for the log view, oldest first. Answers and exits are
394
+ * included; per-tick state/activity updates are not (the summary shows those).
395
+ */
396
+ processLog(limit = 200) {
397
+ const entries = this.processLogEntries;
398
+ return limit >= entries.length ? [...entries] : entries.slice(entries.length - limit);
399
+ }
400
+ /**
401
+ * Events from the resumed session, to render it on load. Empty unless this
402
+ * session was opened with `resume` and history is enabled.
403
+ */
404
+ async resumedTranscript(limit = 400) {
405
+ if (!this.resume || !this.history)
406
+ return [];
407
+ const records = await this.history.readEvents(this.sessionId, limit);
408
+ return records
409
+ .map((record) => historyEventFromRecord(record))
410
+ .filter((event) => Boolean(event));
411
+ }
412
+ recordProcessLog(event) {
413
+ const text = processLogText(event);
414
+ if (text === undefined)
415
+ return;
416
+ // Coalesce streamed chunks into one growing line per process/kind.
417
+ const last = this.processLogEntries[this.processLogEntries.length - 1];
418
+ const streamy = event.type === 'stream' || event.type === 'reasoning';
419
+ if (streamy && last && last.kind === event.type && pidKey(last.pid) === pidKey(event.pid) && !last.done) {
420
+ last.text = (last.text + text).slice(-2000);
421
+ last.done = event.done;
422
+ return;
423
+ }
424
+ this.processLogEntries.push({
425
+ at: event.at,
426
+ pid: event.pid,
427
+ name: event.name,
428
+ kind: event.type,
429
+ text,
430
+ done: event.done,
431
+ });
432
+ if (this.processLogEntries.length > DMLSession.PROCESS_LOG_LIMIT) {
433
+ this.processLogEntries.splice(0, this.processLogEntries.length - DMLSession.PROCESS_LOG_LIMIT);
434
+ }
435
+ }
436
+ /** Start a session-scoped subagent running `goal`; returns immediately. */
437
+ async spawnProcess(goal, options = {}) {
438
+ return (await this.ensureProcessBackend()).spawn(goal, options);
439
+ }
440
+ /** The whole system state: every process this session owns. */
441
+ processes() {
442
+ return this.processBackend?.processes() ?? [];
443
+ }
444
+ processInfo(pid) {
445
+ return this.processBackend?.info(pid);
446
+ }
447
+ /**
448
+ * The harness source a process is running, for inspection. Accepts a pid or a
449
+ * registered name. Undefined when the process is unknown.
450
+ */
451
+ processCode(target) {
452
+ const pid = typeof target === 'string' ? this.processBackend?.whereis(target) : target;
453
+ return pid ? this.processBackend?.codeOf(pid) : undefined;
454
+ }
455
+ /** The version of the harness the session itself is currently editing. */
456
+ currentHarnessVersion() {
457
+ return this.processSnapshotFor().version;
458
+ }
459
+ /** Stop one process by pid or registered name. */
460
+ async stopProcess(target, reason = 'shutdown') {
461
+ await (await this.ensureProcessBackend()).stop(target, reason);
462
+ }
463
+ /** Resolve a registered process name. */
464
+ whereisProcess(name) {
465
+ return this.processBackend?.whereis(name);
466
+ }
467
+ /** Register a process under a session-scoped name. */
468
+ registerProcess(name, pid) {
469
+ this.processBackend?.register(name, pid);
470
+ }
471
+ /** Copy a message term into a process's mailbox. */
472
+ sendProcess(to, messageTerm) {
473
+ this.processBackend?.send(to, messageTerm);
474
+ }
475
+ /** Request/reply against a service process (library(service) protocol). */
476
+ async callProcess(to, messageTerm, timeoutMs = 300_000) {
477
+ return (await this.ensureProcessBackend()).callTerm(to, messageTerm, timeoutMs);
478
+ }
479
+ /** Resolve a process primitive for this (non-process) foreground run. */
480
+ async handleForeground(request, signal) {
481
+ return (await this.ensureProcessBackend()).foregroundHandle(request, signal);
482
+ }
483
+ /** Host tool descriptors, for a worker's proxy tools. */
484
+ toolDescriptors() {
485
+ return [...this.tools.entries()].map(([name, tool]) => ({
486
+ name,
487
+ description: tool.description,
488
+ parameters: tool.parameters,
489
+ }));
490
+ }
491
+ /** Run a model call on behalf of a worker's proxy backend. */
492
+ async completeModelRequest(request) {
493
+ if (!this.llmBackend)
494
+ throw new Error('this session has no LLM backend');
495
+ return this.llmBackend.complete(request);
496
+ }
497
+ /** Run a host tool on behalf of a worker's proxy tool. */
498
+ runHostTool(name, args, signal) {
499
+ const tool = this.tools.get(name);
500
+ if (!tool)
501
+ throw new Error(`unknown host tool: ${name}`);
502
+ return tool.execute(args, { signal });
503
+ }
504
+ /** Prompt the human on behalf of a worker's `input/2`. */
505
+ requestHumanInput(prompt) {
506
+ return this.requireInput(prompt);
507
+ }
508
+ // Accessors for building a worker's init data (see worker-host.ts).
509
+ workspacePathValue() {
510
+ return this.workspacePath;
511
+ }
512
+ modelId() {
513
+ return this.model;
514
+ }
515
+ providerId() {
516
+ return this.provider;
517
+ }
518
+ temperatureValue() {
519
+ return this.temperature;
520
+ }
521
+ maxTokensValue() {
522
+ return this.maxTokens;
523
+ }
524
+ toolPolicyValue() {
525
+ return this.toolPolicy;
526
+ }
527
+ /** The living module as loadable DML source, for a process snapshot. */
528
+ async harnessText() {
529
+ return this.harnessTextSync();
530
+ }
531
+ /** Synchronous form of `harnessText`, for spawn-time stamping. */
532
+ harnessTextSync() {
533
+ const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', '', Text)`);
534
+ return String(plainValue(result?.Text) ?? '');
535
+ }
536
+ /**
537
+ * The harness snapshot and its version for a process about to be spawned.
538
+ *
539
+ * A worker host holds the snapshot the main sent; the main computes it from
540
+ * the live module. Either way, every process carries the exact module it runs
541
+ * and a version that lets two processes be compared at a glance.
542
+ */
543
+ processSnapshotFor() {
544
+ if (this.processSnapshot)
545
+ return this.processSnapshot;
546
+ const text = this.harnessTextSync();
547
+ return { text, version: moduleVersion(text) };
548
+ }
549
+ /**
550
+ * Adopt a process snapshot pushed by the main session (worker hosts only).
551
+ * Keeps a worker's children on the code the user actually committed.
552
+ */
553
+ setProcessSnapshot(text, version) {
554
+ this.processSnapshot = { text, version: version ?? moduleVersion(text) };
555
+ }
556
+ /**
557
+ * A private child session carrying a snapshot of this harness (invariant C).
558
+ *
559
+ * Returns the session and, when the snapshot could not be loaded, the load
560
+ * error. It does **not** throw on a load failure: the child still runs its goal
561
+ * (a swarm item reports `failed`, say), whereas throwing would kill the process
562
+ * before it can report and leave a waiting parent hanging.
563
+ */
564
+ async createProcessSession() {
565
+ const text = this.processSnapshotFor().text;
566
+ const child = await DMLSession.open({
567
+ ...this.openOptions,
568
+ execution: 'inline',
569
+ sessionId: undefined,
570
+ resume: false,
571
+ history: undefined,
572
+ autoLoadHarness: false,
573
+ tools: this.tools,
574
+ toolPolicy: this.toolPolicy,
575
+ onInput: this.inputHandler,
576
+ onInputRequired: undefined,
577
+ });
578
+ let loadError;
579
+ if (text.trim()) {
580
+ const loaded = await child.loadCode(text);
581
+ loadError = loaded.error;
582
+ }
583
+ return { session: child, loadError };
584
+ }
194
585
  /**
195
586
  * Prolog output captured since the last drain.
196
587
  *
@@ -783,7 +1174,7 @@ export class DMLSession {
783
1174
  * The child frame uses the living scratch module and may itself call task()
784
1175
  * (RLM-style recursion); its harness-memory writes are discarded afterward.
785
1176
  */
786
- async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full') {
1177
+ async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full', process) {
787
1178
  const startedAt = Date.now();
788
1179
  const elapsed = () => Date.now() - startedAt;
789
1180
  const childDepth = this.frameDepth + 1;
@@ -814,7 +1205,7 @@ export class DMLSession {
814
1205
  let runError;
815
1206
  this.frameDepth = childDepth;
816
1207
  try {
817
- for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal }))) {
1208
+ for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal, process }))) {
818
1209
  onEvent?.(event);
819
1210
  if (event.type === 'solution') {
820
1211
  if (solutions.length < limit) {
@@ -1359,6 +1750,14 @@ export class DMLSession {
1359
1750
  if (this.disposed)
1360
1751
  return;
1361
1752
  this.disposed = true;
1753
+ if (this.processBackend) {
1754
+ try {
1755
+ await this.processBackend.shutdown();
1756
+ }
1757
+ catch {
1758
+ // teardown is best-effort
1759
+ }
1760
+ }
1362
1761
  if (this.history) {
1363
1762
  try {
1364
1763
  await this.persistSessionHarness(this.history.currentVersion);
@@ -1384,6 +1783,11 @@ export class DMLSession {
1384
1783
  : async () => undefined,
1385
1784
  signal: options.signal,
1386
1785
  onModelCall: () => this.reserveModelCall(),
1786
+ // A run that is not itself a process resolves primitives through the
1787
+ // process backend (local or worker); a process uses its own binding.
1788
+ onProcessRequest: options.process
1789
+ ? (request, signal) => options.process.handle(request, signal)
1790
+ : (request, signal) => this.handleForeground(request, signal),
1387
1791
  };
1388
1792
  }
1389
1793
  // ---- TypeScript compaction (ported from pi) ----
@@ -1511,7 +1915,12 @@ export class DMLSession {
1511
1915
  if (!this.inputHandler) {
1512
1916
  return undefined;
1513
1917
  }
1514
- return this.inputHandler(prompt, options);
1918
+ // Serialize human-channel requests across every session in this process: a
1919
+ // background subagent's input/2 must not clobber a foreground prompt (one
1920
+ // editor, one human). Child process sessions share this module-level queue.
1921
+ const value = sharedInputQueue.then(() => this.inputHandler(prompt, options));
1922
+ sharedInputQueue = value.then(() => undefined, () => undefined);
1923
+ return value;
1515
1924
  }
1516
1925
  /**
1517
1926
  * Reserve one model call for the current turn. When the budget is reached,