@melihmucuk/pi-crew 1.0.18 → 1.0.20
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 +9 -9
- package/agents/code-reviewer.md +17 -12
- package/agents/quality-reviewer.md +9 -18
- package/extension/crew.ts +18 -11
- package/extension/index.ts +33 -15
- package/extension/subagent-session.ts +12 -14
- package/extension/tools.ts +13 -5
- package/extension/ui.ts +8 -20
- package/package.json +6 -6
- package/prompts/pi-crew-review.md +25 -16
- package/skills/pi-crew/REFERENCE.md +7 -2
- package/skills/pi-crew/SKILL.md +8 -1
- package/extension/overflow-recovery.ts +0 -211
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Lists available subagent definitions and active subagents owned by the current s
|
|
|
34
34
|
|
|
35
35
|
#### `crew_spawn`
|
|
36
36
|
|
|
37
|
-
Spawns a subagent in an isolated session. The subagent runs in the background with its own context window, tools, and skills. When it finishes, the result is delivered to the session that spawned it as a steering message that triggers a new turn. If that session is not active, the result is queued until you switch back to it.
|
|
37
|
+
Spawns a subagent in an isolated session. Each spawn includes a concise `brief` label for session lists and a full self-contained `task`. The subagent runs in the background with its own context window, tools, and skills. When it finishes, the result is delivered to the session that spawned it as a steering message that triggers a new turn. If that session is not active, the result is queued until you switch back to it.
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
"spawn scout and find all API endpoints and their authentication methods"
|
|
@@ -100,14 +100,14 @@ A bundled orchestration skill that provides best practices for delegating work t
|
|
|
100
100
|
|
|
101
101
|
pi-crew ships with six subagent definitions that cover common workflows:
|
|
102
102
|
|
|
103
|
-
| Subagent | Purpose | Tools | Model |
|
|
104
|
-
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------- | --------------------------- |
|
|
105
|
-
| **scout** | Investigates codebase and returns structured findings. Read-only.
|
|
106
|
-
| **planner** |
|
|
107
|
-
| **oracle** | Evaluates critical decisions, surfaces blind spots, and challenges assumptions. Read-only.
|
|
108
|
-
| **code-reviewer** | Reviews code
|
|
109
|
-
| **quality-reviewer** | Reviews code
|
|
110
|
-
| **worker** | Implements code changes
|
|
103
|
+
| Subagent | Purpose | Tools | Model | Thinking |
|
|
104
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------- | --------------------------- | -------- |
|
|
105
|
+
| **scout** | Investigates codebase and returns structured findings. Read-only. | read, grep, find, ls, bash | openai-codex/gpt-5.5 | off |
|
|
106
|
+
| **planner** | Produces deterministic implementation plans. Read-only. Does not write code. | read, grep, find, ls, bash | openai-codex/gpt-5.5 | high |
|
|
107
|
+
| **oracle** | Evaluates critical decisions, surfaces blind spots, and challenges assumptions. Read-only. | read, grep, find, ls, bash | openai-codex/gpt-5.5 | xhigh |
|
|
108
|
+
| **code-reviewer** | Reviews scoped code for actionable bugs. Read-only. | read, grep, find, ls, bash | openai-codex/gpt-5.5 | high |
|
|
109
|
+
| **quality-reviewer** | Reviews scoped code for maintainability, duplication, and complexity. Read-only. | read, grep, find, ls, bash | openai-codex/gpt-5.5 | high |
|
|
110
|
+
| **worker** | Implements scoped code changes safely and verifies them. | all | openai-codex/gpt-5.5 | low |
|
|
111
111
|
|
|
112
112
|
Read-only bundled subagents still keep `bash` for inspection workflows like `git` and `ast-grep`. This is an instruction-level contract, not a sandbox boundary.
|
|
113
113
|
|
package/agents/code-reviewer.md
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-reviewer
|
|
3
|
-
description: Reviews
|
|
4
|
-
model: openai-codex/gpt-5.
|
|
3
|
+
description: Reviews scoped code for actionable bugs. Read-only.
|
|
4
|
+
model: openai-codex/gpt-5.5
|
|
5
5
|
thinking: high
|
|
6
6
|
tools: read, grep, find, ls, bash
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
You are a read-only code reviewer. Your goal is not to find something; it is to decide whether the
|
|
9
|
+
You are a read-only code reviewer. Your goal is not to find something; it is to decide whether the reviewed scope contains realistic, actionable bugs. An empty review is a valid successful outcome. Reply in the user's language.
|
|
10
10
|
|
|
11
11
|
Do not modify files. Use bash only for read-only inspection. Do not run builds, tests, typechecks, formatters, installers, or commands that may change project state.
|
|
12
12
|
|
|
13
13
|
## Scope
|
|
14
14
|
|
|
15
|
-
Review the provided scope. If none is provided, review uncommitted changes.
|
|
15
|
+
Review the provided scope. If none is provided, review uncommitted changes.
|
|
16
16
|
|
|
17
|
-
For
|
|
17
|
+
For commits, branches, PRs, files, directories, modules, or "latest" requests, inspect the corresponding diff or code. If "latest" is requested, review the last 5 commits unless a count is given.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
If "full", "codebase", or whole-repo review is requested, perform a bounded bug audit: map the highest-risk areas, deeply inspect selected files, state coverage/skipped areas briefly, and do not imply exhaustive coverage.
|
|
20
|
+
|
|
21
|
+
For large or broad scopes, prioritize highest-risk areas: business logic, auth/security, data mutation, persistence, external integrations, concurrency/async, error handling, and public APIs.
|
|
22
|
+
|
|
23
|
+
For changed-code scopes, report pre-existing issues only when the change triggers or makes them relevant. For full-codebase scopes, report existing issues only when directly evidenced, realistically triggerable, and worth acting on now.
|
|
20
24
|
|
|
21
25
|
## Method
|
|
22
26
|
|
|
23
|
-
Diffs are not enough. Before reporting a finding, read the full
|
|
27
|
+
Diffs are not enough. Before reporting a finding, read the full relevant file involved. Trace direct callers/callees or nearby patterns only when needed. Check local conventions only when relevant. Stop expanding context when it stops adding evidence.
|
|
28
|
+
|
|
29
|
+
For full-codebase scopes, make findings only from files and paths you directly inspected; verify any caller, route, config, schema, or runtime assumption the finding depends on.
|
|
24
30
|
|
|
25
31
|
Do not report findings from skipped or unreviewed files. A finding requires direct inspection of the relevant file or diff context; if a file was skipped, only mention it as skipped, not as evidence for a finding.
|
|
26
32
|
|
|
@@ -40,17 +46,15 @@ Report the same finding pattern at most twice, then list other affected location
|
|
|
40
46
|
|
|
41
47
|
## Severity
|
|
42
48
|
|
|
43
|
-
- Critical:
|
|
44
|
-
- Major: realistic
|
|
45
|
-
- Minor: real non-blocking
|
|
49
|
+
- Critical: urgent, high-impact issue within this reviewer's scope that can cause severe user, data, security, operational, or near-term development breakage.
|
|
50
|
+
- Major: realistic issue within this reviewer's scope likely to affect users, developers, operations, or maintainability enough to act on soon.
|
|
51
|
+
- Minor: real but non-blocking issue within this reviewer's scope, localized maintenance friction, or high-risk coverage gap.
|
|
46
52
|
|
|
47
53
|
## Output
|
|
48
54
|
|
|
49
55
|
If no findings:
|
|
50
56
|
|
|
51
57
|
**No issues found.**
|
|
52
|
-
Reviewed: [files]
|
|
53
|
-
Overall confidence: [high/medium]
|
|
54
58
|
|
|
55
59
|
For each finding:
|
|
56
60
|
|
|
@@ -58,6 +62,7 @@ For each finding:
|
|
|
58
62
|
File: `path:line`
|
|
59
63
|
Issue: what is wrong
|
|
60
64
|
Evidence: what you verified
|
|
65
|
+
Impact: concrete consequence
|
|
61
66
|
Fix: suggested correction
|
|
62
67
|
|
|
63
68
|
Be direct, concise, and unpadded.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: quality-reviewer
|
|
3
|
-
description: Reviews
|
|
4
|
-
model: openai-codex/gpt-5.
|
|
3
|
+
description: Reviews scoped code for maintainability, duplication, and complexity. Read-only.
|
|
4
|
+
model: openai-codex/gpt-5.5
|
|
5
5
|
thinking: high
|
|
6
6
|
tools: read, grep, find, ls, bash
|
|
7
7
|
---
|
|
@@ -16,7 +16,7 @@ Do not modify files. Use bash only for read-only inspection. Do not run builds,
|
|
|
16
16
|
|
|
17
17
|
Review the provided scope. If none is provided, review uncommitted changes. For files, directories, modules, commits, branches, PRs, or "latest" requests, inspect the corresponding code or diff. If "latest" is requested, review the last 5 commits unless a count is given.
|
|
18
18
|
|
|
19
|
-
If "full"
|
|
19
|
+
If "full", "codebase", or whole-repo review is requested, first produce a structural risk map, then deeply review only the highest-risk areas, state coverage/skipped areas briefly, and do not imply exhaustive coverage.
|
|
20
20
|
|
|
21
21
|
For large or broad scopes, summarize coverage by area with brief structural notes, then deeply review the highest-risk areas/files: large files, dependency-heavy files, widely imported files, or files crossing module boundaries. Avoid exhaustive file inventories; state skipped areas briefly.
|
|
22
22
|
|
|
@@ -48,32 +48,23 @@ Default stance: no new abstraction unless it reduces present-day duplication or
|
|
|
48
48
|
|
|
49
49
|
## Severity
|
|
50
50
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- Minor:
|
|
51
|
+
- Critical: urgent, high-impact issue within this reviewer's scope that can cause severe user, data, security, operational, or near-term development breakage.
|
|
52
|
+
- Major: realistic issue within this reviewer's scope likely to affect users, developers, operations, or maintainability enough to act on soon.
|
|
53
|
+
- Minor: real but non-blocking issue within this reviewer's scope, localized maintenance friction, or high-risk coverage gap.
|
|
54
54
|
|
|
55
55
|
## Output
|
|
56
56
|
|
|
57
57
|
If no findings:
|
|
58
58
|
|
|
59
59
|
**No issues found.**
|
|
60
|
-
Reviewed: [files]
|
|
61
|
-
Overall health: [brief assessment]
|
|
62
60
|
|
|
63
61
|
For each finding:
|
|
64
62
|
|
|
65
63
|
**[SEVERITY] Category: Title**
|
|
66
64
|
File: `path:line`
|
|
67
|
-
Issue:
|
|
68
|
-
Impact: concrete future change/debug task made harder
|
|
65
|
+
Issue: what is wrong
|
|
69
66
|
Evidence: what you verified
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
End with:
|
|
73
|
-
|
|
74
|
-
**Quality Review Summary**
|
|
75
|
-
Files reviewed: [count]
|
|
76
|
-
Findings: [count by severity]
|
|
77
|
-
Overall health: [one sentence]
|
|
67
|
+
Impact: concrete consequence
|
|
68
|
+
Fix: suggested correction
|
|
78
69
|
|
|
79
70
|
Be direct, concise, and unpadded.
|
package/extension/crew.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type SendMessageFn,
|
|
9
9
|
type SteeringPayload,
|
|
10
10
|
type SubagentStatus,
|
|
11
|
-
sendRemainingNote,
|
|
12
11
|
sendSteeringMessage,
|
|
13
12
|
} from "./ui.js";
|
|
14
13
|
|
|
@@ -28,6 +27,7 @@ export interface SubagentState {
|
|
|
28
27
|
id: string;
|
|
29
28
|
agentConfig: AgentConfig;
|
|
30
29
|
task: string;
|
|
30
|
+
brief: string;
|
|
31
31
|
status: SubagentStatus;
|
|
32
32
|
ownerSessionId: string;
|
|
33
33
|
session: AgentSession | null;
|
|
@@ -36,7 +36,6 @@ export interface SubagentState {
|
|
|
36
36
|
model: string | undefined;
|
|
37
37
|
error?: string;
|
|
38
38
|
result?: string;
|
|
39
|
-
promptAbortController?: AbortController;
|
|
40
39
|
unsubscribe?: () => void;
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -65,6 +64,7 @@ export interface SpawnContext {
|
|
|
65
64
|
agentDir: string;
|
|
66
65
|
parentSessionFile?: string;
|
|
67
66
|
onWarning?: (message: string) => void;
|
|
67
|
+
brief?: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
type SettledSubagentStatus = Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
|
|
@@ -151,7 +151,7 @@ export class CrewRuntime {
|
|
|
151
151
|
ctx: SpawnContext,
|
|
152
152
|
extensionResolvedPath: string,
|
|
153
153
|
): string {
|
|
154
|
-
const state = this.createAgent(agentConfig, task, ownerSessionId);
|
|
154
|
+
const state = this.createAgent(agentConfig, task, ctx.brief ?? "", ownerSessionId);
|
|
155
155
|
this.refreshWidgetFor(ownerSessionId);
|
|
156
156
|
this.runner.start(state, {
|
|
157
157
|
cwd,
|
|
@@ -228,12 +228,13 @@ export class CrewRuntime {
|
|
|
228
228
|
.map(buildActiveAgentSummary);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
private createAgent(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState {
|
|
231
|
+
private createAgent(agentConfig: AgentConfig, task: string, brief: string, ownerSessionId: string): SubagentState {
|
|
232
232
|
const id = generateId(agentConfig.name, new Set(this.agents.keys()));
|
|
233
233
|
const state: SubagentState = {
|
|
234
234
|
id,
|
|
235
235
|
agentConfig,
|
|
236
236
|
task,
|
|
237
|
+
brief,
|
|
237
238
|
status: "running",
|
|
238
239
|
ownerSessionId,
|
|
239
240
|
session: null,
|
|
@@ -271,7 +272,6 @@ export class CrewRuntime {
|
|
|
271
272
|
|
|
272
273
|
private disposeAgent(state: SubagentState): void {
|
|
273
274
|
state.unsubscribe?.();
|
|
274
|
-
state.promptAbortController = undefined;
|
|
275
275
|
state.session?.dispose();
|
|
276
276
|
this.agents.delete(state.id);
|
|
277
277
|
this.refreshWidgetFor(state.ownerSessionId);
|
|
@@ -320,8 +320,6 @@ export class CrewRuntime {
|
|
|
320
320
|
private schedulePendingFlushFor(sessionId: string): void {
|
|
321
321
|
if (!this.pendingMessages.some((entry) => entry.ownerSessionId === sessionId)) return;
|
|
322
322
|
|
|
323
|
-
// Delay flush to next macrotask. session_start fires before pi-core reconnects the
|
|
324
|
-
// agent event listener; synchronous delivery can lose JSONL persistence.
|
|
325
323
|
this.flushScheduled = true;
|
|
326
324
|
this.scheduleFlush(() => {
|
|
327
325
|
this.flushScheduled = false;
|
|
@@ -370,14 +368,23 @@ export class CrewRuntime {
|
|
|
370
368
|
|
|
371
369
|
const remaining = this.countRunningForOwner(ownerSessionId, payload.id);
|
|
372
370
|
const isIdle = this.activeBinding.isIdle();
|
|
373
|
-
const triggerResultTurn = !(isIdle && remaining > 0);
|
|
371
|
+
const triggerResultTurn = payload.status === "waiting" || !(isIdle && remaining > 0);
|
|
374
372
|
|
|
375
373
|
sendSteeringMessage(payload, this.activeBinding.sendMessage, { isIdle, triggerTurn: triggerResultTurn });
|
|
376
|
-
sendRemainingNote(remaining, this.activeBinding.sendMessage, { isIdle, triggerTurn: isIdle && remaining > 0 });
|
|
377
374
|
}
|
|
378
375
|
}
|
|
379
376
|
|
|
377
|
+
const CREW_RUNTIME_VERSION = 2;
|
|
380
378
|
const crewRuntimeKey = Symbol.for("pi-crew.runtime");
|
|
381
|
-
const globalWithCrewRuntime = globalThis as typeof globalThis & Record<symbol, CrewRuntime | undefined>;
|
|
379
|
+
const globalWithCrewRuntime = globalThis as typeof globalThis & Record<symbol, (CrewRuntime & { __piCrewRuntimeVersion?: number }) | undefined>;
|
|
382
380
|
|
|
383
|
-
|
|
381
|
+
function createCrewRuntime(): CrewRuntime & { __piCrewRuntimeVersion?: number } {
|
|
382
|
+
const runtime = new CrewRuntime() as CrewRuntime & { __piCrewRuntimeVersion?: number };
|
|
383
|
+
runtime.__piCrewRuntimeVersion = CREW_RUNTIME_VERSION;
|
|
384
|
+
return runtime;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const existingRuntime = globalWithCrewRuntime[crewRuntimeKey];
|
|
388
|
+
export const crewRuntime = existingRuntime?.__piCrewRuntimeVersion === CREW_RUNTIME_VERSION
|
|
389
|
+
? existingRuntime
|
|
390
|
+
: (globalWithCrewRuntime[crewRuntimeKey] = createCrewRuntime());
|
package/extension/index.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { crewRuntime } from "./crew.js";
|
|
4
|
+
import { crewRuntime, type CrewRuntime } from "./crew.js";
|
|
5
5
|
import { registerCrewTools } from "./tools.js";
|
|
6
6
|
import { registerCrewMessageRenderers, updateWidget } from "./ui.js";
|
|
7
7
|
|
|
8
8
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
+
interface ProcessHooks {
|
|
11
|
+
once(event: "SIGINT", listener: () => void): unknown;
|
|
12
|
+
on(event: "beforeExit", listener: () => void): unknown;
|
|
13
|
+
exit(code?: number): never;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RegisterPiCrewExtensionOptions {
|
|
17
|
+
crew?: CrewRuntime;
|
|
18
|
+
extensionDir?: string;
|
|
19
|
+
processHooks?: ProcessHooks;
|
|
20
|
+
processHooksSetupKey?: symbol;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
// Process-level cleanup for subagents on exit
|
|
11
24
|
const processHooksSetupKey = Symbol.for("pi-crew.processHooksSetup");
|
|
12
25
|
const globalWithProcessHooks = globalThis as typeof globalThis & Record<
|
|
@@ -14,29 +27,30 @@ const globalWithProcessHooks = globalThis as typeof globalThis & Record<
|
|
|
14
27
|
boolean | undefined
|
|
15
28
|
>;
|
|
16
29
|
|
|
17
|
-
function setupProcessHooks() {
|
|
18
|
-
if (globalWithProcessHooks[
|
|
19
|
-
globalWithProcessHooks[
|
|
30
|
+
function setupProcessHooks(crew: CrewRuntime, processHooks: ProcessHooks, setupKey: symbol) {
|
|
31
|
+
if (globalWithProcessHooks[setupKey]) return;
|
|
32
|
+
globalWithProcessHooks[setupKey] = true;
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
processHooks.once("SIGINT", () => {
|
|
35
|
+
crew.abortAll();
|
|
36
|
+
processHooks.exit(130);
|
|
24
37
|
});
|
|
25
|
-
|
|
38
|
+
processHooks.on("beforeExit", () => crew.abortAll());
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
export
|
|
41
|
+
export function registerPiCrewExtension(pi: ExtensionAPI, options: RegisterPiCrewExtensionOptions = {}) {
|
|
42
|
+
const crew = options.crew ?? crewRuntime;
|
|
29
43
|
let currentCtx: ExtensionContext | undefined;
|
|
30
44
|
|
|
31
|
-
setupProcessHooks();
|
|
45
|
+
setupProcessHooks(crew, options.processHooks ?? process, options.processHooksSetupKey ?? processHooksSetupKey);
|
|
32
46
|
|
|
33
47
|
const refreshWidget = () => {
|
|
34
|
-
if (currentCtx) updateWidget(currentCtx,
|
|
48
|
+
if (currentCtx) updateWidget(currentCtx, crew);
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
const activateSession = (ctx: ExtensionContext) => {
|
|
38
52
|
currentCtx = ctx;
|
|
39
|
-
|
|
53
|
+
crew.activateSession(
|
|
40
54
|
{
|
|
41
55
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
42
56
|
isIdle: () => ctx.isIdle(),
|
|
@@ -52,13 +66,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
52
66
|
|
|
53
67
|
pi.on("session_shutdown", (event, ctx) => {
|
|
54
68
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
55
|
-
|
|
69
|
+
crew.deactivateSession(sessionId);
|
|
56
70
|
|
|
57
71
|
if (event.reason === "quit") {
|
|
58
|
-
|
|
72
|
+
crew.abortAll();
|
|
59
73
|
}
|
|
60
74
|
});
|
|
61
75
|
|
|
62
|
-
registerCrewTools(pi,
|
|
76
|
+
registerCrewTools(pi, crew, options.extensionDir ?? extensionDir);
|
|
63
77
|
registerCrewMessageRenderers(pi);
|
|
64
78
|
}
|
|
79
|
+
|
|
80
|
+
export default function (pi: ExtensionAPI) {
|
|
81
|
+
registerPiCrewExtension(pi);
|
|
82
|
+
}
|
|
@@ -12,7 +12,6 @@ import type { AgentConfig } from "./catalog.js";
|
|
|
12
12
|
import { SUPPORTED_TOOL_NAMES, type SupportedToolName } from "./catalog.js";
|
|
13
13
|
import type { SubagentState } from "./crew.js";
|
|
14
14
|
import type { SubagentStatus } from "./ui.js";
|
|
15
|
-
import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
|
|
16
15
|
|
|
17
16
|
export interface BootstrapContext {
|
|
18
17
|
model: Model<Api> | undefined;
|
|
@@ -176,6 +175,16 @@ function isAborted(state: SubagentState): boolean {
|
|
|
176
175
|
return state.status === "aborted";
|
|
177
176
|
}
|
|
178
177
|
|
|
178
|
+
function normalizeSessionNamePart(value: string): string {
|
|
179
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function formatSubagentSessionName(state: Pick<SubagentState, "agentConfig" | "brief" | "id">): string {
|
|
183
|
+
const agentName = normalizeSessionNamePart(state.agentConfig.name) || "subagent";
|
|
184
|
+
const brief = normalizeSessionNamePart(state.brief) || state.id;
|
|
185
|
+
return `crew: ${agentName} · ${brief}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
179
188
|
export class SubagentSessionRunner implements SubagentRunner {
|
|
180
189
|
constructor(private readonly callbacks: SubagentRunnerCallbacks) {}
|
|
181
190
|
|
|
@@ -188,10 +197,7 @@ export class SubagentSessionRunner implements SubagentRunner {
|
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
abort(state: SubagentState): void {
|
|
191
|
-
state.promptAbortController?.abort();
|
|
192
|
-
state.promptAbortController = undefined;
|
|
193
200
|
state.session?.abortCompaction();
|
|
194
|
-
state.session?.abortRetry();
|
|
195
201
|
state.session?.abort().catch(() => {});
|
|
196
202
|
}
|
|
197
203
|
|
|
@@ -215,31 +221,23 @@ export class SubagentSessionRunner implements SubagentRunner {
|
|
|
215
221
|
return false;
|
|
216
222
|
}
|
|
217
223
|
state.session = session;
|
|
224
|
+
session.setSessionName(formatSubagentSessionName(state));
|
|
218
225
|
return true;
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
private async runPromptCycle(state: SubagentState, prompt: string): Promise<void> {
|
|
222
229
|
if (isAborted(state)) return;
|
|
223
230
|
|
|
224
|
-
const abortController = new AbortController();
|
|
225
|
-
state.promptAbortController = abortController;
|
|
226
|
-
|
|
227
231
|
try {
|
|
228
|
-
|
|
232
|
+
await state.session!.prompt(prompt);
|
|
229
233
|
if (isAborted(state)) return;
|
|
230
234
|
|
|
231
235
|
const outcome = getPromptOutcome(state);
|
|
232
|
-
if (recovery === "failed" && outcome.status !== "error") {
|
|
233
|
-
this.callbacks.onSettled(state, "error", { error: "Context overflow recovery failed" });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
236
|
this.callbacks.onSettled(state, outcome.status, outcome);
|
|
237
237
|
} catch (err) {
|
|
238
238
|
if (isAborted(state)) return;
|
|
239
239
|
const error = err instanceof Error ? err.message : String(err);
|
|
240
240
|
this.callbacks.onSettled(state, "error", { error });
|
|
241
|
-
} finally {
|
|
242
|
-
state.promptAbortController = undefined;
|
|
243
241
|
}
|
|
244
242
|
}
|
|
245
243
|
|
package/extension/tools.ts
CHANGED
|
@@ -175,24 +175,29 @@ export function registerCrewTools(pi: ExtensionAPI, crew: CrewRuntime, extension
|
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
registerActionTool<{ subagent: string; task: string }>(pi, {
|
|
178
|
+
registerActionTool<{ subagent: string; brief: string; task: string }>(pi, {
|
|
179
179
|
name: "crew_spawn",
|
|
180
180
|
label: "Spawn Crew",
|
|
181
181
|
description:
|
|
182
182
|
"Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while your session stays interactive. Results are delivered back to your session as steering messages.",
|
|
183
183
|
parameters: Type.Object({
|
|
184
184
|
subagent: Type.String({ description: "Subagent name from crew_list" }),
|
|
185
|
-
|
|
185
|
+
brief: Type.String({ description: "Concise task label for session lists, ideally under 80 characters. This is not the full task." }),
|
|
186
|
+
task: Type.String({ description: "Full self-contained task to delegate to the subagent" }),
|
|
186
187
|
}),
|
|
187
188
|
promptSnippet: "Spawn a non-blocking subagent. Use crew_list first to see available subagents.",
|
|
188
189
|
promptGuidelines: [
|
|
189
190
|
"crew_spawn: Spawn a discovered subagent for one clearly delegated, self-contained task.",
|
|
190
|
-
"crew_spawn:
|
|
191
|
+
"crew_spawn: Provide brief as a concise human-readable task label for session lists, ideally under 80 characters; do not put the full task there.",
|
|
192
|
+
"crew_spawn: Include only needed context in task: constraints, relevant files, acceptance criteria, and expected output.",
|
|
191
193
|
"crew_spawn: After spawning, ownership transfers to the subagent; do not work on that task yourself.",
|
|
192
194
|
"crew_spawn: Results arrive as steering messages; do not poll crew_list or fabricate results.",
|
|
193
195
|
"crew_spawn: Use the bundled pi-crew skill for detailed delegation patterns.",
|
|
194
196
|
],
|
|
195
197
|
action: (params, ctx) => {
|
|
198
|
+
const brief = params.brief.trim();
|
|
199
|
+
if (!brief) return toolError("brief is required and must not be empty.");
|
|
200
|
+
|
|
196
201
|
const toolCtx = getToolContext(ctx);
|
|
197
202
|
const { agents, warnings } = discoverAgents(toolCtx.cwd);
|
|
198
203
|
notifyDiscoveryWarnings(ctx, shownDiscoveryWarnings, warnings);
|
|
@@ -208,6 +213,7 @@ export function registerCrewTools(pi: ExtensionAPI, crew: CrewRuntime, extension
|
|
|
208
213
|
toolCtx.cwd,
|
|
209
214
|
toolCtx.callerSessionId,
|
|
210
215
|
{
|
|
216
|
+
brief,
|
|
211
217
|
model: ctx.model,
|
|
212
218
|
modelRegistry: ctx.modelRegistry,
|
|
213
219
|
agentDir: getAgentDir(),
|
|
@@ -218,11 +224,13 @@ export function registerCrewTools(pi: ExtensionAPI, crew: CrewRuntime, extension
|
|
|
218
224
|
);
|
|
219
225
|
return toolSuccess(
|
|
220
226
|
`Subagent '${subagent.name}' spawned as ${id}. Result will be delivered as a steering message when done.`,
|
|
221
|
-
{ id, agentName: subagent.name, task: params.task },
|
|
227
|
+
{ id, agentName: subagent.name, brief, task: params.task },
|
|
222
228
|
);
|
|
223
229
|
},
|
|
224
230
|
renderCall(args, theme, _context) {
|
|
225
|
-
|
|
231
|
+
const subagent = args.subagent || "...";
|
|
232
|
+
const title = args.brief ? `${subagent} · ${args.brief}` : subagent;
|
|
233
|
+
return renderCrewCall(theme, "crew_spawn", title, args.task);
|
|
226
234
|
},
|
|
227
235
|
});
|
|
228
236
|
|
package/extension/ui.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
1
2
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
3
|
import {
|
|
3
4
|
type ExtensionAPI,
|
|
@@ -104,23 +105,6 @@ export function sendSteeringMessage(
|
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
export function sendRemainingNote(
|
|
108
|
-
remainingCount: number,
|
|
109
|
-
sendMessage: SendMessageFn,
|
|
110
|
-
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
111
|
-
): void {
|
|
112
|
-
if (remainingCount <= 0) return;
|
|
113
|
-
sendWithDeliveryPolicy(
|
|
114
|
-
{
|
|
115
|
-
customType: "crew-remaining",
|
|
116
|
-
content: `⏳ ${remainingCount} subagent(s) still running`,
|
|
117
|
-
display: true,
|
|
118
|
-
},
|
|
119
|
-
sendMessage,
|
|
120
|
-
opts,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
108
|
export function sendCrewListActiveWarning(
|
|
125
109
|
sendMessage: SendMessageFn,
|
|
126
110
|
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
@@ -161,6 +145,11 @@ function renderWarningMessage(content: unknown, theme: MessageRendererTheme): Bo
|
|
|
161
145
|
return box;
|
|
162
146
|
}
|
|
163
147
|
|
|
148
|
+
function linkFilePath(filePath: string): string {
|
|
149
|
+
const url = pathToFileURL(filePath).href;
|
|
150
|
+
return `\x1b]8;;${url}\x07${filePath}\x1b]8;;\x07`;
|
|
151
|
+
}
|
|
152
|
+
|
|
164
153
|
export function registerCrewMessageRenderers(pi: ExtensionAPI): void {
|
|
165
154
|
pi.registerMessageRenderer("crew-result", (message, { expanded }, theme) => {
|
|
166
155
|
const details = message.details as CrewResultMessageDetails | undefined;
|
|
@@ -175,7 +164,7 @@ export function registerCrewMessageRenderers(pi: ExtensionAPI): void {
|
|
|
175
164
|
box.addChild(new Text(header, 0, 0));
|
|
176
165
|
|
|
177
166
|
if (details?.sessionFile) {
|
|
178
|
-
box.addChild(new Text(theme.fg("muted", `📁 ${details.sessionFile}`), 0, 0));
|
|
167
|
+
box.addChild(new Text(theme.fg("muted", `📁 ${linkFilePath(details.sessionFile)}`), 0, 0));
|
|
179
168
|
}
|
|
180
169
|
|
|
181
170
|
if (body) {
|
|
@@ -193,7 +182,6 @@ export function registerCrewMessageRenderers(pi: ExtensionAPI): void {
|
|
|
193
182
|
return box;
|
|
194
183
|
});
|
|
195
184
|
|
|
196
|
-
pi.registerMessageRenderer("crew-remaining", (message, _options, theme) => renderWarningMessage(message.content, theme));
|
|
197
185
|
pi.registerMessageRenderer("crew-list-warning", (message, _options, theme) => renderWarningMessage(message.content, theme));
|
|
198
186
|
}
|
|
199
187
|
|
|
@@ -260,7 +248,7 @@ function syncWidgetText(state: WidgetState, agents: ActiveAgentSummary[]): void
|
|
|
260
248
|
}
|
|
261
249
|
|
|
262
250
|
export function updateWidget(ctx: ExtensionContext, crew: CrewRuntime): void {
|
|
263
|
-
if (
|
|
251
|
+
if (ctx.mode !== "tui") {
|
|
264
252
|
clearWidget();
|
|
265
253
|
return;
|
|
266
254
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melihmucuk/pi-crew",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Non-blocking subagent orchestration for pi coding agent",
|
|
6
6
|
"files": [
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"typebox": "*"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
47
|
-
"@earendil-works/pi-ai": "^0.
|
|
48
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
49
|
-
"@earendil-works/pi-tui": "^0.
|
|
46
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
47
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
48
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
49
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
50
50
|
"@types/node": "^22.19.17",
|
|
51
51
|
"tsx": "^4.22.3",
|
|
52
|
-
"typebox": "^1.1
|
|
52
|
+
"typebox": "^1.2.1",
|
|
53
53
|
"typescript": "^5.9.3"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -10,7 +10,7 @@ You are a review orchestrator, not a reviewer. Resolve the review scope, gather
|
|
|
10
10
|
|
|
11
11
|
## Scope
|
|
12
12
|
|
|
13
|
-
Use the user's scope when provided. Otherwise rely on each reviewer’s default scope. If “latest” or “recent” is requested, review the last 5 commits unless a count is given.
|
|
13
|
+
Use the user's scope when provided. Otherwise rely on each reviewer’s default scope. If “latest” or “recent” is requested, review the last 5 commits unless a count is given. If “full”, “codebase”, or whole-repo review is requested, treat it as an explicit non-default scope and pass that scope to reviewers.
|
|
14
14
|
|
|
15
15
|
Gather minimal review context: why the changes were made, expected behavior/outcome, feature or bug intent, notable fixes since any prior review, verification already run, and user instructions that are specific to this review.
|
|
16
16
|
|
|
@@ -33,6 +33,8 @@ If you include a Goal, make it specific to the change intent, not the reviewer r
|
|
|
33
33
|
|
|
34
34
|
For default reviews, do not include a Scope section or mention uncommitted/current repo changes in the subagent brief unless needed to disambiguate scope. If you need to state task-specific emphasis, use `Review focus:` instead of `Scope:`.
|
|
35
35
|
|
|
36
|
+
For full/codebase requests, state that the requested scope is a bounded full-codebase review.
|
|
37
|
+
|
|
36
38
|
Do not echo the raw user instruction if it is already represented in the intent summary; quote it only when exact wording matters.
|
|
37
39
|
|
|
38
40
|
Do not restate reviewer-role boilerplate implied by the selected reviewer, such as telling `code-reviewer` to find actionable bugs or telling `quality-reviewer` to review maintainability. Do not include default scope, generic non-goals, acceptance criteria, output format, edit permissions, or severity rules unless the user explicitly overrides them.
|
|
@@ -49,26 +51,33 @@ You may do a minimal spot-check only when a finding is ambiguous, high-impact, o
|
|
|
49
51
|
|
|
50
52
|
Reply in the user's language. Apply the gate before merging.
|
|
51
53
|
|
|
54
|
+
For each accepted finding, preserve enough detail to act without reading subagent logs:
|
|
55
|
+
|
|
56
|
+
**[SEVERITY] Category: Title**
|
|
57
|
+
Source: `code-reviewer` | `quality-reviewer` | `both`
|
|
58
|
+
File: `path:line`
|
|
59
|
+
Issue: what is wrong
|
|
60
|
+
Evidence: what was verified
|
|
61
|
+
Impact: concrete consequence
|
|
62
|
+
Fix: specific suggested correction
|
|
63
|
+
|
|
64
|
+
Do not forward findings as summaries only. If evidence, location, or fix is missing and cannot be inferred from the reviewer result, omit the finding or report it as insufficiently evidenced.
|
|
65
|
+
|
|
52
66
|
Sections:
|
|
53
67
|
|
|
54
|
-
###
|
|
55
|
-
|
|
68
|
+
### Findings
|
|
69
|
+
List all accepted findings in severity order. Use `Source:` to identify `code-reviewer`, `quality-reviewer`, or `both`.
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
Accepted findings only from `code-reviewer`.
|
|
71
|
+
If both reviewers report no accepted findings, write only:
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
Accepted findings only from `quality-reviewer`.
|
|
73
|
+
No accepted findings.
|
|
62
74
|
|
|
63
|
-
###
|
|
64
|
-
-
|
|
65
|
-
- Reviewers
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
- Quality review findings count
|
|
69
|
-
- Overall assessment
|
|
75
|
+
### Summary
|
|
76
|
+
- Scope: [review scope]
|
|
77
|
+
- Reviewers: [completed reviewers and any failures]
|
|
78
|
+
- Findings: [count by severity]
|
|
79
|
+
- Result: [one-sentence overall assessment]
|
|
70
80
|
|
|
71
81
|
Rules:
|
|
72
82
|
- Do not repeat overlapping findings.
|
|
73
|
-
-
|
|
74
|
-
- If both reviewers report no accepted findings, say so clearly.
|
|
83
|
+
- Mark a finding as `Source: both` only when both reviewers clearly reported the same issue.
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Delegation Checklist
|
|
4
4
|
|
|
5
|
-
Before `crew_spawn`,
|
|
5
|
+
Before `crew_spawn`, provide:
|
|
6
|
+
|
|
7
|
+
- `brief`: a concise human-readable task label for session lists, ideally under 80 characters. Use a few words for intent/outcome; do not include the full task, acceptance criteria, long paths, secrets, or mechanical repo state.
|
|
8
|
+
- `task`: a self-contained delegated task body, not mechanically templated.
|
|
9
|
+
|
|
10
|
+
In `task`, include only information that helps this specific subagent do this specific task:
|
|
6
11
|
|
|
7
12
|
- Intent, expected outcome, and relevant user decisions.
|
|
8
13
|
- User-provided references, plus a concise summary after reading them when practical.
|
|
@@ -76,7 +81,7 @@ If ownership overlaps, serialize the work.
|
|
|
76
81
|
## Tool Notes
|
|
77
82
|
|
|
78
83
|
- `crew_list`: discovery before a new spawn decision or requested status snapshot; never completion polling.
|
|
79
|
-
- `crew_spawn`: self-contained
|
|
84
|
+
- `crew_spawn`: provide `brief` plus a self-contained `task`; ownership transfers after spawn.
|
|
80
85
|
- `crew_respond`: send a follow-up to a waiting interactive subagent; fire-and-forget.
|
|
81
86
|
- `crew_done`: close a waiting interactive subagent when complete.
|
|
82
87
|
- `crew_abort`: abort active owned subagents only when obsolete, wrong, or cancelled.
|
package/skills/pi-crew/SKILL.md
CHANGED
|
@@ -21,6 +21,11 @@ See [REFERENCE.md](REFERENCE.md) for examples and detailed handling patterns.
|
|
|
21
21
|
|
|
22
22
|
## Spawn Brief
|
|
23
23
|
|
|
24
|
+
Every `crew_spawn` requires both `brief` and `task`:
|
|
25
|
+
|
|
26
|
+
- `brief`: concise human-readable task label for session lists, ideally under 80 characters. Write the intent/outcome in a few words; do not include the full task, acceptance criteria, long paths, secrets, or mechanical repo state.
|
|
27
|
+
- `task`: self-contained delegated work body with the context the subagent needs.
|
|
28
|
+
|
|
24
29
|
Send a self-contained task, but do not fill a template mechanically. Use only sections that add task-specific value, for example:
|
|
25
30
|
|
|
26
31
|
```md
|
|
@@ -35,6 +40,8 @@ Omit sections that would only restate the selected subagent’s role, default sc
|
|
|
35
40
|
|
|
36
41
|
Include only information that helps this specific subagent do this specific task: intent, expected outcome, relevant decisions, exact errors/output, unusual constraints, and file paths or entry points that genuinely clarify the task. Use short Markdown sections and bullets when they improve scanability, especially for multi-part intent, constraints, observations, requirements, or acceptance criteria; avoid dense paragraphs.
|
|
37
42
|
|
|
43
|
+
For repeated workflows, make each task independent. Do not assume a new subagent knows earlier loop results, owner-session discussion, or what another subagent saw. If prior findings, fixes, decisions, or verification matter, summarize the concrete facts or point to durable artifacts the subagent can inspect. Avoid vague references like “we fixed the first review findings” unless you also state what those findings/fixes were or define the current review target without relying on that history.
|
|
44
|
+
|
|
38
45
|
Do not restate boilerplate implied by the selected subagent’s role, name, or description. Avoid repeating default scope, output format, edit permissions, or repo guidance. Subagents run in the same cwd as the orchestrator, so do not include mechanical Git state they can inspect themselves, such as full changed-file lists, staged/unstaged/untracked inventories, branch/cwd details, or generic project constraints, unless those details define a non-default scope or prevent ambiguity.
|
|
39
46
|
|
|
40
47
|
If the user points to a plan, spec, issue, design, or doc as task intent, read it when practical and summarize the relevant intent instead of merely passing the path. Prefer explaining why the work matters and what outcome is expected over restating repository state.
|
|
@@ -44,7 +51,7 @@ If the user points to a plan, spec, issue, design, or doc as task intent, read i
|
|
|
44
51
|
- Wait for subagent results before using them. Never invent or predict results.
|
|
45
52
|
- Evaluate each result against the task acceptance criteria.
|
|
46
53
|
- If results conflict, are incomplete, or miss criteria, state that clearly and use a follow-up or new spawn only when needed.
|
|
47
|
-
- After spawning, continue only with unrelated work or end the turn.
|
|
54
|
+
- After spawning, do not work on the delegated task; wait for results, continue only with unrelated work, or end the turn.
|
|
48
55
|
|
|
49
56
|
## Interactive Subagents
|
|
50
57
|
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
const OVERFLOW_RECOVERY_TIMEOUT_MS = 120_000;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Short grace period for the first terminal agent_end after prompt() resolves.
|
|
7
|
-
* If this window expires, we still wait the full recovery timeout.
|
|
8
|
-
*/
|
|
9
|
-
const INITIAL_AGENT_END_WAIT_MS = 5_000;
|
|
10
|
-
|
|
11
|
-
type PhaseWaitResult = "done" | "timeout" | "cancelled";
|
|
12
|
-
|
|
13
|
-
export type OverflowRecoveryResult = "none" | "recovered" | "failed";
|
|
14
|
-
|
|
15
|
-
interface DeferredPhase {
|
|
16
|
-
promise: Promise<void>;
|
|
17
|
-
resolve: () => void;
|
|
18
|
-
isDone: () => boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function createDeferredPhase(): DeferredPhase {
|
|
22
|
-
let done = false;
|
|
23
|
-
let resolveFn: (() => void) | undefined;
|
|
24
|
-
|
|
25
|
-
const promise = new Promise<void>((resolve) => {
|
|
26
|
-
resolveFn = () => {
|
|
27
|
-
if (done) return;
|
|
28
|
-
done = true;
|
|
29
|
-
resolve();
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
promise,
|
|
35
|
-
resolve: () => resolveFn?.(),
|
|
36
|
-
isDone: () => done,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
class OverflowRecoveryTracker {
|
|
41
|
-
private overflowDetected = false;
|
|
42
|
-
private compactionWillRetry = false;
|
|
43
|
-
|
|
44
|
-
private autoRetryActive = false;
|
|
45
|
-
private readonly initialAgentEnd = createDeferredPhase();
|
|
46
|
-
private compactionEnd: DeferredPhase | undefined;
|
|
47
|
-
private retryAgentEnd: DeferredPhase | undefined;
|
|
48
|
-
private overflowAutoRetryEnd: DeferredPhase | undefined;
|
|
49
|
-
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
50
|
-
|
|
51
|
-
handleEvent(event: AgentSessionEvent): void {
|
|
52
|
-
switch (event.type) {
|
|
53
|
-
case "agent_end":
|
|
54
|
-
this.onAgentEnd();
|
|
55
|
-
break;
|
|
56
|
-
case "compaction_start":
|
|
57
|
-
this.onCompactionStart(event.reason);
|
|
58
|
-
break;
|
|
59
|
-
case "compaction_end":
|
|
60
|
-
this.onCompactionEnd(event.reason, event.willRetry);
|
|
61
|
-
break;
|
|
62
|
-
case "auto_retry_start":
|
|
63
|
-
this.onAutoRetryStart();
|
|
64
|
-
break;
|
|
65
|
-
case "auto_retry_end":
|
|
66
|
-
this.onAutoRetryEnd();
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async awaitCompletion(signal: AbortSignal): Promise<OverflowRecoveryResult> {
|
|
74
|
-
const cancelPromise = new Promise<void>((resolve) => {
|
|
75
|
-
if (signal.aborted) {
|
|
76
|
-
resolve();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
let initialEnd = await this.waitForPhase(
|
|
84
|
-
this.initialAgentEnd.promise,
|
|
85
|
-
INITIAL_AGENT_END_WAIT_MS,
|
|
86
|
-
cancelPromise,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
if (initialEnd === "timeout") {
|
|
90
|
-
initialEnd = await this.waitForPhase(
|
|
91
|
-
this.initialAgentEnd.promise,
|
|
92
|
-
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
93
|
-
cancelPromise,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (initialEnd !== "done") {
|
|
98
|
-
return this.overflowDetected ? "failed" : "none";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!this.overflowDetected) return "none";
|
|
102
|
-
|
|
103
|
-
if (this.compactionEnd) {
|
|
104
|
-
const compactionEnd = await this.waitForPhase(
|
|
105
|
-
this.compactionEnd.promise,
|
|
106
|
-
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
107
|
-
cancelPromise,
|
|
108
|
-
);
|
|
109
|
-
if (compactionEnd !== "done") return "failed";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!this.compactionWillRetry) return "failed";
|
|
113
|
-
|
|
114
|
-
if (this.retryAgentEnd) {
|
|
115
|
-
const retryEnd = await this.waitForPhase(
|
|
116
|
-
this.retryAgentEnd.promise,
|
|
117
|
-
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
118
|
-
cancelPromise,
|
|
119
|
-
);
|
|
120
|
-
if (retryEnd !== "done") return "failed";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (this.overflowAutoRetryEnd) {
|
|
124
|
-
const autoRetryEnd = await this.waitForPhase(
|
|
125
|
-
this.overflowAutoRetryEnd.promise,
|
|
126
|
-
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
127
|
-
cancelPromise,
|
|
128
|
-
);
|
|
129
|
-
if (autoRetryEnd !== "done") return "failed";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return "recovered";
|
|
133
|
-
} finally {
|
|
134
|
-
for (const timer of this.timers) clearTimeout(timer);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private async waitForPhase(
|
|
139
|
-
phasePromise: Promise<void>,
|
|
140
|
-
timeoutMs: number,
|
|
141
|
-
cancelPromise: Promise<void>,
|
|
142
|
-
): Promise<PhaseWaitResult> {
|
|
143
|
-
return Promise.race([
|
|
144
|
-
phasePromise.then(() => "done" as const),
|
|
145
|
-
cancelPromise.then(() => "cancelled" as const),
|
|
146
|
-
new Promise<"timeout">((resolve) => {
|
|
147
|
-
this.timers.push(setTimeout(() => resolve("timeout"), timeoutMs));
|
|
148
|
-
}),
|
|
149
|
-
]);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// agent_end can be followed immediately by auto_retry_start in the same
|
|
153
|
-
// _processAgentEvent tick. Resolve on microtask so we can ignore retrying
|
|
154
|
-
// attempts and only accept terminal agent_end events.
|
|
155
|
-
private onAgentEnd(): void {
|
|
156
|
-
queueMicrotask(() => {
|
|
157
|
-
if (this.autoRetryActive) return;
|
|
158
|
-
|
|
159
|
-
if (!this.initialAgentEnd.isDone()) {
|
|
160
|
-
this.initialAgentEnd.resolve();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
this.retryAgentEnd?.resolve();
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private onCompactionStart(reason: "manual" | "threshold" | "overflow"): void {
|
|
169
|
-
if (reason !== "overflow") return;
|
|
170
|
-
this.overflowDetected = true;
|
|
171
|
-
this.compactionEnd ??= createDeferredPhase();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private onCompactionEnd(reason: "manual" | "threshold" | "overflow", willRetry: boolean): void {
|
|
175
|
-
if (reason !== "overflow") return;
|
|
176
|
-
|
|
177
|
-
this.compactionWillRetry = willRetry;
|
|
178
|
-
if (willRetry) {
|
|
179
|
-
this.retryAgentEnd ??= createDeferredPhase();
|
|
180
|
-
}
|
|
181
|
-
this.compactionEnd?.resolve();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private onAutoRetryStart(): void {
|
|
185
|
-
this.autoRetryActive = true;
|
|
186
|
-
if (this.overflowDetected) {
|
|
187
|
-
this.overflowAutoRetryEnd ??= createDeferredPhase();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private onAutoRetryEnd(): void {
|
|
192
|
-
this.autoRetryActive = false;
|
|
193
|
-
this.overflowAutoRetryEnd?.resolve();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export async function runPromptWithOverflowRecovery(
|
|
198
|
-
session: AgentSession,
|
|
199
|
-
text: string,
|
|
200
|
-
signal: AbortSignal,
|
|
201
|
-
): Promise<OverflowRecoveryResult> {
|
|
202
|
-
const tracker = new OverflowRecoveryTracker();
|
|
203
|
-
const unsubscribe = session.subscribe((event) => tracker.handleEvent(event));
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
await session.prompt(text);
|
|
207
|
-
return await tracker.awaitCompletion(signal);
|
|
208
|
-
} finally {
|
|
209
|
-
unsubscribe();
|
|
210
|
-
}
|
|
211
|
-
}
|