@getpipher/armory-fleet 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/engine/run-registry.ts +5 -0
- package/src/engine/spawnSubagent.ts +32 -0
- package/src/index.ts +3 -0
- package/src/panel/fleet-panel.ts +79 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/engine/run-registry.ts
|
|
2
2
|
import type { FleetRunStatus } from "../todo-sync/port.ts";
|
|
3
|
+
import type { LiveSessionHandle } from "./spawnSubagent.ts";
|
|
3
4
|
|
|
4
5
|
export interface RunRecord {
|
|
5
6
|
runId: string;
|
|
@@ -22,6 +23,10 @@ export interface RunRecord {
|
|
|
22
23
|
forkedFrom?: string;
|
|
23
24
|
/** SPEC-5b-2: cumulative real tokens (input+output+cacheRead+cacheWrite) — live, updated on each message_end. */
|
|
24
25
|
tokenTotal?: number;
|
|
26
|
+
/** SPEC-5b-4: live session handle while status === "running"; cleared by finishRun.
|
|
27
|
+
* Transient, in-memory only — never written to RunLog (the journal append constructs
|
|
28
|
+
* a plain object, not RunRecord). */
|
|
29
|
+
session?: LiveSessionHandle;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/** runId format: fl-<base36 ms>-<6 random> (SPEC-1 §5.1). */
|
|
@@ -43,6 +43,33 @@ export interface ChildSession {
|
|
|
43
43
|
subscribe(handler: (event: ChildSessionEvent) => void): () => void;
|
|
44
44
|
abort(): Promise<void>;
|
|
45
45
|
dispose(): void;
|
|
46
|
+
/** SPEC-5b-4: optional mid-run steering. Pi backend forwards the native SDK `steer()`; claude omits. */
|
|
47
|
+
steer?(text: string): Promise<void>;
|
|
48
|
+
/** SPEC-5b-4: optional live streaming flag. Pi backend forwards the native SDK getter; claude omits. */
|
|
49
|
+
readonly isStreaming?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** SPEC-5b-4: narrow live-session handle retained on RunRecord while status === "running".
|
|
53
|
+
* Exposes only redirect/cancel/observe/liveness — deliberately no `prompt` or `dispose`
|
|
54
|
+
* (the panel must not start new prompts or tear down the session). */
|
|
55
|
+
export interface LiveSessionHandle {
|
|
56
|
+
steer(text: string): Promise<void>;
|
|
57
|
+
abort(): Promise<void>;
|
|
58
|
+
subscribe(handler: (e: ChildSessionEvent) => void): () => void;
|
|
59
|
+
readonly isStreaming: boolean;
|
|
60
|
+
readonly supportsSteer: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** SPEC-5b-4: wrap a ChildSession into a narrow LiveSessionHandle for the panel.
|
|
64
|
+
* `supportsSteer` is derived from whether the backend implemented the optional `steer`. */
|
|
65
|
+
export function toLiveHandle(session: ChildSession): LiveSessionHandle {
|
|
66
|
+
return {
|
|
67
|
+
steer: (text) => session.steer ? session.steer(text) : Promise.reject(new Error("steer not supported on this backend")),
|
|
68
|
+
abort: () => session.abort(),
|
|
69
|
+
subscribe: (h) => session.subscribe(h),
|
|
70
|
+
get isStreaming() { return session.isStreaming ?? false; },
|
|
71
|
+
get supportsSteer() { return typeof session.steer === "function"; },
|
|
72
|
+
};
|
|
46
73
|
}
|
|
47
74
|
|
|
48
75
|
export interface ChildSessionOpts {
|
|
@@ -193,6 +220,10 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
193
220
|
visionPort,
|
|
194
221
|
});
|
|
195
222
|
|
|
223
|
+
// SPEC-5b-4: retain a narrow live-session handle on the run record so the panel can
|
|
224
|
+
// steer/abort mid-flight. Cleared by finishRun in the same patch as the terminal status.
|
|
225
|
+
opts.runRegistry.update(runId, { session: toLiveHandle(session) });
|
|
226
|
+
|
|
196
227
|
const budget = createTurnBudget(maxTurns);
|
|
197
228
|
let finalText = "";
|
|
198
229
|
let tokenTotal = 0;
|
|
@@ -281,6 +312,7 @@ async function finishRun(
|
|
|
281
312
|
opts.runRegistry.update(runId, {
|
|
282
313
|
status, endedAt, resultSummary: finalText.slice(0, 120),
|
|
283
314
|
resumedFrom: opts.resumeLink, forkedFrom: opts.forkLink,
|
|
315
|
+
session: undefined, // SPEC-5b-4: clear the live handle (invariant: session ⟺ running)
|
|
284
316
|
});
|
|
285
317
|
try {
|
|
286
318
|
opts.runLog?.append(runId, {
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,9 @@ function wrapPiSession(inner: ChildSession, backendSessionId: string): ChildSess
|
|
|
61
61
|
handler({ type: "session_init", backendSessionId });
|
|
62
62
|
return inner.subscribe(handler);
|
|
63
63
|
},
|
|
64
|
+
// SPEC-5b-4: forward the native SDK steer + isStreaming to the real pi session.
|
|
65
|
+
steer: (t) => inner.steer ? inner.steer(t) : Promise.reject(new Error("pi session has no steer")),
|
|
66
|
+
get isStreaming() { return inner.isStreaming ?? false; },
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
|
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -91,6 +91,9 @@ export class FleetPanel extends Container {
|
|
|
91
91
|
private runTimeline: RunLogEvent[] | null = null;
|
|
92
92
|
private resumeInput: Input | null = null;
|
|
93
93
|
private resumeMode = false;
|
|
94
|
+
// SPEC-5b-4: Steer inline input state (mid-run redirect; mirrors resumeMode/resumeInput).
|
|
95
|
+
private steerInput: Input | null = null;
|
|
96
|
+
private steerMode = false;
|
|
94
97
|
// SPEC-5b-3: full-message overlay (second level over the 5b-1 timeline) + stored SelectList refs
|
|
95
98
|
// so handleInput can forward keys to the active overlay (Container/TUI routes input only to the
|
|
96
99
|
// focused component = this panel; children receive keys only if we forward them).
|
|
@@ -289,6 +292,11 @@ export class FleetPanel extends Container {
|
|
|
289
292
|
this.addChild(tl);
|
|
290
293
|
}
|
|
291
294
|
this.addChild(new Text(this.theme.fg("dim", " enter:Full-message esc:Back"), 0, 0));
|
|
295
|
+
} else if (this.steerMode && this.steerInput) {
|
|
296
|
+
// SPEC-5b-4: Fleet tab — mid-run steer input.
|
|
297
|
+
this.addChild(new Text(this.theme.fg("accent", " steer> "), 0, 0));
|
|
298
|
+
this.addChild(this.steerInput);
|
|
299
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
292
300
|
} else if (this.resumeMode && this.resumeInput) {
|
|
293
301
|
// SPEC-5b-1: Runs tab — resume follow-up input.
|
|
294
302
|
this.addChild(new Text(this.theme.fg("accent", " follow-up> "), 0, 0));
|
|
@@ -328,7 +336,7 @@ export class FleetPanel extends Container {
|
|
|
328
336
|
: this.lcRevising
|
|
329
337
|
? " enter:Submit-feedback esc:Cancel"
|
|
330
338
|
: this.view === "fleet"
|
|
331
|
-
? " r:Run-new s:Stop o:Open-todo tab:Lifecycle q:Quit"
|
|
339
|
+
? " r:Run-new s:Steer x:Stop o:Open-todo tab:Lifecycle q:Quit"
|
|
332
340
|
: this.view === "lifecycle"
|
|
333
341
|
? " r:Run-lifecycle i:Info tab:Runs q:Quit"
|
|
334
342
|
: this.view === "runs"
|
|
@@ -418,6 +426,8 @@ export class FleetPanel extends Container {
|
|
|
418
426
|
this.fullMessageEvent = null;
|
|
419
427
|
this.timelineList = null;
|
|
420
428
|
this.messageBodyList = null;
|
|
429
|
+
this.steerMode = false; // SPEC-5b-4: drop any in-flight steer input on tab switch
|
|
430
|
+
this.steerInput = null;
|
|
421
431
|
this.list = this.buildList();
|
|
422
432
|
this.renderShell();
|
|
423
433
|
}
|
|
@@ -453,6 +463,12 @@ export class FleetPanel extends Container {
|
|
|
453
463
|
this.invalidate();
|
|
454
464
|
return;
|
|
455
465
|
}
|
|
466
|
+
if (this.steerMode && this.steerInput) {
|
|
467
|
+
if (matchesKey(data, "escape")) { this.cancelSteer(); return; }
|
|
468
|
+
this.steerInput.handleInput(data);
|
|
469
|
+
this.invalidate();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
456
472
|
if (this.resumeMode && this.resumeInput) {
|
|
457
473
|
if (matchesKey(data, "escape")) { this.cancelResume(); return; }
|
|
458
474
|
this.resumeInput.handleInput(data);
|
|
@@ -509,6 +525,11 @@ export class FleetPanel extends Container {
|
|
|
509
525
|
this.renderShell();
|
|
510
526
|
return;
|
|
511
527
|
}
|
|
528
|
+
// SPEC-5b-4: Fleet view — s:Steer (pi-only) + x:Stop (any backend) on the selected running row.
|
|
529
|
+
if (this.view === "fleet") {
|
|
530
|
+
if (matchesKey(data, "s")) { this.startSteer(); return; }
|
|
531
|
+
if (matchesKey(data, "x")) { this.executeStop(); return; }
|
|
532
|
+
}
|
|
512
533
|
// SPEC-5b-1: Runs view — enter/i:Replay R:Resume F:Fork
|
|
513
534
|
if (this.view === "runs" && this.deps.runLog) {
|
|
514
535
|
if (matchesKey(data, "enter") || matchesKey(data, "i")) {
|
|
@@ -661,6 +682,63 @@ export class FleetPanel extends Container {
|
|
|
661
682
|
this.renderShell();
|
|
662
683
|
}
|
|
663
684
|
|
|
685
|
+
/** SPEC-5b-4: Steer — open the inline "steer> " input for the selected running row (pi-only). */
|
|
686
|
+
private startSteer(): void {
|
|
687
|
+
const sel = this.list.getSelectedItem();
|
|
688
|
+
if (!sel) return;
|
|
689
|
+
const run = this.deps.runRegistry.get(sel.value);
|
|
690
|
+
if (!run || run.status !== "running") { this.onNotify("run already finished", "warning"); return; }
|
|
691
|
+
if (!run.session) { this.onNotify("run already finished", "warning"); return; }
|
|
692
|
+
if (!run.session.supportsSteer) { this.onNotify("steer not supported on claude backend", "warning"); return; }
|
|
693
|
+
this.steerInput = new Input();
|
|
694
|
+
this.steerInput.onSubmit = (text: string) => {
|
|
695
|
+
if (!text.trim()) { this.cancelSteer(); return; }
|
|
696
|
+
void this.executeSteer(run.runId, text.trim());
|
|
697
|
+
};
|
|
698
|
+
this.steerInput.onEscape = () => this.cancelSteer();
|
|
699
|
+
this.steerMode = true;
|
|
700
|
+
this.renderShell();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private cancelSteer(): void {
|
|
704
|
+
this.steerMode = false;
|
|
705
|
+
this.steerInput = null;
|
|
706
|
+
this.renderShell();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private async executeSteer(runId: string, text: string): Promise<void> {
|
|
710
|
+
// Re-check: the run may have finished between pressing s and submitting.
|
|
711
|
+
const run = this.deps.runRegistry.get(runId);
|
|
712
|
+
if (!run || !run.session) { this.onNotify("run already finished", "warning"); this.cancelSteer(); return; }
|
|
713
|
+
if (!run.session.supportsSteer) { this.onNotify("steer not supported on claude backend", "warning"); this.cancelSteer(); return; }
|
|
714
|
+
this.steerMode = false;
|
|
715
|
+
this.steerInput = null;
|
|
716
|
+
this.renderShell();
|
|
717
|
+
try {
|
|
718
|
+
await run.session.steer(text);
|
|
719
|
+
this.onNotify("steer queued; lands after current tool calls", "info");
|
|
720
|
+
} catch (e) {
|
|
721
|
+
this.onNotify(`steer failed: ${(e as Error).message}`, "error");
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** SPEC-5b-4: Stop — abort the selected running row (any backend). */
|
|
726
|
+
private executeStop(): void {
|
|
727
|
+
const sel = this.list.getSelectedItem();
|
|
728
|
+
if (!sel) return;
|
|
729
|
+
const run = this.deps.runRegistry.get(sel.value);
|
|
730
|
+
if (!run || run.status !== "running") { this.onNotify("run already finished", "warning"); return; }
|
|
731
|
+
if (!run.session) { this.onNotify("run already finished", "warning"); return; }
|
|
732
|
+
void (async () => {
|
|
733
|
+
try {
|
|
734
|
+
await run.session!.abort();
|
|
735
|
+
this.onNotify("run aborted", "info");
|
|
736
|
+
} catch (e) {
|
|
737
|
+
this.onNotify(`abort failed: ${(e as Error).message}`, "error");
|
|
738
|
+
}
|
|
739
|
+
})();
|
|
740
|
+
}
|
|
741
|
+
|
|
664
742
|
private async executeResume(prior: RunMeta, followUp: string): Promise<void> {
|
|
665
743
|
this.resumeMode = false;
|
|
666
744
|
this.resumeInput = null;
|