@melihmucuk/pi-crew 1.0.7 → 1.0.9
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/agents/code-reviewer.md +18 -6
- package/agents/planner.md +9 -1
- package/agents/quality-reviewer.md +20 -13
- package/agents/scout.md +33 -26
- package/dist/agent-discovery.d.ts +0 -5
- package/dist/agent-discovery.js +1 -1
- package/dist/bootstrap-session.d.ts +13 -4
- package/dist/bootstrap-session.js +25 -16
- package/dist/index.js +37 -7
- package/dist/integration/register-command.d.ts +2 -2
- package/dist/integration/register-command.js +5 -5
- package/dist/integration/register-renderers.js +3 -0
- package/dist/integration/register-tools.d.ts +2 -2
- package/dist/integration/register-tools.js +2 -2
- package/dist/integration/tool-presentation.d.ts +2 -3
- package/dist/integration/tool-presentation.js +7 -8
- package/dist/integration/tools/crew-abort.d.ts +1 -1
- package/dist/integration/tools/crew-abort.js +3 -3
- package/dist/integration/tools/crew-done.d.ts +1 -1
- package/dist/integration/tools/crew-done.js +2 -2
- package/dist/integration/tools/crew-list.d.ts +1 -1
- package/dist/integration/tools/crew-list.js +3 -3
- package/dist/integration/tools/crew-respond.d.ts +1 -1
- package/dist/integration/tools/crew-respond.js +6 -7
- package/dist/integration/tools/crew-spawn.d.ts +1 -1
- package/dist/integration/tools/crew-spawn.js +17 -14
- package/dist/integration/tools/tool-deps.d.ts +3 -2
- package/dist/integration.d.ts +2 -2
- package/dist/integration.js +3 -3
- package/dist/runtime/crew-runtime.d.ts +61 -0
- package/dist/{crew-manager.js → runtime/crew-runtime.js} +84 -58
- package/dist/runtime/delivery-coordinator.d.ts +16 -7
- package/dist/runtime/delivery-coordinator.js +46 -20
- package/dist/runtime/subagent-registry.d.ts +1 -0
- package/dist/runtime/subagent-registry.js +3 -0
- package/dist/runtime/subagent-state.d.ts +2 -0
- package/dist/status-widget.d.ts +2 -2
- package/dist/status-widget.js +3 -3
- package/dist/subagent-messages.d.ts +5 -2
- package/dist/subagent-messages.js +5 -4
- package/docs/architecture.md +106 -843
- package/package.json +1 -1
- package/prompts/pi-crew-plan.md +82 -123
- package/prompts/pi-crew-review.md +64 -115
- package/dist/crew-manager.d.ts +0 -44
|
@@ -2,18 +2,18 @@ import { Text } from "@mariozechner/pi-tui";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { discoverAgents } from "../../agent-discovery.js";
|
|
4
4
|
import { STATUS_ICON } from "../../subagent-messages.js";
|
|
5
|
-
export function registerCrewListTool({ pi,
|
|
5
|
+
export function registerCrewListTool({ pi, crew, notifyDiscoveryWarnings, }) {
|
|
6
6
|
pi.registerTool({
|
|
7
7
|
name: "crew_list",
|
|
8
8
|
label: "List Crew",
|
|
9
|
-
description: "List available subagent definitions
|
|
9
|
+
description: "List available subagent definitions and currently running subagents with their status.",
|
|
10
10
|
parameters: Type.Object({}),
|
|
11
11
|
promptSnippet: "List subagent definitions and active subagents",
|
|
12
12
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
13
13
|
const { agents, warnings } = discoverAgents(ctx.cwd);
|
|
14
14
|
notifyDiscoveryWarnings(ctx, warnings);
|
|
15
15
|
const callerSessionId = ctx.sessionManager.getSessionId();
|
|
16
|
-
const running =
|
|
16
|
+
const running = crew.getActiveSummariesForOwner(callerSessionId);
|
|
17
17
|
const lines = [];
|
|
18
18
|
lines.push("## Available subagents");
|
|
19
19
|
if (agents.length === 0) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CrewToolDeps } from "./tool-deps.js";
|
|
2
|
-
export declare function registerCrewRespondTool({ pi,
|
|
2
|
+
export declare function registerCrewRespondTool({ pi, crew }: CrewToolDeps): void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { renderCrewCall, renderCrewResult, toolError, toolSuccess,
|
|
3
|
-
export function registerCrewRespondTool({ pi,
|
|
2
|
+
import { renderCrewCall, renderCrewResult, toolError, toolSuccess, } from "../tool-presentation.js";
|
|
3
|
+
export function registerCrewRespondTool({ pi, crew }) {
|
|
4
4
|
pi.registerTool({
|
|
5
5
|
name: "crew_respond",
|
|
6
6
|
label: "Respond to Crew",
|
|
7
|
-
description: "Send a follow-up message to an interactive subagent that is waiting for a response.
|
|
7
|
+
description: "Send a follow-up message to an interactive subagent that is waiting for a response.",
|
|
8
8
|
parameters: Type.Object({
|
|
9
9
|
subagent_id: Type.String({
|
|
10
10
|
description: "ID of the waiting subagent (from crew_list or crew_spawn result)",
|
|
@@ -14,14 +14,13 @@ export function registerCrewRespondTool({ pi, crewManager }) {
|
|
|
14
14
|
promptSnippet: "Send a follow-up message to a waiting interactive subagent.",
|
|
15
15
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
16
16
|
const callerSessionId = ctx.sessionManager.getSessionId();
|
|
17
|
-
const { error } =
|
|
17
|
+
const { error } = crew.respond(params.subagent_id, params.message, callerSessionId);
|
|
18
18
|
if (error)
|
|
19
19
|
return toolError(error);
|
|
20
|
-
return toolSuccess(`Message sent to subagent ${params.subagent_id}. Response will be delivered as a steering message.`, { id: params.subagent_id });
|
|
20
|
+
return toolSuccess(`Message sent to subagent ${params.subagent_id}. Response will be delivered as a steering message.`, { id: params.subagent_id, message: params.message });
|
|
21
21
|
},
|
|
22
22
|
renderCall(args, theme, _context) {
|
|
23
|
-
|
|
24
|
-
return renderCrewCall(theme, "crew_respond", args.subagent_id || "...", preview);
|
|
23
|
+
return renderCrewCall(theme, "crew_respond", args.subagent_id || "...", args.message);
|
|
25
24
|
},
|
|
26
25
|
renderResult(result, _options, theme, _context) {
|
|
27
26
|
return renderCrewResult(result, theme);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CrewToolDeps } from "./tool-deps.js";
|
|
2
|
-
export declare function registerCrewSpawnTool({ pi,
|
|
2
|
+
export declare function registerCrewSpawnTool({ pi, crew, extensionDir, notifyDiscoveryWarnings, }: CrewToolDeps): void;
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { discoverAgents } from "../../agent-discovery.js";
|
|
3
|
-
import { renderCrewCall, renderCrewResult, toolError, toolSuccess,
|
|
4
|
-
export function registerCrewSpawnTool({ pi,
|
|
3
|
+
import { renderCrewCall, renderCrewResult, toolError, toolSuccess, } from "../tool-presentation.js";
|
|
4
|
+
export function registerCrewSpawnTool({ pi, crew, extensionDir, notifyDiscoveryWarnings, }) {
|
|
5
5
|
pi.registerTool({
|
|
6
6
|
name: "crew_spawn",
|
|
7
7
|
label: "Spawn Crew",
|
|
8
|
-
description: "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
|
|
8
|
+
description: "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.",
|
|
9
9
|
parameters: Type.Object({
|
|
10
10
|
subagent: Type.String({ description: "Subagent name from crew_list" }),
|
|
11
11
|
task: Type.String({ description: "Task to delegate to the subagent" }),
|
|
12
12
|
}),
|
|
13
13
|
promptSnippet: "Spawn a non-blocking subagent. Use crew_list first to see available subagents.",
|
|
14
14
|
promptGuidelines: [
|
|
15
|
-
"Use
|
|
16
|
-
"crew_spawn:
|
|
17
|
-
"crew_spawn:
|
|
18
|
-
"crew_spawn:
|
|
19
|
-
"crew_spawn:
|
|
20
|
-
"crew_spawn:
|
|
21
|
-
"crew_spawn: Interactive subagents (marked with 'interactive' in crew_list) stay alive after responding. Use crew_respond to continue the conversation and crew_done to close when finished.",
|
|
15
|
+
"Use crew_list first to see available subagents before spawning.",
|
|
16
|
+
"crew_spawn: The subagent runs in isolation with no access to your session. Include file paths, requirements, and known locations directly in the task parameter.",
|
|
17
|
+
"crew_spawn: DELEGATE means STOP. After spawning, either work on an UNRELATED task or end your turn. Never continue the delegated task yourself.",
|
|
18
|
+
"crew_spawn: To avoid duplication, gather only enough context to write a useful task (key files, entry points). Do not pre-investigate the full problem.",
|
|
19
|
+
"crew_spawn: Results arrive asynchronously as steering messages. Do not predict or fabricate results. Wait for all crew-result messages before acting on them.",
|
|
20
|
+
"crew_spawn: Interactive subagents stay alive after responding. Use crew_respond to continue and crew_done to close when finished.",
|
|
22
21
|
],
|
|
23
22
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
24
23
|
const { agents, warnings } = discoverAgents(ctx.cwd);
|
|
@@ -29,12 +28,16 @@ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings
|
|
|
29
28
|
return toolError(`Unknown subagent: "${params.subagent}". Available: ${available}`);
|
|
30
29
|
}
|
|
31
30
|
const ownerSessionId = ctx.sessionManager.getSessionId();
|
|
32
|
-
const id =
|
|
33
|
-
|
|
31
|
+
const id = crew.spawn(subagent, params.task, ctx.cwd, ownerSessionId, {
|
|
32
|
+
model: ctx.model,
|
|
33
|
+
modelRegistry: ctx.modelRegistry,
|
|
34
|
+
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
35
|
+
onWarning: (msg) => ctx.ui.notify(msg, "warning"),
|
|
36
|
+
}, extensionDir);
|
|
37
|
+
return toolSuccess(`Subagent '${subagent.name}' spawned as ${id}. Result will be delivered as a steering message when done.`, { id, agentName: subagent.name, task: params.task });
|
|
34
38
|
},
|
|
35
39
|
renderCall(args, theme, _context) {
|
|
36
|
-
|
|
37
|
-
return renderCrewCall(theme, "crew_spawn", args.subagent || "...", preview);
|
|
40
|
+
return renderCrewCall(theme, "crew_spawn", args.subagent || "...", args.task);
|
|
38
41
|
},
|
|
39
42
|
renderResult(result, _options, theme, _context) {
|
|
40
43
|
return renderCrewResult(result, theme);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { AgentDiscoveryWarning } from "../../agent-discovery.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { CrewRuntime } from "../../runtime/crew-runtime.js";
|
|
4
4
|
export interface CrewToolDeps {
|
|
5
5
|
pi: ExtensionAPI;
|
|
6
|
-
|
|
6
|
+
crew: CrewRuntime;
|
|
7
|
+
extensionDir: string;
|
|
7
8
|
notifyDiscoveryWarnings: (ctx: ExtensionContext, warnings: AgentDiscoveryWarning[]) => void;
|
|
8
9
|
}
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type {
|
|
3
|
-
export declare function registerCrewIntegration(pi: ExtensionAPI,
|
|
2
|
+
import type { CrewRuntime } from "./runtime/crew-runtime.js";
|
|
3
|
+
export declare function registerCrewIntegration(pi: ExtensionAPI, crew: CrewRuntime, extensionDir: string): void;
|
package/dist/integration.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { registerCrewCommand } from "./integration/register-command.js";
|
|
2
2
|
import { registerCrewMessageRenderers } from "./integration/register-renderers.js";
|
|
3
3
|
import { registerCrewTools } from "./integration/register-tools.js";
|
|
4
|
-
export function registerCrewIntegration(pi,
|
|
5
|
-
registerCrewTools(pi,
|
|
6
|
-
registerCrewCommand(pi,
|
|
4
|
+
export function registerCrewIntegration(pi, crew, extensionDir) {
|
|
5
|
+
registerCrewTools(pi, crew, extensionDir);
|
|
6
|
+
registerCrewCommand(pi, crew);
|
|
7
7
|
registerCrewMessageRenderers(pi);
|
|
8
8
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { AgentConfig } from "../agent-discovery.js";
|
|
4
|
+
import { type ActiveRuntimeBinding } from "./delivery-coordinator.js";
|
|
5
|
+
import { type AbortableAgentSummary, type ActiveAgentSummary } from "./subagent-state.js";
|
|
6
|
+
export type { AbortableAgentSummary, ActiveAgentSummary, } from "./subagent-state.js";
|
|
7
|
+
export interface AbortOwnedResult {
|
|
8
|
+
abortedIds: string[];
|
|
9
|
+
missingIds: string[];
|
|
10
|
+
foreignIds: string[];
|
|
11
|
+
}
|
|
12
|
+
interface AbortOptions {
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SpawnContext {
|
|
16
|
+
model: Model<Api> | undefined;
|
|
17
|
+
modelRegistry: ModelRegistry;
|
|
18
|
+
parentSessionFile?: string;
|
|
19
|
+
onWarning?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Process-level singleton that owns all durable subagent state.
|
|
23
|
+
*
|
|
24
|
+
* This survives extension instance replacement caused by runtime
|
|
25
|
+
* teardown/recreation on /resume, /new, /fork (pi 0.65.0+).
|
|
26
|
+
* Each new extension instance rebinds delivery and widget hooks
|
|
27
|
+
* via activateSession/deactivateSession.
|
|
28
|
+
*/
|
|
29
|
+
declare class CrewRuntime {
|
|
30
|
+
private readonly registry;
|
|
31
|
+
private readonly delivery;
|
|
32
|
+
private readonly refreshCallbacks;
|
|
33
|
+
private refreshWidgetFor;
|
|
34
|
+
activateSession(binding: ActiveRuntimeBinding, refreshWidget?: () => void): void;
|
|
35
|
+
deactivateSession(sessionId: string): void;
|
|
36
|
+
spawn(agentConfig: AgentConfig, task: string, cwd: string, ownerSessionId: string, ctx: SpawnContext, extensionResolvedPath: string): string;
|
|
37
|
+
private attachSessionListeners;
|
|
38
|
+
private attachSpawnedSession;
|
|
39
|
+
private settleAgent;
|
|
40
|
+
private disposeAgent;
|
|
41
|
+
private runPromptCycle;
|
|
42
|
+
private spawnSession;
|
|
43
|
+
respond(id: string, message: string, callerSessionId: string): {
|
|
44
|
+
error?: string;
|
|
45
|
+
};
|
|
46
|
+
done(id: string, callerSessionId: string): {
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
abort(id: string, opts: AbortOptions): boolean;
|
|
50
|
+
abortOwned(ids: string[], callerSessionId: string, opts: AbortOptions): AbortOwnedResult;
|
|
51
|
+
abortAllOwned(callerSessionId: string, opts: AbortOptions): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Abort all running subagents (process-level cleanup).
|
|
54
|
+
* Called from process exit hooks.
|
|
55
|
+
*/
|
|
56
|
+
abortAll(): void;
|
|
57
|
+
getAbortableAgents(): AbortableAgentSummary[];
|
|
58
|
+
getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[];
|
|
59
|
+
}
|
|
60
|
+
export declare const crewRuntime: CrewRuntime;
|
|
61
|
+
export type { CrewRuntime };
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { bootstrapSession } from "
|
|
2
|
-
import { DeliveryCoordinator } from "./
|
|
3
|
-
import { runPromptWithOverflowRecovery } from "./
|
|
4
|
-
import { SubagentRegistry } from "./
|
|
5
|
-
import { isAbortableStatus, isAborted, } from "./
|
|
1
|
+
import { bootstrapSession } from "../bootstrap-session.js";
|
|
2
|
+
import { DeliveryCoordinator } from "./delivery-coordinator.js";
|
|
3
|
+
import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
|
|
4
|
+
import { SubagentRegistry } from "./subagent-registry.js";
|
|
5
|
+
import { isAbortableStatus, isAborted, } from "./subagent-state.js";
|
|
6
|
+
function toBootstrapContext(ctx) {
|
|
7
|
+
return {
|
|
8
|
+
model: ctx.model,
|
|
9
|
+
modelRegistry: ctx.modelRegistry,
|
|
10
|
+
parentSessionFile: ctx.parentSessionFile,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
6
13
|
function getLastAssistantMessage(messages) {
|
|
7
14
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
8
15
|
const msg = messages[i];
|
|
@@ -43,26 +50,41 @@ function getPromptOutcome(state) {
|
|
|
43
50
|
result: text ?? "(no output)",
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Process-level singleton that owns all durable subagent state.
|
|
55
|
+
*
|
|
56
|
+
* This survives extension instance replacement caused by runtime
|
|
57
|
+
* teardown/recreation on /resume, /new, /fork (pi 0.65.0+).
|
|
58
|
+
* Each new extension instance rebinds delivery and widget hooks
|
|
59
|
+
* via activateSession/deactivateSession.
|
|
60
|
+
*/
|
|
61
|
+
class CrewRuntime {
|
|
48
62
|
registry = new SubagentRegistry();
|
|
49
63
|
delivery = new DeliveryCoordinator();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
64
|
+
// Per-session refresh callbacks, keyed by ownerSessionId
|
|
65
|
+
refreshCallbacks = new Map();
|
|
66
|
+
refreshWidgetFor(sessionId) {
|
|
67
|
+
this.refreshCallbacks.get(sessionId)?.();
|
|
54
68
|
}
|
|
55
|
-
activateSession(
|
|
56
|
-
|
|
69
|
+
activateSession(binding, refreshWidget) {
|
|
70
|
+
if (refreshWidget) {
|
|
71
|
+
this.refreshCallbacks.set(binding.sessionId, refreshWidget);
|
|
72
|
+
}
|
|
73
|
+
this.delivery.activateSession(binding, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
74
|
+
refreshWidget?.();
|
|
75
|
+
}
|
|
76
|
+
deactivateSession(sessionId) {
|
|
77
|
+
this.delivery.deactivateSession(sessionId);
|
|
78
|
+
this.refreshCallbacks.delete(sessionId);
|
|
57
79
|
}
|
|
58
|
-
spawn(agentConfig, task, cwd, ownerSessionId, ctx,
|
|
80
|
+
spawn(agentConfig, task, cwd, ownerSessionId, ctx, extensionResolvedPath) {
|
|
59
81
|
const state = this.registry.create(agentConfig, task, ownerSessionId);
|
|
60
|
-
this.
|
|
61
|
-
void this.spawnSession(state, cwd, ctx
|
|
82
|
+
this.refreshWidgetFor(ownerSessionId);
|
|
83
|
+
void this.spawnSession(state, cwd, ctx, extensionResolvedPath);
|
|
62
84
|
return state.id;
|
|
63
85
|
}
|
|
64
86
|
attachSessionListeners(state, session) {
|
|
65
|
-
session.subscribe((event) => {
|
|
87
|
+
state.unsubscribe = session.subscribe((event) => {
|
|
66
88
|
if (event.type !== "turn_end")
|
|
67
89
|
return;
|
|
68
90
|
state.turns++;
|
|
@@ -72,7 +94,7 @@ export class CrewManager {
|
|
|
72
94
|
state.contextTokens = assistantMsg.usage.totalTokens;
|
|
73
95
|
state.model = assistantMsg.model;
|
|
74
96
|
}
|
|
75
|
-
this.
|
|
97
|
+
this.refreshWidgetFor(state.ownerSessionId);
|
|
76
98
|
});
|
|
77
99
|
}
|
|
78
100
|
attachSpawnedSession(state, session) {
|
|
@@ -83,90 +105,89 @@ export class CrewManager {
|
|
|
83
105
|
state.session = session;
|
|
84
106
|
return true;
|
|
85
107
|
}
|
|
86
|
-
|
|
87
|
-
* Single owner for post-prompt and terminal state transitions.
|
|
88
|
-
* Publishes the outcome, updates state, and disposes finished subagents.
|
|
89
|
-
*/
|
|
90
|
-
settleAgent(state, nextStatus, opts, pi) {
|
|
108
|
+
settleAgent(state, nextStatus, opts) {
|
|
91
109
|
state.status = nextStatus;
|
|
92
110
|
state.result = opts.result;
|
|
93
111
|
state.error = opts.error;
|
|
94
112
|
this.delivery.deliver(state.ownerSessionId, {
|
|
95
113
|
id: state.id,
|
|
96
114
|
agentName: state.agentConfig.name,
|
|
115
|
+
sessionFile: state.session?.sessionFile,
|
|
97
116
|
status: state.status,
|
|
98
117
|
result: state.result,
|
|
99
118
|
error: state.error,
|
|
100
|
-
},
|
|
119
|
+
}, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
101
120
|
if (state.status !== "waiting") {
|
|
102
121
|
this.disposeAgent(state);
|
|
103
122
|
}
|
|
104
123
|
else {
|
|
105
|
-
this.
|
|
124
|
+
this.refreshWidgetFor(state.ownerSessionId);
|
|
106
125
|
}
|
|
107
126
|
}
|
|
108
127
|
disposeAgent(state) {
|
|
128
|
+
state.unsubscribe?.();
|
|
129
|
+
state.promptAbortController = undefined;
|
|
109
130
|
state.session?.dispose();
|
|
110
131
|
this.registry.delete(state.id);
|
|
111
|
-
this.
|
|
132
|
+
this.refreshWidgetFor(state.ownerSessionId);
|
|
112
133
|
}
|
|
113
|
-
async runPromptCycle(state, prompt
|
|
134
|
+
async runPromptCycle(state, prompt) {
|
|
114
135
|
if (isAborted(state))
|
|
115
136
|
return;
|
|
116
137
|
const abortController = new AbortController();
|
|
117
|
-
|
|
138
|
+
state.promptAbortController = abortController;
|
|
118
139
|
try {
|
|
119
140
|
const recovery = await runPromptWithOverflowRecovery(state.session, prompt, abortController.signal);
|
|
120
141
|
if (isAborted(state))
|
|
121
142
|
return;
|
|
122
143
|
const outcome = getPromptOutcome(state);
|
|
123
|
-
// If overflow recovery ran but failed, keep the error from outcome.
|
|
124
|
-
// If it recovered, outcome now reflects the retry turn's result.
|
|
125
144
|
if (recovery === "failed" && outcome.status !== "error") {
|
|
126
145
|
this.settleAgent(state, "error", {
|
|
127
146
|
error: "Context overflow recovery failed",
|
|
128
|
-
}
|
|
147
|
+
});
|
|
129
148
|
return;
|
|
130
149
|
}
|
|
131
|
-
this.settleAgent(state, outcome.status, outcome
|
|
150
|
+
this.settleAgent(state, outcome.status, outcome);
|
|
132
151
|
}
|
|
133
152
|
catch (err) {
|
|
134
153
|
if (isAborted(state))
|
|
135
154
|
return;
|
|
136
155
|
const error = err instanceof Error ? err.message : String(err);
|
|
137
|
-
this.settleAgent(state, "error", { error }
|
|
156
|
+
this.settleAgent(state, "error", { error });
|
|
138
157
|
}
|
|
139
158
|
finally {
|
|
140
|
-
|
|
159
|
+
state.promptAbortController = undefined;
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
|
-
async spawnSession(state, cwd,
|
|
162
|
+
async spawnSession(state, cwd, ctx, extensionResolvedPath) {
|
|
144
163
|
try {
|
|
145
164
|
if (isAborted(state))
|
|
146
165
|
return;
|
|
147
|
-
const { session } = await bootstrapSession({
|
|
166
|
+
const { session, warnings } = await bootstrapSession({
|
|
148
167
|
agentConfig: state.agentConfig,
|
|
149
168
|
cwd,
|
|
150
|
-
ctx,
|
|
151
|
-
extensionResolvedPath
|
|
152
|
-
parentSessionFile,
|
|
169
|
+
ctx: toBootstrapContext(ctx),
|
|
170
|
+
extensionResolvedPath,
|
|
153
171
|
});
|
|
172
|
+
// Emit bootstrap warnings to UI
|
|
173
|
+
for (const warning of warnings) {
|
|
174
|
+
ctx.onWarning?.(warning);
|
|
175
|
+
}
|
|
154
176
|
if (!this.attachSpawnedSession(state, session))
|
|
155
177
|
return;
|
|
156
178
|
this.attachSessionListeners(state, session);
|
|
157
|
-
await this.runPromptCycle(state, state.task
|
|
179
|
+
await this.runPromptCycle(state, state.task);
|
|
158
180
|
}
|
|
159
181
|
catch (err) {
|
|
160
182
|
if (isAborted(state))
|
|
161
183
|
return;
|
|
162
|
-
// Only bootstrap errors reach here; runPromptCycle handles its own errors
|
|
163
184
|
if (state.status === "running") {
|
|
164
185
|
const error = err instanceof Error ? err.message : String(err);
|
|
165
|
-
this.settleAgent(state, "error", { error }
|
|
186
|
+
this.settleAgent(state, "error", { error });
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
}
|
|
169
|
-
respond(id, message,
|
|
190
|
+
respond(id, message, callerSessionId) {
|
|
170
191
|
const state = this.registry.get(id);
|
|
171
192
|
if (!state)
|
|
172
193
|
return { error: `No subagent with id "${id}"` };
|
|
@@ -181,8 +202,8 @@ export class CrewManager {
|
|
|
181
202
|
if (!state.session)
|
|
182
203
|
return { error: `Subagent "${id}" has no active session` };
|
|
183
204
|
state.status = "running";
|
|
184
|
-
this.
|
|
185
|
-
void this.runPromptCycle(state, message
|
|
205
|
+
this.refreshWidgetFor(state.ownerSessionId);
|
|
206
|
+
void this.runPromptCycle(state, message);
|
|
186
207
|
return {};
|
|
187
208
|
}
|
|
188
209
|
done(id, callerSessionId) {
|
|
@@ -198,19 +219,19 @@ export class CrewManager {
|
|
|
198
219
|
this.disposeAgent(state);
|
|
199
220
|
return {};
|
|
200
221
|
}
|
|
201
|
-
abort(id,
|
|
222
|
+
abort(id, opts) {
|
|
202
223
|
const state = this.registry.get(id);
|
|
203
224
|
if (!state || !isAbortableStatus(state.status))
|
|
204
225
|
return false;
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
state.promptAbortController?.abort();
|
|
227
|
+
state.promptAbortController = undefined;
|
|
207
228
|
state.session?.abortCompaction();
|
|
208
229
|
state.session?.abortRetry();
|
|
209
230
|
state.session?.abort().catch(() => { });
|
|
210
|
-
this.settleAgent(state, "aborted", { error: opts.reason }
|
|
231
|
+
this.settleAgent(state, "aborted", { error: opts.reason });
|
|
211
232
|
return true;
|
|
212
233
|
}
|
|
213
|
-
abortOwned(ids, callerSessionId,
|
|
234
|
+
abortOwned(ids, callerSessionId, opts) {
|
|
214
235
|
const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
|
|
215
236
|
const result = {
|
|
216
237
|
abortedIds: [],
|
|
@@ -227,7 +248,7 @@ export class CrewManager {
|
|
|
227
248
|
result.foreignIds.push(id);
|
|
228
249
|
continue;
|
|
229
250
|
}
|
|
230
|
-
if (this.abort(id,
|
|
251
|
+
if (this.abort(id, opts)) {
|
|
231
252
|
result.abortedIds.push(id);
|
|
232
253
|
}
|
|
233
254
|
else {
|
|
@@ -236,18 +257,22 @@ export class CrewManager {
|
|
|
236
257
|
}
|
|
237
258
|
return result;
|
|
238
259
|
}
|
|
239
|
-
abortAllOwned(callerSessionId,
|
|
260
|
+
abortAllOwned(callerSessionId, opts) {
|
|
240
261
|
const ids = this.registry.getOwnedAbortableIds(callerSessionId);
|
|
241
262
|
for (const id of ids) {
|
|
242
|
-
this.abort(id,
|
|
263
|
+
this.abort(id, opts);
|
|
243
264
|
}
|
|
244
265
|
return ids;
|
|
245
266
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Abort all running subagents (process-level cleanup).
|
|
269
|
+
* Called from process exit hooks.
|
|
270
|
+
*/
|
|
271
|
+
abortAll() {
|
|
272
|
+
const allAgents = this.registry.getAllRunning();
|
|
273
|
+
for (const state of allAgents) {
|
|
274
|
+
this.abort(state.id, { reason: "Aborted on process exit" });
|
|
275
|
+
}
|
|
251
276
|
}
|
|
252
277
|
getAbortableAgents() {
|
|
253
278
|
return this.registry.getAbortableAgents();
|
|
@@ -256,3 +281,4 @@ export class CrewManager {
|
|
|
256
281
|
return this.registry.getActiveSummariesForOwner(ownerSessionId);
|
|
257
282
|
}
|
|
258
283
|
}
|
|
284
|
+
export const crewRuntime = new CrewRuntime();
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type SteeringPayload, type SendMessageFn } from "../subagent-messages.js";
|
|
2
|
+
export interface ActiveRuntimeBinding {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
isIdle: () => boolean;
|
|
5
|
+
sendMessage: SendMessageFn;
|
|
6
|
+
}
|
|
3
7
|
export declare class DeliveryCoordinator {
|
|
4
|
-
private
|
|
5
|
-
private currentIsIdle;
|
|
8
|
+
private binding;
|
|
6
9
|
private pendingMessages;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
private flushScheduled;
|
|
11
|
+
activateSession(binding: ActiveRuntimeBinding, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
|
|
12
|
+
deactivateSession(sessionId: string): void;
|
|
13
|
+
deliver(ownerSessionId: string, payload: SteeringPayload, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
|
|
14
|
+
/**
|
|
15
|
+
* Remove pending messages older than the TTL.
|
|
16
|
+
* Called during activateSession to prevent unbounded memory growth.
|
|
17
|
+
*/
|
|
18
|
+
private cleanStaleMessages;
|
|
10
19
|
private flushPending;
|
|
11
20
|
/**
|
|
12
21
|
* Result messages always go first. If more subagents are still running and the
|
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
import { sendRemainingNote, sendSteeringMessage, } from "../subagent-messages.js";
|
|
2
2
|
export class DeliveryCoordinator {
|
|
3
|
-
|
|
4
|
-
currentIsIdle = () => true;
|
|
3
|
+
binding;
|
|
5
4
|
pendingMessages = [];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this.
|
|
5
|
+
flushScheduled = false;
|
|
6
|
+
activateSession(binding, countRunningForOwner) {
|
|
7
|
+
this.binding = binding;
|
|
9
8
|
// Delay flush to next macrotask. session_start fires before pi-core
|
|
10
9
|
// calls _reconnectToAgent(), so synchronous delivery would emit agent
|
|
11
10
|
// events while the session listener is disconnected, losing JSONL persistence.
|
|
12
|
-
if (this.pendingMessages.some((entry) => entry.ownerSessionId === sessionId)) {
|
|
13
|
-
|
|
11
|
+
if (this.pendingMessages.some((entry) => entry.ownerSessionId === binding.sessionId)) {
|
|
12
|
+
this.flushScheduled = true;
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
this.flushScheduled = false;
|
|
15
|
+
this.flushPending(countRunningForOwner);
|
|
16
|
+
}, 0);
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
this.
|
|
19
|
+
deactivateSession(sessionId) {
|
|
20
|
+
if (this.binding?.sessionId === sessionId) {
|
|
21
|
+
this.binding = undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
deliver(ownerSessionId, payload, countRunningForOwner) {
|
|
25
|
+
if (!this.binding || ownerSessionId !== this.binding.sessionId || this.flushScheduled) {
|
|
26
|
+
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
|
|
19
27
|
return;
|
|
20
28
|
}
|
|
21
|
-
this.send(ownerSessionId, payload,
|
|
29
|
+
this.send(ownerSessionId, payload, countRunningForOwner);
|
|
22
30
|
}
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Remove pending messages older than the TTL.
|
|
33
|
+
* Called during activateSession to prevent unbounded memory growth.
|
|
34
|
+
*/
|
|
35
|
+
cleanStaleMessages() {
|
|
36
|
+
const maxAgeMs = 86_400_000; // 24 hours
|
|
37
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
38
|
+
this.pendingMessages = this.pendingMessages.filter((entry) => entry.queuedAt >= cutoff);
|
|
25
39
|
}
|
|
26
|
-
flushPending(
|
|
40
|
+
flushPending(countRunningForOwner) {
|
|
41
|
+
if (!this.binding)
|
|
42
|
+
return;
|
|
43
|
+
const targetSessionId = this.binding.sessionId;
|
|
44
|
+
// Clean up stale messages first (older than TTL)
|
|
45
|
+
this.cleanStaleMessages();
|
|
27
46
|
const toDeliver = [];
|
|
28
47
|
const remaining = [];
|
|
29
48
|
for (const entry of this.pendingMessages) {
|
|
30
|
-
if (entry.ownerSessionId ===
|
|
49
|
+
if (entry.ownerSessionId === targetSessionId) {
|
|
31
50
|
toDeliver.push(entry);
|
|
32
51
|
}
|
|
33
52
|
else {
|
|
53
|
+
// Keep all other messages - they may be for sessions that will be reactivated later
|
|
34
54
|
remaining.push(entry);
|
|
35
55
|
}
|
|
36
56
|
}
|
|
57
|
+
// Keep messages for other sessions
|
|
37
58
|
this.pendingMessages = remaining;
|
|
59
|
+
// Deliver messages for the active session
|
|
38
60
|
for (const entry of toDeliver) {
|
|
39
|
-
this.send(entry.ownerSessionId, entry.payload,
|
|
61
|
+
this.send(entry.ownerSessionId, entry.payload, countRunningForOwner);
|
|
40
62
|
}
|
|
41
63
|
}
|
|
42
64
|
/**
|
|
@@ -44,15 +66,19 @@ export class DeliveryCoordinator {
|
|
|
44
66
|
* owner is idle, queue the result without triggering, then queue the separate
|
|
45
67
|
* remaining note with triggerTurn so the next turn sees both in order.
|
|
46
68
|
*/
|
|
47
|
-
send(ownerSessionId, payload,
|
|
69
|
+
send(ownerSessionId, payload, countRunningForOwner) {
|
|
70
|
+
if (!this.binding || this.binding.sessionId !== ownerSessionId) {
|
|
71
|
+
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
48
74
|
const remaining = countRunningForOwner(ownerSessionId, payload.id);
|
|
49
|
-
const isIdle = this.
|
|
75
|
+
const isIdle = this.binding.isIdle();
|
|
50
76
|
const triggerResultTurn = !(isIdle && remaining > 0);
|
|
51
|
-
sendSteeringMessage(payload,
|
|
77
|
+
sendSteeringMessage(payload, this.binding.sendMessage, {
|
|
52
78
|
isIdle,
|
|
53
79
|
triggerTurn: triggerResultTurn,
|
|
54
80
|
});
|
|
55
|
-
sendRemainingNote(remaining,
|
|
81
|
+
sendRemainingNote(remaining, this.binding.sendMessage, {
|
|
56
82
|
isIdle,
|
|
57
83
|
triggerTurn: isIdle && remaining > 0,
|
|
58
84
|
});
|