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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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,
|