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.
Files changed (92) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/config.d.ts +2 -2
  3. package/dist/cli/repl.d.ts +36 -0
  4. package/dist/cli/repl.d.ts.map +1 -1
  5. package/dist/cli/repl.js +414 -14
  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 +547 -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 +44 -0
  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.map +1 -1
  49. package/dist/repl/render.js +19 -5
  50. package/dist/repl/render.js.map +1 -1
  51. package/dist/repl/session.d.ts +142 -3
  52. package/dist/repl/session.d.ts.map +1 -1
  53. package/dist/repl/session.js +435 -4
  54. package/dist/repl/session.js.map +1 -1
  55. package/dist/repl/tui/autocomplete.d.ts.map +1 -1
  56. package/dist/repl/tui/autocomplete.js +7 -0
  57. package/dist/repl/tui/autocomplete.js.map +1 -1
  58. package/dist/repl/tui/harness-view.d.ts +102 -0
  59. package/dist/repl/tui/harness-view.d.ts.map +1 -0
  60. package/dist/repl/tui/harness-view.js +323 -0
  61. package/dist/repl/tui/harness-view.js.map +1 -0
  62. package/dist/repl/tui/renderer.d.ts.map +1 -1
  63. package/dist/repl/tui/renderer.js +6 -1
  64. package/dist/repl/tui/renderer.js.map +1 -1
  65. package/dist/repl/tui/services-panel.d.ts +35 -0
  66. package/dist/repl/tui/services-panel.d.ts.map +1 -0
  67. package/dist/repl/tui/services-panel.js +108 -0
  68. package/dist/repl/tui/services-panel.js.map +1 -0
  69. package/dist/repl/tui/status-bar.d.ts +2 -0
  70. package/dist/repl/tui/status-bar.d.ts.map +1 -1
  71. package/dist/repl/tui/status-bar.js +2 -0
  72. package/dist/repl/tui/status-bar.js.map +1 -1
  73. package/dist/repl/tui/tool-pane.d.ts.map +1 -1
  74. package/dist/repl/tui/tool-pane.js +6 -2
  75. package/dist/repl/tui/tool-pane.js.map +1 -1
  76. package/dist/repl/worker-host.d.ts +80 -0
  77. package/dist/repl/worker-host.d.ts.map +1 -0
  78. package/dist/repl/worker-host.js +305 -0
  79. package/dist/repl/worker-host.js.map +1 -0
  80. package/dist/repl/worker-protocol.d.ts +123 -0
  81. package/dist/repl/worker-protocol.d.ts.map +1 -0
  82. package/dist/repl/worker-protocol.js +9 -0
  83. package/dist/repl/worker-protocol.js.map +1 -0
  84. package/dist/runner.d.ts +24 -0
  85. package/dist/runner.d.ts.map +1 -1
  86. package/dist/runner.js +58 -0
  87. package/dist/runner.js.map +1 -1
  88. package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
  89. package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
  90. package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
  91. package/package.json +7 -6
  92. package/src/prolog-src/deepclause_mi.pl +547 -2
package/dist/cli/repl.js CHANGED
@@ -15,7 +15,12 @@ import { HistoryStore } from '../repl/history.js';
15
15
  import { resolveReplJudgeSelection } from '../repl/judge.js';
16
16
  import { ReplRenderer } from '../repl/render.js';
17
17
  import { TranscriptDocument } from '../repl/tui/document.js';
18
+ import { ServicesPanel } from '../repl/tui/services-panel.js';
19
+ import { formatProcessCode, formatProcessTable } from '../repl/harness-tools.js';
18
20
  import { ChoicePicker } from '../repl/tui/picker.js';
21
+ import { openPiRuntime } from '../repl/backend.js';
22
+ import { listLoginProviders, loginToProvider } from '../repl/auth.js';
23
+ import { HarnessView } from '../repl/tui/harness-view.js';
19
24
  import { TuiRenderer } from '../repl/tui/renderer.js';
20
25
  import { WorkingLine } from '../repl/tui/working-line.js';
21
26
  import { createReplAutocomplete } from '../repl/tui/autocomplete.js';
@@ -23,7 +28,7 @@ import { initTuiTheme, scrollbarStyles } from '../repl/tui/theme.js';
23
28
  import { SizedTerminal } from '../repl/tui/terminal.js';
24
29
  import { ReplStatusTracker } from '../repl/status.js';
25
30
  import { StatusBarComponent } from '../repl/tui/status-bar.js';
