pi-crew 0.6.3 → 0.7.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.
- package/CHANGELOG.md +84 -0
- package/README.md +80 -1
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -1
- package/src/extension/knowledge-injection.ts +71 -0
- package/src/extension/pi-api.ts +47 -0
- package/src/extension/register.ts +32 -1
- package/src/extension/registration/commands.ts +65 -1
- package/src/extension/registration/compaction-guard.ts +154 -14
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/registration/team-tool.ts +18 -11
- package/src/extension/team-tool/handle-settings.ts +57 -0
- package/src/extension/team-tool/inspect.ts +4 -1
- package/src/extension/team-tool/plan.ts +8 -1
- package/src/extension/team-tool/run.ts +4 -3
- package/src/extension/team-tool-types.ts +2 -0
- package/src/runtime/intercom-bridge.ts +5 -1
- package/src/runtime/replace.ts +555 -0
- package/src/runtime/resilient-edit.ts +166 -0
- package/src/runtime/single-agent-compose.ts +87 -0
- package/src/runtime/team-runner.ts +23 -6
- package/src/schema/team-tool-schema.ts +6 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/state/usage.ts +73 -0
- package/src/ui/card-colors.ts +126 -0
- package/src/ui/deploy-bundled-themes.ts +71 -0
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/render-diff.ts +37 -3
- package/src/ui/settings-overlay.ts +70 -7
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/syntax-highlight.ts +42 -23
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/theme-discovery.ts +188 -0
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +640 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/guards.ts +110 -0
- package/themes/crew-catppuccin-latte.json +96 -0
- package/themes/crew-catppuccin-mocha.json +93 -0
- package/themes/crew-dark.json +90 -0
- package/themes/crew-dracula.json +83 -0
- package/themes/crew-gruvbox-dark.json +83 -0
- package/themes/crew-gruvbox-light.json +90 -0
- package/themes/crew-nord.json +85 -0
- package/themes/crew-one-dark.json +89 -0
- package/themes/crew-solarized-dark.json +90 -0
- package/themes/crew-solarized-light.json +92 -0
- package/themes/crew-tokyo-night.json +85 -0
- package/src/runtime/budget-tracker.ts +0 -354
- package/src/state/memory-store.ts +0 -244
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knowledge-injection.ts — Project knowledge that accumulates across runs (O4).
|
|
3
|
+
*
|
|
4
|
+
* ROADMAP Phase 1 / T1.3 ("downsized memory"): a deliberately minimal
|
|
5
|
+
* replacement for the deleted 244-LOC MemoryStore. Crews and the main
|
|
6
|
+
* session read `.crew/knowledge.md` and have it injected into the system
|
|
7
|
+
* prompt, so pi-crew "remembers" project context across runs.
|
|
8
|
+
*
|
|
9
|
+
* Philosophy (Round 6 stress-test): radically downsized. Just a Markdown
|
|
10
|
+
* file the user can edit, surfaced into every run. No vector DB, no
|
|
11
|
+
* embeddings, no graph. Simple = trustworthy.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi-api.ts";
|
|
16
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
17
|
+
|
|
18
|
+
/** The knowledge file, relative to the project crew root. */
|
|
19
|
+
export const KNOWLEDGE_FILENAME = "knowledge.md";
|
|
20
|
+
/** Cap injected knowledge to avoid unbounded system-prompt growth. */
|
|
21
|
+
const MAX_KNOWLEDGE_BYTES = 16_000;
|
|
22
|
+
|
|
23
|
+
/** Resolve the knowledge file path for a cwd (may not exist yet). */
|
|
24
|
+
export function knowledgePath(cwd: string): string {
|
|
25
|
+
return path.join(projectCrewRoot(cwd), KNOWLEDGE_FILENAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read knowledge content, truncated to a safe size. "" if absent/empty. */
|
|
29
|
+
export function readKnowledge(cwd: string): string {
|
|
30
|
+
try {
|
|
31
|
+
const p = knowledgePath(cwd);
|
|
32
|
+
if (!fs.existsSync(p)) return "";
|
|
33
|
+
let content = fs.readFileSync(p, "utf8").trim();
|
|
34
|
+
if (content.length > MAX_KNOWLEDGE_BYTES) {
|
|
35
|
+
content = `${content.slice(0, MAX_KNOWLEDGE_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_BYTES} bytes -->`;
|
|
36
|
+
}
|
|
37
|
+
return content;
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the injected prompt fragment (empty if no knowledge). */
|
|
44
|
+
export function buildKnowledgeFragment(cwd: string): string {
|
|
45
|
+
const content = readKnowledge(cwd);
|
|
46
|
+
if (!content) return "";
|
|
47
|
+
return [
|
|
48
|
+
"",
|
|
49
|
+
"# Project knowledge (from .crew/knowledge.md)",
|
|
50
|
+
"The following project knowledge was captured by pi-crew from prior runs.",
|
|
51
|
+
"Use it to avoid repeating past mistakes and to respect project conventions.",
|
|
52
|
+
"You may update .crew/knowledge.md when you learn something durable.",
|
|
53
|
+
"",
|
|
54
|
+
content,
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register the knowledge-injection hook. Appends project knowledge to the
|
|
60
|
+
* system prompt on every agent start (main session + each crew worker,
|
|
61
|
+
* since workers are child Pi processes that also fire before_agent_start).
|
|
62
|
+
*/
|
|
63
|
+
export function registerKnowledgeInjection(pi: ExtensionAPI): void {
|
|
64
|
+
pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {
|
|
65
|
+
const options = (event as BeforeAgentStartEvent & { systemPromptOptions?: { cwd?: unknown } }).systemPromptOptions ?? {};
|
|
66
|
+
const cwd = typeof options.cwd === "string" ? options.cwd : process.cwd();
|
|
67
|
+
const fragment = buildKnowledgeFragment(cwd);
|
|
68
|
+
if (!fragment) return undefined;
|
|
69
|
+
return { systemPrompt: `${event.systemPrompt}${fragment}` };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-api.ts — pi-crew's stable seam against the Pi extension API.
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE (roadmap Phase 0 / Pillar 4 "Pi-Native, Protocol-Aware"):
|
|
5
|
+
* Centralize every symbol pi-crew imports from
|
|
6
|
+
* `@earendil-works/pi-coding-agent` so that:
|
|
7
|
+
* 1. The coupling surface is auditable in ONE file (8 symbols today).
|
|
8
|
+
* 2. A Pi API rename/restructure requires updating only this seam.
|
|
9
|
+
* 3. New code imports from here ("./pi-api") rather than the raw package,
|
|
10
|
+
* establishing the indirection that hedges Pi-coupling risk.
|
|
11
|
+
*
|
|
12
|
+
* COUpling SURFACE (kept intentionally small — public extension API only):
|
|
13
|
+
* - ExtensionAPI the registration entry point (pi)
|
|
14
|
+
* - ExtensionContext per-event/session context (ctx)
|
|
15
|
+
* - ExtensionCommandContext slash-command context
|
|
16
|
+
* - ToolDefinition tool schema type
|
|
17
|
+
* - defineTool tool factory
|
|
18
|
+
* - createBashTool built-in bash tool (for custom tooling)
|
|
19
|
+
* - AgentSessionEvent session event type
|
|
20
|
+
* - BeforeAgentStartEvent pre-turn system-prompt hook event
|
|
21
|
+
*
|
|
22
|
+
* NEW code should `import { ExtensionAPI, ExtensionContext } from "./pi-api"`.
|
|
23
|
+
* Existing files may keep their direct imports; migrate opportunistically
|
|
24
|
+
* (no big-bang refactor — see roadmap Phase 0 T0.3/T0.4 notes).
|
|
25
|
+
*
|
|
26
|
+
* Note: these are TYPE-level re-exports (erased at runtime). Runtime coupling
|
|
27
|
+
* to Pi is via the `pi` and `ctx` objects passed by Pi's loader — that is
|
|
28
|
+
* unavoidable and correct (pi-crew IS a Pi extension). This seam documents
|
|
29
|
+
* and centralizes the type-level surface.
|
|
30
|
+
*/
|
|
31
|
+
export type {
|
|
32
|
+
ExtensionAPI,
|
|
33
|
+
ExtensionContext,
|
|
34
|
+
ExtensionCommandContext,
|
|
35
|
+
ToolDefinition,
|
|
36
|
+
AgentSessionEvent,
|
|
37
|
+
BeforeAgentStartEvent,
|
|
38
|
+
} from "@earendil-works/pi-coding-agent";
|
|
39
|
+
|
|
40
|
+
export { defineTool, createBashTool } from "@earendil-works/pi-coding-agent";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The Pi package version pi-crew was built against. Used for diagnostics
|
|
44
|
+
* and to surface version-drift if Pi upgrades introduce breaking changes.
|
|
45
|
+
* Update this when bumping the pi-coding-agent dependency.
|
|
46
|
+
*/
|
|
47
|
+
export const BUILT_AGAINST_PI_VERSION = "0.79.3";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
stopAsyncRunNotifier,
|
|
18
18
|
} from "./async-notifier.ts";
|
|
19
19
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
20
|
+
import { registerKnowledgeInjection } from "./knowledge-injection.ts";
|
|
20
21
|
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
21
22
|
import type { ScheduledJob } from "../runtime/scheduler.ts";
|
|
22
23
|
import { clearHooksScoped } from "../hooks/registry.ts";
|
|
@@ -61,8 +62,9 @@ import {
|
|
|
61
62
|
type CrewWidgetState,
|
|
62
63
|
stopCrewWidget,
|
|
63
64
|
updateCrewWidget,
|
|
64
|
-
} from "../ui/
|
|
65
|
+
} from "../ui/widget/index.ts";
|
|
65
66
|
import { summarizeHeartbeats } from "../ui/heartbeat-aggregator.ts";
|
|
67
|
+
import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
|
|
66
68
|
import {
|
|
67
69
|
requestRender,
|
|
68
70
|
setExtensionWidget,
|
|
@@ -187,6 +189,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
187
189
|
const disposeI18n = initI18n(pi);
|
|
188
190
|
resetTimings();
|
|
189
191
|
time("register:start");
|
|
192
|
+
// Deploy bundled themes (crew-dark, crew-dracula, etc.) to ~/.pi/agent/themes/
|
|
193
|
+
// so Pi's theme loader discovers them. Best-effort, idempotent.
|
|
194
|
+
deployBundledThemes();
|
|
190
195
|
const globalStore = globalThis as Record<string | symbol, unknown>;
|
|
191
196
|
const runtimeCleanupStoreKey = Symbol("__piCrewRuntimeCleanup");
|
|
192
197
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
@@ -981,6 +986,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
981
986
|
};
|
|
982
987
|
time("register.policy");
|
|
983
988
|
registerAutonomousPolicy(pi);
|
|
989
|
+
registerKnowledgeInjection(pi);
|
|
990
|
+
time("register.knowledge");
|
|
984
991
|
time("register.rpc");
|
|
985
992
|
function getPiEvents():
|
|
986
993
|
| Parameters<typeof registerPiCrewRpc>[0]
|
|
@@ -1194,6 +1201,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1194
1201
|
|
|
1195
1202
|
pi.on("session_start", (_event, ctx) => {
|
|
1196
1203
|
runArtifactCleanup(ctx.cwd);
|
|
1204
|
+
|
|
1205
|
+
// Restore brief mode state from session entries
|
|
1206
|
+
try {
|
|
1207
|
+
const entries = ctx.sessionManager?.getEntries?.();
|
|
1208
|
+
if (entries) {
|
|
1209
|
+
import("../ui/tool-renderers/brief-mode.ts").then(({ restoreBriefState }) => {
|
|
1210
|
+
restoreBriefState(entries);
|
|
1211
|
+
}).catch(() => {/* non-critical */});
|
|
1212
|
+
}
|
|
1213
|
+
} catch { /* non-critical */ }
|
|
1214
|
+
|
|
1197
1215
|
time("register.session-start");
|
|
1198
1216
|
cleanedUp = false;
|
|
1199
1217
|
sessionGeneration++;
|
|
@@ -1974,6 +1992,19 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1974
1992
|
|
|
1975
1993
|
registerCleanupHandler(pi);
|
|
1976
1994
|
|
|
1995
|
+
|
|
1996
|
+
// Resilient edit (Phase 2): OPT-IN via CREW_RESILIENT_EDIT=1.
|
|
1997
|
+
// Wraps the native edit tool with the cascading replace() fallback so that
|
|
1998
|
+
// slightly-wrong oldText (indentation drift, whitespace) still matches.
|
|
1999
|
+
// Auto-disables if pi-diff is loaded (it has its own replace integration).
|
|
2000
|
+
if (process.env.CREW_RESILIENT_EDIT === "1") {
|
|
2001
|
+
import("../runtime/resilient-edit.ts")
|
|
2002
|
+
.then(({ wrapEditWithResilientReplace }) => {
|
|
2003
|
+
wrapEditWithResilientReplace(pi);
|
|
2004
|
+
})
|
|
2005
|
+
.catch(() => { /* non-critical */ });
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1977
2008
|
registerTeamCommands(pi, {
|
|
1978
2009
|
startForegroundRun,
|
|
1979
2010
|
abortForegroundRun,
|
|
@@ -364,13 +364,52 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
364
364
|
} catch (error) {
|
|
365
365
|
ctx.ui.notify(`Failed to save: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
366
366
|
}
|
|
367
|
-
}, () => done(undefined))
|
|
367
|
+
}, () => done(undefined), async (action: string, value: unknown) => {
|
|
368
|
+
// Action callbacks (Pi theme switch) write to a different store
|
|
369
|
+
// than pi-crew config (e.g. ~/.pi/agent/settings.json).
|
|
370
|
+
try {
|
|
371
|
+
if (action === "piTheme" && typeof value === "string") {
|
|
372
|
+
// Live theme switch: ctx.ui.setTheme() swaps the global theme,
|
|
373
|
+
// persists it to settings.json, and triggers a UI redraw — no
|
|
374
|
+
// restart needed. Falls back to file-write + restart hint if
|
|
375
|
+
// the live API is unavailable (e.g. non-TUI mode).
|
|
376
|
+
if (typeof ctx.ui.setTheme === "function") {
|
|
377
|
+
const res = ctx.ui.setTheme(value);
|
|
378
|
+
if (res.success) {
|
|
379
|
+
ctx.ui.notify(`Theme: ${value} (applied live)`, "info");
|
|
380
|
+
} else {
|
|
381
|
+
const { setPiTheme } = await import("../../ui/theme-discovery.ts");
|
|
382
|
+
setPiTheme(value);
|
|
383
|
+
ctx.ui.notify(`Theme saved as '${value}' but failed to apply: ${res.error ?? "unknown"}. Restart Pi.`, "warning");
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
const { setPiTheme } = await import("../../ui/theme-discovery.ts");
|
|
387
|
+
setPiTheme(value);
|
|
388
|
+
ctx.ui.notify(`Pi theme set to '${value}'. Restart Pi to apply.`, "info");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
ctx.ui.notify(`Failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
393
|
+
}
|
|
394
|
+
});
|
|
368
395
|
return overlay;
|
|
369
396
|
}, { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
370
397
|
return;
|
|
371
398
|
}
|
|
372
399
|
const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, teamCommandContext(ctx));
|
|
373
400
|
await notifyCommandResult(ctx, commandText(result));
|
|
401
|
+
// Live-switch hook: when the text subcommand 'theme <name>' succeeds,
|
|
402
|
+
// also apply the theme live via ctx.ui.setTheme() (no restart). The
|
|
403
|
+
// handler above only writes to settings.json.
|
|
404
|
+
const trimmed = args.trim();
|
|
405
|
+
if (trimmed.startsWith("theme ")) {
|
|
406
|
+
const themeName = trimmed.slice(6).trim();
|
|
407
|
+
if (themeName && typeof ctx.ui.setTheme === "function") {
|
|
408
|
+
const res = ctx.ui.setTheme(themeName);
|
|
409
|
+
if (res.success) ctx.ui.notify(`Theme: ${themeName} (applied live)`, "info");
|
|
410
|
+
else ctx.ui.notify(`Saved but live-switch failed: ${res.error ?? "unknown"}. Restart Pi.`, "warning");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
374
413
|
},
|
|
375
414
|
})
|
|
376
415
|
|
|
@@ -571,6 +610,31 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
571
610
|
pi.registerCommand("team-help", { description: "Show pi-crew command help", handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
572
611
|
await notifyCommandResult(ctx, piTeamsHelp());
|
|
573
612
|
} });
|
|
613
|
+
|
|
614
|
+
// Brief mode command — toggle compact tool output display
|
|
615
|
+
pi.registerCommand("crew-brief", {
|
|
616
|
+
description: "Toggle brief tool output mode: on | off | status",
|
|
617
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
618
|
+
const { isBrief, setBrief, BRIEF_ENTRY_TYPE, makeBriefEntry } = await import("../../ui/tool-renderers/brief-mode.ts");
|
|
619
|
+
const trimmed = args.trim();
|
|
620
|
+
|
|
621
|
+
if (trimmed === "on") {
|
|
622
|
+
setBrief(true);
|
|
623
|
+
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: true });
|
|
624
|
+
ctx.ui.notify("Brief mode: on — tool output will show compact summaries", "info");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (trimmed === "off") {
|
|
628
|
+
setBrief(false);
|
|
629
|
+
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: false });
|
|
630
|
+
ctx.ui.notify("Brief mode: off — full tool output restored", "info");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// status (default)
|
|
634
|
+
ctx.ui.notify(`Brief mode: ${isBrief() ? "on" : "off"}`, "info");
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
574
638
|
time("register.commands");
|
|
575
639
|
printTimings();
|
|
576
640
|
}
|
|
@@ -12,6 +12,8 @@ const HARD_RATIO = 0.95;
|
|
|
12
12
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
13
13
|
const MAX_ARTIFACT_INDEX_RUNS = 10;
|
|
14
14
|
const MAX_ARTIFACT_INDEX_ITEMS = 80;
|
|
15
|
+
/** Run statuses that mean the run is still in-flight and may need resuming. */
|
|
16
|
+
const IN_FLIGHT_RUN_STATUSES = new Set(["queued", "planning", "running"]);
|
|
15
17
|
|
|
16
18
|
function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
|
|
17
19
|
const value = ctx.model?.contextWindow;
|
|
@@ -66,6 +68,99 @@ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
|
|
|
66
68
|
return lines.join("\n");
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Collect in-flight (non-terminal) crew runs that must be resumable after
|
|
73
|
+
* compaction. These are runs the agent was actively working on or awaiting.
|
|
74
|
+
*/
|
|
75
|
+
export function collectInFlightRuns(cwd: string): TeamRunManifest[] {
|
|
76
|
+
return listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS).filter((run) =>
|
|
77
|
+
IN_FLIGHT_RUN_STATUSES.has(run.status),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build an explicit resume directive that survives compaction. This is the
|
|
83
|
+
* core of O10 (compaction resilience): after compaction, the agent MUST know
|
|
84
|
+
* what crew tasks were in-flight and how to continue them.
|
|
85
|
+
*/
|
|
86
|
+
function formatResumeDirective(runs: TeamRunManifest[]): string {
|
|
87
|
+
if (!runs.length) return "";
|
|
88
|
+
const lines = [
|
|
89
|
+
"",
|
|
90
|
+
"# pi-crew in-flight task resume directive (CRITICAL — do not drop)",
|
|
91
|
+
"The following pi-crew runs were in progress when the context was compacted.",
|
|
92
|
+
"After compaction, you MUST continue these tasks — do NOT consider them finished.",
|
|
93
|
+
];
|
|
94
|
+
for (const run of runs) {
|
|
95
|
+
const wf = run.workflow ? `, workflow=${run.workflow}` : "";
|
|
96
|
+
lines.push(
|
|
97
|
+
`- runId=${run.runId} (status=${run.status}, team=${run.team}${wf}): ${run.goal}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push("To resume: call the `team` tool with action='status' to check progress, then");
|
|
102
|
+
lines.push("action='wait' (to join a still-running task) or action='summary' / action='get'");
|
|
103
|
+
lines.push("to retrieve results. If a task was mid-execution and the worker is still alive,");
|
|
104
|
+
lines.push("it continues independently — just re-attach. Do not restart completed work.");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a short continuation prompt for sendUserMessage. This is what actually
|
|
110
|
+
* makes the agent resume after compaction — Pi's threshold compaction does NOT
|
|
111
|
+
* auto-retry (it stops and waits for user input). By injecting this as a user
|
|
112
|
+
* message that triggers a turn, the agent automatically continues the in-flight
|
|
113
|
+
* crew task instead of stalling until the user types "continue".
|
|
114
|
+
*/
|
|
115
|
+
export function buildContinuationPrompt(runs: TeamRunManifest[]): string {
|
|
116
|
+
if (!runs.length) return "";
|
|
117
|
+
const lines = [
|
|
118
|
+
"[pi-crew] Context was compacted while crew tasks were still in-flight. Continue the work — do not wait for me.",
|
|
119
|
+
];
|
|
120
|
+
for (const run of runs) {
|
|
121
|
+
const wf = run.workflow ? `, workflow=${run.workflow}` : "";
|
|
122
|
+
lines.push(`- runId=${run.runId} (status=${run.status}, team=${run.team}${wf}): ${run.goal}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("Resume: call `team` with action='status' to check progress, then action='wait' (join a running task), action='summary', or action='get' as appropriate. If a worker is still alive it continues independently — just re-attach. Do NOT restart completed work.");
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Trigger automatic agent continuation after compaction. Fire-and-forget the
|
|
131
|
+
* promise — never block the compaction flow. The sendUserMessage type is
|
|
132
|
+
* declared `void` but the runtime returns a Promise (it triggers an agent turn).
|
|
133
|
+
*/
|
|
134
|
+
export function triggerContinuation(pi: ExtensionAPI, ctx: ExtensionContext, runs: TeamRunManifest[]): void {
|
|
135
|
+
if (!runs.length) return;
|
|
136
|
+
const prompt = buildContinuationPrompt(runs);
|
|
137
|
+
try {
|
|
138
|
+
const result = pi.sendUserMessage(prompt) as unknown;
|
|
139
|
+
Promise.resolve(result).catch(() => {
|
|
140
|
+
// best-effort: if continuation fails, at least notify
|
|
141
|
+
try {
|
|
142
|
+
ctx.ui.notify("pi-crew: auto-continuation after compaction failed — use team status to resume manually.", "warning");
|
|
143
|
+
} catch {
|
|
144
|
+
// swallow
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
// best-effort
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Combined customInstructions injected into proactive compaction summaries. */
|
|
153
|
+
function buildCompactionInstructions(cwd: string): string {
|
|
154
|
+
const artifactIndex = collectCrewArtifactIndex(cwd);
|
|
155
|
+
const inFlight = collectInFlightRuns(cwd);
|
|
156
|
+
const parts = [
|
|
157
|
+
"Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.",
|
|
158
|
+
];
|
|
159
|
+
if (artifactIndex.length > 0) parts.push(formatCrewArtifactIndex(artifactIndex));
|
|
160
|
+
if (inFlight.length > 0) parts.push(formatResumeDirective(inFlight));
|
|
161
|
+
return parts.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
69
164
|
export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
|
|
70
165
|
let pendingCompactReason: string | null = null;
|
|
71
166
|
let compactionInProgress = false;
|
|
@@ -73,18 +168,32 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
|
|
|
73
168
|
const startCompact = (ctx: ExtensionContext, reason: string): void => {
|
|
74
169
|
if (compactionInProgress) return;
|
|
75
170
|
compactionInProgress = true;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
171
|
+
const customInstructions = buildCompactionInstructions(ctx.cwd);
|
|
172
|
+
// Append a durable resume entry so it appears in the post-compaction
|
|
173
|
+
// context regardless of how summarization treats customInstructions.
|
|
174
|
+
const inFlight = collectInFlightRuns(ctx.cwd);
|
|
175
|
+
if (inFlight.length > 0) {
|
|
176
|
+
pi.appendEntry("crew:resume-directive", {
|
|
79
177
|
reason,
|
|
80
178
|
createdAt: new Date().toISOString(),
|
|
81
|
-
|
|
179
|
+
runs: inFlight.map((r) => ({
|
|
180
|
+
runId: r.runId,
|
|
181
|
+
status: r.status,
|
|
182
|
+
team: r.team,
|
|
183
|
+
workflow: r.workflow,
|
|
184
|
+
goal: r.goal,
|
|
185
|
+
})),
|
|
82
186
|
});
|
|
83
187
|
}
|
|
84
188
|
ctx.compact({
|
|
85
|
-
customInstructions
|
|
189
|
+
customInstructions,
|
|
86
190
|
onComplete: () => {
|
|
87
191
|
compactionInProgress = false;
|
|
192
|
+
// O10 FIX: Pi's threshold compaction does NOT auto-retry — it
|
|
193
|
+
// stops and waits for user input. Trigger automatic
|
|
194
|
+
// continuation so the agent resumes the in-flight crew task.
|
|
195
|
+
const runs = collectInFlightRuns(ctx.cwd);
|
|
196
|
+
triggerContinuation(pi, ctx, runs);
|
|
88
197
|
ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
|
|
89
198
|
},
|
|
90
199
|
onError: (error) => {
|
|
@@ -94,17 +203,49 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
|
|
|
94
203
|
});
|
|
95
204
|
};
|
|
96
205
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
pi.on("session_before_compact", async (_event, ctx) => {
|
|
102
|
-
// Always allow compaction - context-mode needs it for resume snapshots
|
|
206
|
+
// Allow compaction to proceed. pi-crew state is preserved via the
|
|
207
|
+
// customInstructions + resume-directive entry appended in startCompact,
|
|
208
|
+
// and re-injected post-compaction by the session_compact handler below.
|
|
209
|
+
pi.on("session_before_compact", async (_event, _ctx) => {
|
|
103
210
|
return;
|
|
104
211
|
});
|
|
105
212
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
213
|
+
// O10: After ANY compaction (proactive OR reactive/Pi-triggered), detect
|
|
214
|
+
// in-flight crew runs and trigger automatic continuation. This is the
|
|
215
|
+
// critical fix: Pi's threshold compaction does NOT auto-retry — it stops
|
|
216
|
+
// and waits for user input. By injecting a continuation user message that
|
|
217
|
+
// triggers a turn, the agent automatically resumes the in-flight crew task.
|
|
218
|
+
// This covers the common case where Pi auto-compacts without going through
|
|
219
|
+
// our proactive startCompact path.
|
|
220
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
221
|
+
try {
|
|
222
|
+
const inFlight = collectInFlightRuns(ctx.cwd);
|
|
223
|
+
if (inFlight.length === 0) return;
|
|
224
|
+
// Re-append the resume directive entry for durable record.
|
|
225
|
+
pi.appendEntry("crew:resume-directive", {
|
|
226
|
+
reason: "post-compaction-continuation",
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
runs: inFlight.map((r) => ({
|
|
229
|
+
runId: r.runId,
|
|
230
|
+
status: r.status,
|
|
231
|
+
team: r.team,
|
|
232
|
+
workflow: r.workflow,
|
|
233
|
+
goal: r.goal,
|
|
234
|
+
})),
|
|
235
|
+
});
|
|
236
|
+
ctx.ui.notify(
|
|
237
|
+
`Context compacted. ${inFlight.length} pi-crew run(s) still in-flight — auto-resuming.`,
|
|
238
|
+
"info",
|
|
239
|
+
);
|
|
240
|
+
// THE FIX: trigger automatic continuation. Without this, Pi stops
|
|
241
|
+
// after threshold compaction and the user must type "continue".
|
|
242
|
+
triggerContinuation(pi, ctx, inFlight);
|
|
243
|
+
} catch {
|
|
244
|
+
// best-effort: never block compaction completion
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Proactive compaction with dynamic threshold.
|
|
108
249
|
pi.on("turn_end", (_event, ctx) => {
|
|
109
250
|
if (compactionInProgress) return;
|
|
110
251
|
const hasActiveForeground = options.foregroundControllers.size > 0 || options.foregroundTeamRunControllers.size > 0;
|
|
@@ -112,7 +253,6 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
|
|
|
112
253
|
// If deferred compaction is pending and foreground just ended, check if still needed
|
|
113
254
|
if (!hasActiveForeground && pendingCompactReason) {
|
|
114
255
|
pendingCompactReason = null;
|
|
115
|
-
// Only compact if ratio still >= TRIGGER_RATIO, otherwise skip
|
|
116
256
|
if (ratio === undefined || ratio < TRIGGER_RATIO) return;
|
|
117
257
|
startCompact(ctx, "deferred");
|
|
118
258
|
return;
|
|
@@ -22,7 +22,8 @@ import { t } from "../../i18n.ts";
|
|
|
22
22
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
23
23
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
24
24
|
import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
26
|
+
import { agentToolRenderer } from "../../ui/tool-renderers/index.ts";
|
|
26
27
|
|
|
27
28
|
const TOOL_PROGRESS_TICK_MS = 1000;
|
|
28
29
|
|
|
@@ -98,11 +99,15 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
98
99
|
},
|
|
99
100
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
101
|
renderCall(args: any, theme: any, context: any): any {
|
|
101
|
-
return
|
|
102
|
+
return agentToolRenderer.renderCall(args, theme, context);
|
|
102
103
|
},
|
|
103
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
105
|
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
105
|
-
|
|
106
|
+
try {
|
|
107
|
+
return agentToolRenderer.renderResult(result, options, theme, context);
|
|
108
|
+
} catch (e: any) {
|
|
109
|
+
return new Text('agent-err: ' + (e?.message ?? 'unknown'), 0, 0);
|
|
110
|
+
}
|
|
106
111
|
},
|
|
107
112
|
};
|
|
108
113
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { loadConfig } from "../../config/config.ts";
|
|
4
4
|
import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
5
|
-
import type { CrewWidgetState } from "../../ui/
|
|
6
|
-
import { updateCrewWidget } from "../../ui/
|
|
5
|
+
import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
6
|
+
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
7
7
|
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
8
8
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
9
9
|
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
10
10
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
11
11
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
13
|
+
import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
|
|
13
14
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
14
15
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
15
16
|
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
@@ -48,7 +49,7 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
|
|
|
48
49
|
if (!override) return { ok: true, cwd: baseCwd };
|
|
49
50
|
try {
|
|
50
51
|
const resolved = resolveRealContainedPath(baseCwd, override);
|
|
51
|
-
const stat =
|
|
52
|
+
const stat = statSync(resolved);
|
|
52
53
|
if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
|
|
53
54
|
return { ok: true, cwd: resolved };
|
|
54
55
|
} catch (error) {
|
|
@@ -107,11 +108,15 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
107
108
|
},
|
|
108
109
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
110
|
renderCall(args: any, theme: any, context: any): any {
|
|
110
|
-
return
|
|
111
|
+
return teamToolRenderer.renderCall(args, theme, context);
|
|
111
112
|
},
|
|
112
113
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
114
|
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
114
|
-
|
|
115
|
+
try {
|
|
116
|
+
return teamToolRenderer.renderResult(result, options, theme, context);
|
|
117
|
+
} catch {
|
|
118
|
+
return new Text(statusIcon("completed", theme) + " done", 0, 0);
|
|
119
|
+
}
|
|
115
120
|
},
|
|
116
121
|
};
|
|
117
122
|
pi.registerTool(tool);
|
|
@@ -133,13 +138,15 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
|
|
|
133
138
|
try {
|
|
134
139
|
if (!cwd || !runId) {
|
|
135
140
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
136
|
-
|
|
141
|
+
const msg = `team status=starting elapsed=${elapsed}s`;
|
|
142
|
+
onUpdate({ content: [{ type: "text", text: msg }] });
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
|
-
const loaded = loadRunManifestById(cwd, runId);
|
|
145
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
140
146
|
if (!loaded) {
|
|
141
147
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
142
|
-
|
|
148
|
+
const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
|
|
149
|
+
onUpdate({ content: [{ type: "text", text: msg }] });
|
|
143
150
|
return;
|
|
144
151
|
}
|
|
145
152
|
let agents;
|
|
@@ -162,7 +169,7 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
|
|
|
162
169
|
const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
|
|
163
170
|
if (typeof timer.unref === "function") timer.unref();
|
|
164
171
|
return {
|
|
165
|
-
attach: (boundCwd, boundRunId) => { cwd = boundCwd; runId = boundRunId; tick(); },
|
|
172
|
+
attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
|
|
166
173
|
stop: () => clearInterval(timer),
|
|
167
174
|
};
|
|
168
175
|
}
|