26
- import { Container, ScrollView, Spacer, TuiAltScreen, VStack, matchesKey, } from '@earendil-works/pi-tui';
31
+ import { Container, HStack, ScrollView, Spacer, TuiAltScreen, VStack, matchesKey, } from '@earendil-works/pi-tui';
27
32
  import { editorTheme, formatInputAnswer } from '../repl/tui/theme.js';
28
33
  import { PromptEditor } from '../repl/tui/editor.js';
29
34
  import { loadProjectContext } from '../repl/project-context.js';
@@ -77,6 +82,13 @@ export function parseVizArgs(arg) {
77
82
  export function formatTurnInput(turn) {
78
83
  return turn.mode === 'goal' ? `/goal ${turn.request}` : turn.request;
79
84
  }
85
+ /**
86
+ * Slash commands are executed (or queued), never steered into a running model
87
+ * turn: `/send`, `/call`, `/ps`, `/stop` … address processes, not the model.
88
+ */
89
+ export function isSlashCommand(line) {
90
+ return line.trim().startsWith('/');
91
+ }
80
92
  /**
81
93
  * Classify one REPL input line.
82
94
  *
@@ -119,6 +131,9 @@ export function classifyReplInput(line) {
119
131
  return { kind: 'export', path: arg || undefined };
120
132
  case 'model':
121
133
  return { kind: 'model', model: arg || undefined };
134
+ case 'harness':
135
+ case 'side':
136
+ return { kind: 'harness', width: arg || undefined };
122
137
  case 'reasoning':
123
138
  case 'reasoning-level':
124
139
  return { kind: 'reasoning', level: arg || undefined };
@@ -144,6 +159,36 @@ export function classifyReplInput(line) {
144
159
  case 'gen':
145
160
  case 'imagine':
146
161
  return arg ? { kind: 'gen', prompt: arg } : { kind: 'unknown-command', name: 'gen (missing prompt)' };
162
+ case 'spawn':
163
+ case 'serve':
164
+ return arg ? { kind: 'spawn', goal: arg } : { kind: 'unknown-command', name: 'spawn (missing goal)' };
165
+ case 'ps':
166
+ case 'processes': {
167
+ const watch = /(?:^|\s)--watch(?:\s|$)/.test(arg);
168
+ const logs = /(?:^|\s)--logs(?:\s|$)/.test(arg);
169
+ // `--code` alone shows the current harness version; `--code <target>`
170
+ // prints the harness snapshot that process is running.
171
+ const codeMatch = /(?:^|\s)--code(?:=(\S+)|\s+(\S+))?/.exec(arg);
172
+ const code = codeMatch ? (codeMatch[1] ?? codeMatch[2] ?? '') : undefined;
173
+ return { kind: 'ps', watch, logs, code };
174
+ }
175
+ case 'send':
176
+ case 'cast': {
177
+ const [target, ...rest] = arg.split(/\s+/);
178
+ return target && rest.length > 0
179
+ ? { kind: 'send', target, message: rest.join(' ') }
180
+ : { kind: 'unknown-command', name: 'send (usage: /send <name> <message>)' };
181
+ }
182
+ case 'call': {
183
+ const [target, ...rest] = arg.split(/\s+/);
184
+ return target && rest.length > 0
185
+ ? { kind: 'call', target, message: rest.join(' ') }
186
+ : { kind: 'unknown-command', name: 'call (usage: /call <name> <message>)' };
187
+ }
188
+ case 'stop':
189
+ return arg ? { kind: 'stop', target: arg } : { kind: 'unknown-command', name: 'stop (usage: /stop <name>)' };
190
+ case 'login':
191
+ return { kind: 'login' };
147
192
  default:
148
193
  return { kind: 'unknown-command', name };
149
194
  }
@@ -456,6 +501,7 @@ export async function createReplRuntime(options) {
456
501
  modelResolver,
457
502
  history: historyOptions,
458
503
  harness: harnessName,
504
+ execution: options.execution,
459
505
  autoLoadHarness: !(options.load?.length),
460
506
  compaction: { settings: replCompaction, auto: replCompaction.auto },
461
507
  contextWindow,
@@ -484,7 +530,7 @@ export async function createReplRuntime(options) {
484
530
  + "or run 'deepclause set-backend pi'.";
485
531
  }
486
532
  }
487
- return {
533
+ const runtime = {
488
534
  repl,
489
535
  renderer,
490
536
  workspace,
@@ -511,6 +557,23 @@ export async function createReplRuntime(options) {
511
557
  setOnInput: (handler) => repl.setOnInput(handler),
512
558
  missingCredential,
513
559
  };
560
+ // Session-scoped subagents render their lifecycle and output through the
561
+ // active renderer (the TUI swaps it in after this runtime is built).
562
+ repl.session.setProcessEventSink((event) => {
563
+ const label = `⚙ ${event.name}`;
564
+ const sink = runtime.renderer;
565
+ if (event.type === 'output' && event.content) {
566
+ sink.writeLine(`${label} ${event.content.trimEnd()}`);
567
+ }
568
+ else if (event.type === 'answer') {
569
+ sink.writeLine(`${label} › ${String(event.answer ?? 'ok')}`);
570
+ }
571
+ else if (event.type === 'exited' && event.state !== 'done') {
572
+ const reason = event.reason && event.reason !== 'normal' ? ` (${String(event.reason)})` : '';
573
+ sink.writeLine(`${label} ${event.state}${reason}`);
574
+ }
575
+ });
576
+ return runtime;
514
577
  }
515
578
  /** Package version, read once from package.json for the banner/`--version`. */
516
579
  export const PACKAGE_VERSION = (() => {
@@ -562,13 +625,28 @@ async function printBanner(runtime, loaded) {
562
625
  if (harnessFresh) {
563
626
  lines.push('', ` ${bold('Two modes.')} Plain English authors the harness; ${accent('/goal')} runs DML directly.`, '', ` ${accent('<plain English>')} default. Ask for anything: add a new skill, add a tool,`, ` change existing logic, run or test code in the living`, ` module, or just ask a question about the harness. An authoring`, ` agent writes the DML, tests it with ${dim('execute')}, and commits it.`, ` ${accent('/goal <dml>')} run DML/Prolog directly against the current harness.`, '', ` ${bold('Example')}`, ` add a research(Question, Answer) skill that answers with task/2`, ` run research on "why is the sky blue?"`, ` A = "…"`, '', ` ...or run DML directly (${accent('/goal')}):`, ` ${accent('/goal')} research("why is the sky blue?", A).`, ` A = "…"`, '', ` When you're ready, export the harness and re-use it whenever you need:`, ` /export research.dml save a portable harness`, ` ${dim('$')} deepclause repl -l research.dml load it back any time`, ` ${dim('$')} deepclause repl --session last resume the last session`);
564
627
  }
565
- lines.push('', ` ${accent('/goal')} ${dim('<dml>')} run a DML goal, e.g. ${dim('/goal member(X, [1,2,3])')}`, ` ${accent('<plain English>')} author the harness`, '', ` ${accent('/help')} ${accent('/listing')} ${accent('/viz')} ${accent('/export')} ${accent('/freeze')} ${accent('/model')} ${accent('/halt')}`);
628
+ lines.push('', ` ${accent('/goal')} ${dim('<dml>')} run a DML goal, e.g. ${dim('/goal member(X, [1,2,3])')}`, ` ${accent('<plain English>')} author the harness`, ` ${accent('/spawn')} ${dim('<goal>')} start a session-scoped subagent`, ` ${accent('/ps')} ${dim('[--watch|--logs|--code <pid>]')} list subagents / inspect a snapshot`, ` ${accent('/send')} ${accent('/call')} ${accent('/stop')} message, call, or stop a subagent`, '', ` ${accent('/help')} ${accent('/listing')} ${accent('/viz')} ${accent('/export')} ${accent('/freeze')} ${accent('/model')} ${accent('/halt')}`);
566
629
  if (runtime.historyEnabled) {
567
630
  lines.push(` ${dim('resume later:')} deepclause repl --session ${runtime.sessionId}`);
568
631
  }
569
632
  lines.push('');
570
633
  runtime.renderer.writeLine(lines.join('\n'));
571
634
  }
635
+ /**
636
+ * Replay a resumed session's prior events so the transcript continues instead of
637
+ * starting blank. Bounded, and skipped when the session was not resumed or
638
+ * tracing was off.
639
+ */
640
+ async function renderResumedSession(runtime) {
641
+ const events = await runtime.repl.resumedTranscript().catch(() => []);
642
+ if (events.length === 0)
643
+ return;
644
+ runtime.renderer.writeLine('');
645
+ runtime.renderer.writeLine(`── resumed session ${runtime.sessionId} — runtime state was reset ──`);
646
+ for (const event of events)
647
+ runtime.renderer.render(event);
648
+ runtime.renderer.writeLine('');
649
+ }
572
650
  /** Dispatch one classified input. Returns false when the REPL should exit. */
573
651
  export async function dispatchReplInput(runtime, input, options = {}) {
574
652
  const { repl, renderer } = runtime;
@@ -577,6 +655,53 @@ export async function dispatchReplInput(runtime, input, options = {}) {
577
655
  case 'empty':
578
656
  case 'comment':
579
657
  return true;
658
+ case 'spawn': {
659
+ try {
660
+ const pid = await repl.session.spawnProcess(input.goal, input.name ? { name: input.name } : {});
661
+ print(`spawned ${formatProcessTable(repl.session.processes().filter((p) => p.pid.id === pid.id))}`);
662
+ }
663
+ catch (error) {
664
+ print(`ERROR: ${error.message}`);
665
+ }
666
+ return true;
667
+ }
668
+ case 'ps': {
669
+ if (input.code !== undefined) {
670
+ for (const line of formatProcessCode(repl.session, input.code).split('\n'))
671
+ print(line);
672
+ return true;
673
+ }
674
+ for (const line of formatProcessTable(repl.session.processes(), repl.session.currentHarnessVersion()).split('\n'))
675
+ print(line);
676
+ return true;
677
+ }
678
+ case 'send': {
679
+ try {
680
+ repl.session.sendProcess(input.target, input.message);
681
+ }
682
+ catch (error) {
683
+ print(`ERROR: ${error.message}`);
684
+ }
685
+ return true;
686
+ }
687
+ case 'call': {
688
+ try {
689
+ const reply = await repl.session.callProcess(input.target, input.message, 60_000);
690
+ print(`${input.target} › ${reply}`);
691
+ }
692
+ catch (error) {
693
+ print(`ERROR: ${error.message}`);
694
+ }
695
+ return true;
696
+ }
697
+ case 'stop': {
698
+ await repl.session.stopProcess(input.target);
699
+ print(`stopped ${input.target}`);
700
+ return true;
701
+ }
702
+ case 'login':
703
+ print("sign in from the terminal: deepclause login (or /login in the TUI)");
704
+ return true;
580
705
  case 'unknown-command':
581
706
  print(`unknown command: /${input.name} — try /help.`);
582
707
  return true;
@@ -755,6 +880,11 @@ function helpText() {
755
880
  ' question. An agent writes, tests, and commits the DML.',
756
881
  '',
757
882
  ' Commands',
883
+ ' /spawn <goal> start a session-scoped subagent (alias /serve)',
884
+ ' /ps [--watch|--logs|--code <pid>] list subagents; --watch pins a panel, --logs follows output, --code shows a process harness snapshot',
885
+ ' /send <name> <msg> send a DML term to a subagent (alias /cast)',
886
+ ' /call <name> <msg> request/reply with a service subagent',
887
+ ' /stop <name> stop a subagent',
758
888
  ' /listing [filter] inspect the harness (all | Name/Arity | tools | scripts)',
759
889
  ' /viz [entry] [--spec] diagram of a skill, drawn in the terminal',
760
890
  ' (--graphics=auto|kitty|iterm2|ascii|off)',
@@ -766,6 +896,7 @@ function helpText() {
766
896
  ' /compact [notes] compact the authoring memory',
767
897
  ' /export [file] save the harness (use .zip to bundle linked files)',
768
898
  ' /model [provider/id] show / set the model',
899
+ ' /login sign in to a provider (from the terminal or the TUI)',
769
900
  ' /freeze /unfreeze read-only lock',
770
901
  ' /params show session parameters',
771
902
  ' /trace /notrace toggle verbose logging',
@@ -1016,7 +1147,9 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1016
1147
  },
1017
1148
  });
1018
1149
  runtime.renderer = renderer;
1019
- const runState = { running: false, steerable: false };
1150
+ const runState = { running: false, steerable: false, bufferable: false };
1151
+ /** Slash commands typed during a turn/command, run when it finishes. */
1152
+ const pendingCommands = [];
1020
1153
  /** Lines typed during a goal query, consumed by the solution pager. */
1021
1154
  const bufferedInput = [];
1022
1155
  const statusBar = new StatusBarComponent({
@@ -1027,6 +1160,15 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1027
1160
  backend: runtime.backend,
1028
1161
  isFrozen: () => runtime.repl.isFrozen(),
1029
1162
  isRunning: () => runState.running,
1163
+ processLabel: () => {
1164
+ const active = runtime.repl.session
1165
+ .processes()
1166
+ .filter((process) => ['running', 'runnable', 'waiting', 'input'].includes(process.state));
1167
+ if (active.length === 0)
1168
+ return undefined;
1169
+ const attention = active.some((process) => process.state === 'input');
1170
+ return `⚙ ${active.length} svc${attention ? ' !' : ''}`;
1171
+ },
1030
1172
  });
1031
1173
  transcript.onChange = () => tui.requestRender();
1032
1174
  renderer.setImageSink((lines) => document.appendImageLines(lines));
@@ -1077,16 +1219,58 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1077
1219
  }
1078
1220
  else if (text.trim()) {
1079
1221
  // No pending ask means a turn is running.
1080
- if (runState.steerable) {
1222
+ const trimmed = text.trim();
1223
+ if (isSlashCommand(trimmed)) {
1224
+ // Slash commands are never steering. UI toggles and read-only
1225
+ // snapshots act now; everything else is queued for when the current
1226
+ // turn/command finishes, so /send, /call, /stop … reach the processes
1227
+ // instead of being injected into the model's conversation.
1228
+ const immediate = classifyReplInput(trimmed);
1229
+ transcript.append(trimmed, 'user');
1230
+ if (immediate.kind === 'ps') {
1231
+ if (immediate.logs) {
1232
+ const on = !(servicesVisible && servicesLogs);
1233
+ servicesLogs = on;
1234
+ servicesVisible = true;
1235
+ }
1236
+ else if (immediate.watch) {
1237
+ servicesVisible = !servicesVisible;
1238
+ }
1239
+ else if (immediate.code !== undefined) {
1240
+ for (const line of formatProcessCode(runtime.repl.session, immediate.code).split('\n')) {
1241
+ renderer.writeLine(line);
1242
+ }
1243
+ }
1244
+ else {
1245
+ for (const line of formatProcessTable(runtime.repl.session.processes(), runtime.repl.session.currentHarnessVersion()).split('\n')) {
1246
+ renderer.writeLine(line);
1247
+ }
1248
+ }
1249
+ tui.requestRender();
1250
+ }
1251
+ else if (immediate.kind === 'harness') {
1252
+ void handleHarnessCommand(immediate.width);
1253
+ }
1254
+ else {
1255
+ pendingCommands.push(trimmed);
1256
+ transcript.append(' (queued — runs when the current turn finishes)');
1257
+ }
1258
+ }
1259
+ else if (runState.steerable) {
1081
1260
  runtime.renderer.endTextBlock?.();
1082
1261
  transcript.append(text, 'steer');
1083
1262
  runtime.repl.steer(text);
1084
1263
  }
1085
- else {
1264
+ else if (runState.bufferable) {
1086
1265
  // A goal query is running: queue the line for the solution pager.
1087
1266
  bufferedInput.push(text);
1088
1267
  transcript.append(text, 'steer');
1089
1268
  }
1269
+ else {
1270
+ // A command (e.g. /call) is running and owns the prompt; typing does
1271
+ // not steer it. Say so rather than mislabeling the line as steering.
1272
+ transcript.append(' (a command is running — wait, or press Ctrl-C)');
1273
+ }
1090
1274
  }
1091
1275
  if (editor) {
1092
1276
  editor.setPrompt(null);
@@ -1144,6 +1328,10 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1144
1328
  if (!holder.turn)
1145
1329
  return false;
1146
1330
  holder.turn.abort(new Error('cancelled by user'));
1331
+ // Close the streamed block and clear the dock spinner so an interrupted turn
1332
+ // leaves no live line behind.
1333
+ runtime.renderer.endTextBlock?.();
1334
+ working.set(null);
1147
1335
  transcript.append('(interrupted — Ctrl-C exits)');
1148
1336
  tui.requestRender();
1149
1337
  return true;
@@ -1290,6 +1478,36 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1290
1478
  if (chosen)
1291
1479
  await apply(chosen);
1292
1480
  };
1481
+ /**
1482
+ * `/harness` toggles the side view; `/harness 40`, `wider` and `narrower` resize it.
1483
+ * The width is a layout basis, so a resize rebuilds the root.
1484
+ */
1485
+ const handleHarnessCommand = async (width) => {
1486
+ if (!width) {
1487
+ harnessVisible = !harnessVisible;
1488
+ applyLayout();
1489
+ if (harnessVisible)
1490
+ harnessView.refresh();
1491
+ tui.invalidate();
1492
+ tui.requestRender();
1493
+ renderer.writeLine(`harness view ${harnessVisible ? 'on' : 'off'}${harnessVisible ? ` (${harnessWidth} columns)` : ''}`);
1494
+ return;
1495
+ }
1496
+ const requested = width === 'wider' ? harnessWidth + 8
1497
+ : width === 'narrower' ? harnessWidth - 8
1498
+ : Number(width);
1499
+ if (!Number.isFinite(requested) || requested < 20 || requested > 120) {
1500
+ renderer.writeLine(`harness view width must be 20-120 columns (got '${width}')`);
1501
+ return;
1502
+ }
1503
+ harnessWidth = Math.round(requested);
1504
+ if (!harnessVisible)
1505
+ harnessVisible = true;
1506
+ applyLayout();
1507
+ tui.invalidate();
1508
+ tui.requestRender();
1509
+ renderer.writeLine(`harness view ${harnessWidth} columns`);
1510
+ };
1293
1511
  const pickModel = async () => {
1294
1512
  const catalog = runtime.modelResolver?.catalog() ?? [];
1295
1513
  if (catalog.length === 0) {
@@ -1316,6 +1534,121 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1316
1534
  await persistSetting({ backend: { kind: 'pi', model: picked } }, 'model');
1317
1535
  renderer.writeLine(`default model set to ${picked} (takes effect on the next start)`);
1318
1536
  };
1537
+ /** `/login`: sign in to a provider from inside the TUI. */
1538
+ const runLogin = async () => {
1539
+ let handle;
1540
+ try {
1541
+ handle = await openPiRuntime({});
1542
+ }
1543
+ catch (error) {
1544
+ renderer.writeLine(`login unavailable: ${error instanceof Error ? error.message : String(error)}`);
1545
+ return;
1546
+ }
1547
+ const providers = listLoginProviders(handle);
1548
+ if (providers.length === 0) {
1549
+ renderer.writeLine('no login providers available');
1550
+ return;
1551
+ }
1552
+ const picked = await choose((done) => new ChoicePicker({
1553
+ title: 'Sign in to a provider',
1554
+ hint: 'Esc to cancel',
1555
+ items: providers.map((provider) => ({
1556
+ value: provider.id,
1557
+ label: `${provider.name} (${provider.id})`,
1558
+ description: provider.configured ? '✓ configured' : provider.authType,
1559
+ })),
1560
+ onSelect: (item) => done(item.value),
1561
+ onCancel: () => done(),
1562
+ }));
1563
+ if (!picked)
1564
+ return;
1565
+ const interaction = {
1566
+ prompt: async (prompt) => {
1567
+ if (prompt.options && prompt.options.length > 0) {
1568
+ const method = await choose((done) => new ChoicePicker({
1569
+ title: prompt.message,
1570
+ items: prompt.options.map((option) => ({
1571
+ value: option.id,
1572
+ label: option.label,
1573
+ description: option.description,
1574
+ })),
1575
+ onSelect: (item) => done(item.value),
1576
+ onCancel: () => done(),
1577
+ }));
1578
+ if (!method)
1579
+ throw new Error('cancelled');
1580
+ return method;
1581
+ }
1582
+ const answer = await ask(`${prompt.message}${prompt.placeholder ? ` (${prompt.placeholder})` : ''} `);
1583
+ if (answer === null)
1584
+ throw new Error('cancelled');
1585
+ return answer;
1586
+ },
1587
+ notify: (event) => {
1588
+ if (event.type === 'auth_url' && event.url)
1589
+ renderer.writeLine(`open this URL to authorize: ${event.url}`);
1590
+ else if (event.type === 'device_code')
1591
+ renderer.writeLine(`visit ${event.verificationUri} and enter code ${event.userCode}`);
1592
+ else if (event.message)
1593
+ renderer.writeLine(event.message);
1594
+ },
1595
+ };
1596
+ try {
1597
+ await loginToProvider(handle, picked, { interaction });
1598
+ await persistSetting({ backend: { kind: 'pi' } }, 'login');
1599
+ renderer.writeLine(`signed in to ${picked} (run /model to pick one of its models)`);
1600
+ }
1601
+ catch (error) {
1602
+ renderer.writeLine(`login failed: ${error instanceof Error ? error.message : String(error)}`);
1603
+ }
1604
+ };
1605
+ // The experiment: a live side view of the harness, beside the transcript. It reads
1606
+ // the session module (so uncommitted edits show) and refreshes after anything that
1607
+ // can change it. Hidden on a narrow terminal, where there is no room for a column.
1608
+ const harnessView = new HarnessView(() => runtime.repl.session.harnessView(), {
1609
+ // The view asks; the host fetches (the listing query is async) and hands it back.
1610
+ requestSource: (indicator) => {
1611
+ void runtime.repl.session.listing(indicator).then((text) => {
1612
+ harnessView.setSource(indicator, text.split('\n'));
1613
+ tui.requestRender();
1614
+ });
1615
+ },
1616
+ // Escape with nothing drilled into: hand the keyboard back to the editor.
1617
+ onExit: () => {
1618
+ if (editor)
1619
+ tui.setFocus(editor);
1620
+ tui.requestRender();
1621
+ },
1622
+ });
1623
+ harnessView.refresh();
1624
+ let harnessVisible = options.harnessView ?? false;
1625
+ let servicesVisible = false;
1626
+ let servicesLogs = false;
1627
+ const servicesPanel = new ServicesPanel({
1628
+ getProcesses: () => runtime.repl.session.processes(),
1629
+ getLog: () => runtime.repl.session.processLog(200),
1630
+ isVisible: () => servicesVisible,
1631
+ mode: () => (servicesLogs ? 'logs' : 'summary'),
1632
+ currentHarnessVersion: () => runtime.repl.session.currentHarnessVersion(),
1633
+ });
1634
+ const refreshHarness = () => {
1635
+ if (!harnessVisible)
1636
+ return;
1637
+ harnessView.refresh();
1638
+ tui.requestRender();
1639
+ };
1640
+ // Poll while the view is open. It is meant to be watched, and a change has to be
1641
+ // visible when it happens, not at the next turn boundary.
1642
+ const harnessTimer = setInterval(refreshHarness, 600);
1643
+ const servicesTimer = setInterval(() => {
1644
+ if (!servicesVisible)
1645
+ return;
1646
+ const active = runtime.repl.session
1647
+ .processes()
1648
+ .some((process) => ['running', 'runnable', 'waiting', 'input'].includes(process.state));
1649
+ if (active || servicesLogs)
1650
+ tui.requestRender();
1651
+ }, 600);
1319
1652
  // Transcript grows and scrolls; everything below it stays put.
1320
1653
  const scroll = new ScrollView(document, {
1321
1654
  follow: 'end',
@@ -1330,23 +1663,51 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1330
1663
  // do not consume scrollback.
1331
1664
  const dock = new VStack([
1332
1665
  { component: working, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
1666
+ { component: servicesPanel, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
1333
1667
  { component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1334
1668
  { component: editorContainer, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1335
1669
  { component: statusBar, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1336
1670
  ]);
1337
- tui.setLayoutRoot(new VStack([
1338
- { component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1339
- { component: scroll, basis: 0, grow: 1, shrink: 1, minSize: 1 },
1340
- { component: dock, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1341
- ]));
1671
+ // Rebuilt rather than mutated: a stack entry's basis is read from the entries given
1672
+ // at construction, so resizing means handing the TUI a new root.
1673
+ let harnessWidth = 34;
1674
+ const applyLayout = () => {
1675
+ const columns = new HStack([
1676
+ { component: scroll, basis: 0, grow: 1, shrink: 1, minSize: 20 },
1677
+ {
1678
+ component: harnessView,
1679
+ basis: harnessWidth,
1680
+ grow: 0,
1681
+ shrink: 0,
1682
+ minSize: 20,
1683
+ visible: (viewport) => harnessVisible && viewport.width >= 96,
1684
+ },
1685
+ ], { gap: 1 });
1686
+ tui.setLayoutRoot(new VStack([
1687
+ { component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1688
+ { component: columns, basis: 0, grow: 1, shrink: 1, minSize: 1 },
1689
+ { component: dock, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1690
+ ]));
1691
+ };
1692
+ applyLayout();
1342
1693
  tui.start();
1343
1694
  await printBanner(runtime, loaded);
1695
+ await renderResumedSession(runtime);
1344
1696
  tui.requestRender();
1345
1697
  try {
1346
1698
  while (true) {
1347
- const line = await ask(runtime.promptPrefix);
1348
- if (line === null)
1349
- break;
1699
+ // A slash command typed while a turn/command was running: run it now that
1700
+ // the REPL is idle, before asking for fresh input.
1701
+ let line;
1702
+ if (pendingCommands.length > 0) {
1703
+ line = pendingCommands.shift();
1704
+ tui.requestRender();
1705
+ }
1706
+ else {
1707
+ line = await ask(runtime.promptPrefix);
1708
+ if (line === null)
1709
+ break;
1710
+ }
1350
1711
  const input = classifyReplInput(line);
1351
1712
  if (input.kind === 'halt')
1352
1713
  break;
@@ -1364,10 +1725,32 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1364
1725
  await pickReasoningMode();
1365
1726
  continue;
1366
1727
  }
1728
+ if (input.kind === 'harness') {
1729
+ await handleHarnessCommand(input.width);
1730
+ continue;
1731
+ }
1732
+ if (input.kind === 'login') {
1733
+ await runLogin();
1734
+ continue;
1735
+ }
1736
+ if (input.kind === 'ps' && (input.watch || input.logs)) {
1737
+ if (input.logs) {
1738
+ // Toggle the follow-the-log view; it is always visible when on.
1739
+ const turningOn = !(servicesVisible && servicesLogs);
1740
+ servicesLogs = turningOn;
1741
+ servicesVisible = true;
1742
+ }
1743
+ else {
1744
+ servicesVisible = !servicesVisible;
1745
+ }
1746
+ tui.requestRender();
1747
+ continue;
1748
+ }
1367
1749
  const controller = new AbortController();
1368
1750
  holder.turn = controller;
1369
1751
  runState.running = true;
1370
1752
  runState.steerable = input.kind === 'prompt';
1753
+ runState.bufferable = input.kind === 'goal';
1371
1754
  bufferedInput.length = 0;
1372
1755
  tui.requestRender();
1373
1756
  try {
@@ -1395,6 +1778,8 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1395
1778
  tui.stop();
1396
1779
  process.off('SIGINT', exitRepl);
1397
1780
  process.stdout.off('resize', onResize);
1781
+ clearInterval(harnessTimer);
1782
+ clearInterval(servicesTimer);
1398
1783
  working.stop();
1399
1784
  // Restore stdout so the exit footer and any later output are visible.
1400
1785
  runtime.renderer.setSink((text) => process.stdout.write(text), (text) => process.stderr.write(text));
@@ -1417,6 +1802,9 @@ export async function startRepl(options) {
1417
1802
  channel: message.channel,
1418
1803
  });
1419
1804
  }
1805
+ if (options.execution === 'worker') {
1806
+ runtime.renderer.writeLine('note: subagents run in a worker thread (--execution worker)');
1807
+ }
1420
1808
  }
1421
1809
  catch (error) {
1422
1810
  process.stderr.write(`ERROR: ${error.message}\n`);
@@ -1494,6 +1882,7 @@ export async function startRepl(options) {
1494
1882
  return await runTui(runtime, options, saveHarnessOnExit, options.load ?? []);
1495
1883
  }
1496
1884
  await printBanner(runtime, options.load ?? []);
1885
+ await renderResumedSession(runtime);
1497
1886
  return await runInteractive(runtime, options, saveHarnessOnExit);
1498
1887
  }
1499
1888
  finally {
@@ -1553,6 +1942,14 @@ export function registerReplCommand(program) {
1553
1942
  .option('--no-tools', 'register no capability host tools')
1554
1943
  .option('--sandbox', 'run shell tools in AgentVM')
1555
1944
  .option('--require-sandbox', 'refuse to run unless the shell is sandboxed (bwrap/AgentVM/sandbox-exec)')
1945
+ .option('--harness-view', 'open the live harness side view (experiment)')
1946
+ .option('--execution <mode>', 'process execution mode: worker (L2, default) or inline (L1)', (value) => {
1947
+ if (value !== 'inline' && value !== 'worker') {
1948
+ throw new Error(`invalid --execution: ${value} (use inline or worker)`);
1949
+ }
1950
+ return value;
1951
+ })
1952
+ .option('--workers <n>', 'worker pool size for --execution worker', parseInt)
1556
1953
  .option('--no-ask', 'make ask_user / input/2 fail instead of prompting')
1557
1954
  .option('--image-model <provider:id>', 'image model for generation (default: first available)')
1558
1955
  .option('--no-open', 'do not hand images to the OS viewer on terminals without inline images')
@@ -1598,6 +1995,9 @@ export function registerReplCommand(program) {
1598
1995
  noTools: options.tools === false,
1599
1996
  sandbox: options.sandbox,
1600
1997
  requireSandbox: options.requireSandbox,
1998
+ harnessView: options.harnessView,
1999
+ execution: options.execution,
2000
+ workers: options.workers,
1601
2001
  noAsk: options.ask === false,
1602
2002
  imageModel: options.imageModel,
1603
2003
  open: options.open,