@melihmucuk/pi-crew 1.0.17 → 1.0.18
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/extension/catalog.ts +543 -0
- package/extension/crew.ts +383 -0
- package/extension/index.ts +5 -6
- package/extension/subagent-session.ts +270 -0
- package/extension/tools.ts +323 -0
- package/extension/ui.ts +309 -0
- package/package.json +1 -1
- package/extension/agent-catalog.ts +0 -369
- package/extension/agent-config-fields.ts +0 -359
- package/extension/agent-discovery.ts +0 -123
- package/extension/bootstrap-session.ts +0 -131
- package/extension/integration/crew-tool-actions.ts +0 -306
- package/extension/integration/crew-tool-executor.ts +0 -109
- package/extension/integration/register-renderers.ts +0 -77
- package/extension/integration/register-tools.ts +0 -47
- package/extension/integration/tool-presentation.ts +0 -30
- package/extension/integration/tools/crew-abort.ts +0 -56
- package/extension/integration/tools/crew-done.ts +0 -27
- package/extension/integration/tools/crew-list.ts +0 -36
- package/extension/integration/tools/crew-respond.ts +0 -38
- package/extension/integration/tools/crew-spawn.ts +0 -46
- package/extension/message-delivery-policy.ts +0 -22
- package/extension/runtime/crew-runtime.ts +0 -263
- package/extension/runtime/owner-session-coordinator.ts +0 -138
- package/extension/runtime/subagent-lifecycle.ts +0 -203
- package/extension/runtime/subagent-registry.ts +0 -122
- package/extension/runtime/subagent-transitions.ts +0 -100
- package/extension/status-widget.ts +0 -107
- package/extension/subagent-messages.ts +0 -116
- package/extension/tool-registry.ts +0 -19
- /package/extension/{runtime/overflow-recovery.ts → overflow-recovery.ts} +0 -0
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { SendMessageFn } from "../message-delivery-policy.js";
|
|
2
|
-
import {
|
|
3
|
-
type SteeringPayload,
|
|
4
|
-
sendRemainingNote,
|
|
5
|
-
sendSteeringMessage,
|
|
6
|
-
} from "../subagent-messages.js";
|
|
7
|
-
|
|
8
|
-
export interface ActiveRuntimeBinding {
|
|
9
|
-
sessionId: string;
|
|
10
|
-
isIdle: () => boolean;
|
|
11
|
-
sendMessage: SendMessageFn;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface PendingMessage {
|
|
15
|
-
ownerSessionId: string;
|
|
16
|
-
payload: SteeringPayload;
|
|
17
|
-
queuedAt: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface OwnerSessionCoordinatorDeps {
|
|
21
|
-
countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
|
|
22
|
-
onRefreshOwnerSession: (ownerSessionId: string) => void;
|
|
23
|
-
now?: () => number;
|
|
24
|
-
scheduleFlush?: (callback: () => void) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const PENDING_MESSAGE_TTL_MS = 86_400_000;
|
|
28
|
-
|
|
29
|
-
export class OwnerSessionCoordinator {
|
|
30
|
-
private binding: ActiveRuntimeBinding | undefined;
|
|
31
|
-
private pendingMessages: PendingMessage[] = [];
|
|
32
|
-
private flushScheduled = false;
|
|
33
|
-
private readonly countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
|
|
34
|
-
private readonly onRefreshOwnerSession: (ownerSessionId: string) => void;
|
|
35
|
-
private readonly now: () => number;
|
|
36
|
-
private readonly scheduleFlush: (callback: () => void) => void;
|
|
37
|
-
|
|
38
|
-
constructor(deps: OwnerSessionCoordinatorDeps) {
|
|
39
|
-
this.countRunningForOwner = deps.countRunningForOwner;
|
|
40
|
-
this.onRefreshOwnerSession = deps.onRefreshOwnerSession;
|
|
41
|
-
this.now = deps.now ?? Date.now;
|
|
42
|
-
this.scheduleFlush = deps.scheduleFlush ?? ((callback) => setTimeout(callback, 0));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
activateSession(binding: ActiveRuntimeBinding): void {
|
|
46
|
-
this.binding = binding;
|
|
47
|
-
|
|
48
|
-
// Delay flush to next macrotask. session_start fires before pi-core
|
|
49
|
-
// calls _reconnectToAgent(), so synchronous delivery would emit agent
|
|
50
|
-
// events while the session listener is disconnected, losing JSONL persistence.
|
|
51
|
-
if (this.pendingMessages.some((entry) => entry.ownerSessionId === binding.sessionId)) {
|
|
52
|
-
this.flushScheduled = true;
|
|
53
|
-
this.scheduleFlush(() => {
|
|
54
|
-
this.flushScheduled = false;
|
|
55
|
-
this.flushPending();
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
deactivateSession(sessionId: string): void {
|
|
61
|
-
if (this.binding?.sessionId === sessionId) {
|
|
62
|
-
this.binding = undefined;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
refresh(ownerSessionId: string): void {
|
|
67
|
-
this.onRefreshOwnerSession(ownerSessionId);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
deliver(ownerSessionId: string, payload: SteeringPayload): void {
|
|
71
|
-
if (!this.binding || ownerSessionId !== this.binding.sessionId || this.flushScheduled) {
|
|
72
|
-
this.queue(ownerSessionId, payload);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.send(ownerSessionId, payload);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private queue(ownerSessionId: string, payload: SteeringPayload): void {
|
|
80
|
-
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: this.now() });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private cleanStaleMessages(): void {
|
|
84
|
-
const cutoff = this.now() - PENDING_MESSAGE_TTL_MS;
|
|
85
|
-
this.pendingMessages = this.pendingMessages.filter(
|
|
86
|
-
(entry) => entry.queuedAt >= cutoff,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private flushPending(): void {
|
|
91
|
-
if (!this.binding) return;
|
|
92
|
-
const targetSessionId = this.binding.sessionId;
|
|
93
|
-
|
|
94
|
-
this.cleanStaleMessages();
|
|
95
|
-
|
|
96
|
-
const toDeliver: PendingMessage[] = [];
|
|
97
|
-
const remaining: PendingMessage[] = [];
|
|
98
|
-
|
|
99
|
-
for (const entry of this.pendingMessages) {
|
|
100
|
-
if (entry.ownerSessionId === targetSessionId) {
|
|
101
|
-
toDeliver.push(entry);
|
|
102
|
-
} else {
|
|
103
|
-
remaining.push(entry);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
this.pendingMessages = remaining;
|
|
108
|
-
|
|
109
|
-
for (const entry of toDeliver) {
|
|
110
|
-
this.send(entry.ownerSessionId, entry.payload);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Result messages always go first. If more subagents are still running and the
|
|
116
|
-
* owner is idle, queue the result without triggering, then queue the separate
|
|
117
|
-
* remaining note with triggerTurn so the next turn sees both in order.
|
|
118
|
-
*/
|
|
119
|
-
private send(ownerSessionId: string, payload: SteeringPayload): void {
|
|
120
|
-
if (!this.binding || this.binding.sessionId !== ownerSessionId) {
|
|
121
|
-
this.queue(ownerSessionId, payload);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const remaining = this.countRunningForOwner(ownerSessionId, payload.id);
|
|
126
|
-
const isIdle = this.binding.isIdle();
|
|
127
|
-
const triggerResultTurn = !(isIdle && remaining > 0);
|
|
128
|
-
|
|
129
|
-
sendSteeringMessage(payload, this.binding.sendMessage, {
|
|
130
|
-
isIdle,
|
|
131
|
-
triggerTurn: triggerResultTurn,
|
|
132
|
-
});
|
|
133
|
-
sendRemainingNote(remaining, this.binding.sendMessage, {
|
|
134
|
-
isIdle,
|
|
135
|
-
triggerTurn: isIdle && remaining > 0,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type { BootstrapContext } from "../bootstrap-session.js";
|
|
5
|
-
import { bootstrapSession } from "../bootstrap-session.js";
|
|
6
|
-
import type { SubagentStatus } from "../subagent-messages.js";
|
|
7
|
-
import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
|
|
8
|
-
import type { SubagentState } from "./subagent-registry.js";
|
|
9
|
-
import { isAborted } from "./subagent-transitions.js";
|
|
10
|
-
|
|
11
|
-
interface PromptOutcome {
|
|
12
|
-
status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
|
|
13
|
-
result?: string;
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface StartOptions {
|
|
18
|
-
cwd: string;
|
|
19
|
-
ctx: BootstrapContext;
|
|
20
|
-
extensionResolvedPath: string;
|
|
21
|
-
onWarning?: (message: string) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface SubagentLifecycleCallbacks {
|
|
25
|
-
isCurrent: (state: SubagentState) => boolean;
|
|
26
|
-
onProgress: (ownerSessionId: string) => void;
|
|
27
|
-
onSettled: (
|
|
28
|
-
state: SubagentState,
|
|
29
|
-
status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">,
|
|
30
|
-
outcome: { result?: string; error?: string },
|
|
31
|
-
) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getLastAssistantMessage(
|
|
35
|
-
messages: AgentMessage[],
|
|
36
|
-
): AssistantMessage | undefined {
|
|
37
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
38
|
-
const msg = messages[i];
|
|
39
|
-
if (msg.role === "assistant") {
|
|
40
|
-
return msg as AssistantMessage;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getAssistantText(
|
|
47
|
-
message: AssistantMessage | undefined,
|
|
48
|
-
): string | undefined {
|
|
49
|
-
if (!message) return undefined;
|
|
50
|
-
|
|
51
|
-
const texts: string[] = [];
|
|
52
|
-
for (const part of message.content) {
|
|
53
|
-
if (part.type === "text") {
|
|
54
|
-
texts.push(part.text);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return texts.length > 0 ? texts.join("\n") : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getPromptOutcome(state: SubagentState): PromptOutcome {
|
|
62
|
-
const lastAssistant = getLastAssistantMessage(state.session!.messages);
|
|
63
|
-
const text = getAssistantText(lastAssistant);
|
|
64
|
-
|
|
65
|
-
if (lastAssistant?.stopReason === "error") {
|
|
66
|
-
return {
|
|
67
|
-
status: "error",
|
|
68
|
-
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (lastAssistant?.stopReason === "aborted") {
|
|
73
|
-
return {
|
|
74
|
-
status: "aborted",
|
|
75
|
-
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
status: state.agentConfig.interactive ? "waiting" : "done",
|
|
81
|
-
result: text ?? "(no output)",
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export class SubagentLifecycle {
|
|
86
|
-
constructor(private readonly callbacks: SubagentLifecycleCallbacks) {}
|
|
87
|
-
|
|
88
|
-
start(state: SubagentState, opts: StartOptions): void {
|
|
89
|
-
void this.spawnSession(state, opts);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
respond(state: SubagentState, message: string): void {
|
|
93
|
-
void this.runPromptCycle(state, message);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
abortPrompt(state: SubagentState): void {
|
|
97
|
-
state.promptAbortController?.abort();
|
|
98
|
-
state.promptAbortController = undefined;
|
|
99
|
-
state.session?.abortCompaction();
|
|
100
|
-
state.session?.abortRetry();
|
|
101
|
-
state.session?.abort().catch(() => {});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private attachSessionListeners(
|
|
105
|
-
state: SubagentState,
|
|
106
|
-
session: AgentSession,
|
|
107
|
-
): void {
|
|
108
|
-
state.unsubscribe = session.subscribe((event) => {
|
|
109
|
-
if (event.type !== "turn_end") return;
|
|
110
|
-
|
|
111
|
-
state.turns++;
|
|
112
|
-
const msg = event.message;
|
|
113
|
-
if (msg.role === "assistant") {
|
|
114
|
-
const assistantMsg = msg as AssistantMessage;
|
|
115
|
-
state.contextTokens = assistantMsg.usage.totalTokens;
|
|
116
|
-
state.model = assistantMsg.model;
|
|
117
|
-
}
|
|
118
|
-
this.callbacks.onProgress(state.ownerSessionId);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private attachSpawnedSession(
|
|
123
|
-
state: SubagentState,
|
|
124
|
-
session: AgentSession,
|
|
125
|
-
): boolean {
|
|
126
|
-
if (!this.callbacks.isCurrent(state)) {
|
|
127
|
-
session.dispose();
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
state.session = session;
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async runPromptCycle(
|
|
136
|
-
state: SubagentState,
|
|
137
|
-
prompt: string,
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
if (isAborted(state)) return;
|
|
140
|
-
|
|
141
|
-
const abortController = new AbortController();
|
|
142
|
-
state.promptAbortController = abortController;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const recovery = await runPromptWithOverflowRecovery(
|
|
146
|
-
state.session!,
|
|
147
|
-
prompt,
|
|
148
|
-
abortController.signal,
|
|
149
|
-
);
|
|
150
|
-
if (isAborted(state)) return;
|
|
151
|
-
|
|
152
|
-
const outcome = getPromptOutcome(state);
|
|
153
|
-
|
|
154
|
-
if (recovery === "failed" && outcome.status !== "error") {
|
|
155
|
-
this.callbacks.onSettled(state, "error", {
|
|
156
|
-
error: "Context overflow recovery failed",
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.callbacks.onSettled(state, outcome.status, outcome);
|
|
162
|
-
} catch (err) {
|
|
163
|
-
if (isAborted(state)) return;
|
|
164
|
-
|
|
165
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
166
|
-
this.callbacks.onSettled(state, "error", { error });
|
|
167
|
-
} finally {
|
|
168
|
-
state.promptAbortController = undefined;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private async spawnSession(
|
|
173
|
-
state: SubagentState,
|
|
174
|
-
opts: StartOptions,
|
|
175
|
-
): Promise<void> {
|
|
176
|
-
try {
|
|
177
|
-
if (isAborted(state)) return;
|
|
178
|
-
|
|
179
|
-
const { session, warnings } = await bootstrapSession({
|
|
180
|
-
agentConfig: state.agentConfig,
|
|
181
|
-
cwd: opts.cwd,
|
|
182
|
-
ctx: opts.ctx,
|
|
183
|
-
extensionResolvedPath: opts.extensionResolvedPath,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
for (const warning of warnings) {
|
|
187
|
-
opts.onWarning?.(warning);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!this.attachSpawnedSession(state, session)) return;
|
|
191
|
-
|
|
192
|
-
this.attachSessionListeners(state, session);
|
|
193
|
-
await this.runPromptCycle(state, state.task);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
if (isAborted(state)) return;
|
|
196
|
-
|
|
197
|
-
if (state.status === "running") {
|
|
198
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
199
|
-
this.callbacks.onSettled(state, "error", { error });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { AgentConfig } from "../agent-discovery.js";
|
|
4
|
-
import type { SubagentStatus } from "../subagent-messages.js";
|
|
5
|
-
import { isAbortableStatus } from "./subagent-transitions.js";
|
|
6
|
-
|
|
7
|
-
export interface SubagentState {
|
|
8
|
-
id: string;
|
|
9
|
-
agentConfig: AgentConfig;
|
|
10
|
-
task: string;
|
|
11
|
-
status: SubagentStatus;
|
|
12
|
-
ownerSessionId: string;
|
|
13
|
-
session: AgentSession | null;
|
|
14
|
-
turns: number;
|
|
15
|
-
contextTokens: number;
|
|
16
|
-
model: string | undefined;
|
|
17
|
-
error?: string;
|
|
18
|
-
result?: string;
|
|
19
|
-
promptAbortController?: AbortController;
|
|
20
|
-
unsubscribe?: () => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ActiveAgentSummary {
|
|
24
|
-
id: string;
|
|
25
|
-
agentName: string;
|
|
26
|
-
status: SubagentStatus;
|
|
27
|
-
turns: number;
|
|
28
|
-
contextTokens: number;
|
|
29
|
-
model: string | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function generateId(name: string, existingIds: Set<string>): string {
|
|
33
|
-
for (let i = 0; i < 10; i++) {
|
|
34
|
-
const id = `${name}-${randomBytes(4).toString("hex")}`;
|
|
35
|
-
if (!existingIds.has(id)) return id;
|
|
36
|
-
}
|
|
37
|
-
return `${name}-${randomBytes(8).toString("hex")}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildActiveAgentSummary(
|
|
41
|
-
state: SubagentState,
|
|
42
|
-
): ActiveAgentSummary {
|
|
43
|
-
return {
|
|
44
|
-
id: state.id,
|
|
45
|
-
agentName: state.agentConfig.name,
|
|
46
|
-
status: state.status,
|
|
47
|
-
turns: state.turns,
|
|
48
|
-
contextTokens: state.contextTokens,
|
|
49
|
-
model: state.model,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class SubagentRegistry {
|
|
54
|
-
private activeAgents = new Map<string, SubagentState>();
|
|
55
|
-
|
|
56
|
-
create(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState {
|
|
57
|
-
const id = generateId(agentConfig.name, new Set(this.activeAgents.keys()));
|
|
58
|
-
const state: SubagentState = {
|
|
59
|
-
id,
|
|
60
|
-
agentConfig,
|
|
61
|
-
task,
|
|
62
|
-
status: "running",
|
|
63
|
-
ownerSessionId,
|
|
64
|
-
session: null,
|
|
65
|
-
turns: 0,
|
|
66
|
-
contextTokens: 0,
|
|
67
|
-
model: undefined,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
this.activeAgents.set(id, state);
|
|
71
|
-
return state;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
get(id: string): SubagentState | undefined {
|
|
75
|
-
return this.activeAgents.get(id);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
hasState(state: SubagentState): boolean {
|
|
79
|
-
return this.activeAgents.get(state.id) === state;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
delete(id: string): void {
|
|
83
|
-
this.activeAgents.delete(id);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
countRunningForOwner(ownerSessionId: string, excludeId: string): number {
|
|
87
|
-
let count = 0;
|
|
88
|
-
for (const state of this.activeAgents.values()) {
|
|
89
|
-
if (
|
|
90
|
-
state.id !== excludeId &&
|
|
91
|
-
state.ownerSessionId === ownerSessionId &&
|
|
92
|
-
state.status === "running"
|
|
93
|
-
) {
|
|
94
|
-
count++;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return count;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[] {
|
|
101
|
-
return Array.from(this.activeAgents.values())
|
|
102
|
-
.filter(
|
|
103
|
-
(state) => isAbortableStatus(state.status) && state.ownerSessionId === ownerSessionId,
|
|
104
|
-
)
|
|
105
|
-
.map(buildActiveAgentSummary);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
getOwnedAbortableIds(ownerSessionId: string): string[] {
|
|
109
|
-
return Array.from(this.activeAgents.values())
|
|
110
|
-
.filter(
|
|
111
|
-
(state) =>
|
|
112
|
-
state.ownerSessionId === ownerSessionId && isAbortableStatus(state.status),
|
|
113
|
-
)
|
|
114
|
-
.map((state) => state.id);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
getAllAbortable(): SubagentState[] {
|
|
118
|
-
return Array.from(this.activeAgents.values()).filter((state) =>
|
|
119
|
-
isAbortableStatus(state.status),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { SubagentStatus } from "../subagent-messages.js";
|
|
2
|
-
import type { SubagentState } from "./subagent-registry.js";
|
|
3
|
-
|
|
4
|
-
export type SettledSubagentStatus = Extract<
|
|
5
|
-
SubagentStatus,
|
|
6
|
-
"done" | "waiting" | "error" | "aborted"
|
|
7
|
-
>;
|
|
8
|
-
|
|
9
|
-
export interface SubagentTransitionOutcome {
|
|
10
|
-
result?: string;
|
|
11
|
-
error?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type SubagentTransitionResult =
|
|
15
|
-
| { ok: true; state: SubagentState }
|
|
16
|
-
| { ok: false; error: string };
|
|
17
|
-
|
|
18
|
-
export function isAborted(state: SubagentState): boolean {
|
|
19
|
-
return state.status === "aborted";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function isAbortableStatus(status: SubagentStatus): boolean {
|
|
23
|
-
return status === "running" || status === "waiting";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function canAbortSubagent(
|
|
27
|
-
state: SubagentState | undefined,
|
|
28
|
-
): state is SubagentState {
|
|
29
|
-
return Boolean(state && isAbortableStatus(state.status));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function validateOwnedSubagent(
|
|
33
|
-
state: SubagentState | undefined,
|
|
34
|
-
id: string,
|
|
35
|
-
callerSessionId: string,
|
|
36
|
-
missingMessage: string,
|
|
37
|
-
): SubagentTransitionResult {
|
|
38
|
-
if (!state) return { ok: false, error: missingMessage };
|
|
39
|
-
if (state.ownerSessionId !== callerSessionId) {
|
|
40
|
-
return { ok: false, error: `Subagent "${id}" belongs to a different session` };
|
|
41
|
-
}
|
|
42
|
-
return { ok: true, state };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function startSubagentResponse(
|
|
46
|
-
state: SubagentState | undefined,
|
|
47
|
-
id: string,
|
|
48
|
-
callerSessionId: string,
|
|
49
|
-
): SubagentTransitionResult {
|
|
50
|
-
const owned = validateOwnedSubagent(
|
|
51
|
-
state,
|
|
52
|
-
id,
|
|
53
|
-
callerSessionId,
|
|
54
|
-
`No subagent with id "${id}"`,
|
|
55
|
-
);
|
|
56
|
-
if (!owned.ok) return owned;
|
|
57
|
-
|
|
58
|
-
if (owned.state.status !== "waiting") {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
error: `Subagent "${id}" is not waiting for a response (status: ${owned.state.status})`,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
if (!owned.state.session) {
|
|
65
|
-
return { ok: false, error: `Subagent "${id}" has no active session` };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
owned.state.status = "running";
|
|
69
|
-
return owned;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function validateSubagentDone(
|
|
73
|
-
state: SubagentState | undefined,
|
|
74
|
-
id: string,
|
|
75
|
-
callerSessionId: string,
|
|
76
|
-
): SubagentTransitionResult {
|
|
77
|
-
const owned = validateOwnedSubagent(
|
|
78
|
-
state,
|
|
79
|
-
id,
|
|
80
|
-
callerSessionId,
|
|
81
|
-
`No active subagent with id "${id}"`,
|
|
82
|
-
);
|
|
83
|
-
if (!owned.ok) return owned;
|
|
84
|
-
|
|
85
|
-
if (owned.state.status !== "waiting") {
|
|
86
|
-
return { ok: false, error: `Subagent "${id}" is not in waiting state` };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return owned;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function settleSubagent(
|
|
93
|
-
state: SubagentState,
|
|
94
|
-
status: SettledSubagentStatus,
|
|
95
|
-
outcome: SubagentTransitionOutcome,
|
|
96
|
-
): void {
|
|
97
|
-
state.status = status;
|
|
98
|
-
state.result = outcome.result;
|
|
99
|
-
state.error = outcome.error;
|
|
100
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { ActiveAgentSummary } from "./runtime/crew-runtime.js";
|
|
4
|
-
import type { CrewRuntime } from "./runtime/crew-runtime.js";
|
|
5
|
-
|
|
6
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
const SPINNER_INTERVAL_MS = 80;
|
|
8
|
-
|
|
9
|
-
function formatTokens(tokens: number): string {
|
|
10
|
-
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
11
|
-
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
12
|
-
return String(tokens);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function buildLine(agent: ActiveAgentSummary, frame: string): string {
|
|
16
|
-
const model = agent.model ?? "…";
|
|
17
|
-
const icon = agent.status === "waiting" ? "⏳" : frame;
|
|
18
|
-
return `${icon} ${agent.id} (${model}) · turn ${agent.turns} · ${formatTokens(agent.contextTokens)} ctx`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface WidgetState {
|
|
22
|
-
ctx: ExtensionContext;
|
|
23
|
-
text: Text;
|
|
24
|
-
// biome-ignore lint: TUI type from factory param
|
|
25
|
-
tui: any;
|
|
26
|
-
timer: ReturnType<typeof setInterval>;
|
|
27
|
-
frameIndex: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let widget: WidgetState | undefined;
|
|
31
|
-
|
|
32
|
-
function disposeWidget(state: WidgetState): void {
|
|
33
|
-
clearInterval(state.timer);
|
|
34
|
-
if (widget === state) {
|
|
35
|
-
widget = undefined;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function clearWidget(): void {
|
|
40
|
-
const current = widget;
|
|
41
|
-
if (!current) return;
|
|
42
|
-
disposeWidget(current);
|
|
43
|
-
current.ctx.ui.setWidget("crew-status", undefined);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function hasRunningAgent(agents: ActiveAgentSummary[]): boolean {
|
|
47
|
-
return agents.some((agent) => agent.status === "running");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function syncWidgetText(state: WidgetState, agents: ActiveAgentSummary[]): void {
|
|
51
|
-
const frame = SPINNER_FRAMES[state.frameIndex % SPINNER_FRAMES.length];
|
|
52
|
-
const lines = agents.map((agent) => buildLine(agent, frame));
|
|
53
|
-
state.text.setText(lines.join("\n"));
|
|
54
|
-
state.tui.requestRender();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function updateWidget(ctx: ExtensionContext, crew: CrewRuntime): void {
|
|
58
|
-
if (!ctx.hasUI) {
|
|
59
|
-
clearWidget();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const ownerSessionId = ctx.sessionManager.getSessionId();
|
|
64
|
-
const running = crew.getActiveSummariesForOwner(ownerSessionId);
|
|
65
|
-
if (running.length === 0) {
|
|
66
|
-
clearWidget();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (widget && widget.ctx !== ctx) {
|
|
71
|
-
clearWidget();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (widget) {
|
|
75
|
-
syncWidgetText(widget, running);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
ctx.ui.setWidget("crew-status", (tui, _theme) => {
|
|
80
|
-
const text = new Text("", 1, 0);
|
|
81
|
-
const state: WidgetState = {
|
|
82
|
-
ctx,
|
|
83
|
-
text,
|
|
84
|
-
tui,
|
|
85
|
-
frameIndex: 0,
|
|
86
|
-
timer: setInterval(() => {
|
|
87
|
-
const agents = crew.getActiveSummariesForOwner(ownerSessionId);
|
|
88
|
-
if (agents.length === 0) {
|
|
89
|
-
clearWidget();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (!hasRunningAgent(agents)) return;
|
|
93
|
-
state.frameIndex++;
|
|
94
|
-
syncWidgetText(state, agents);
|
|
95
|
-
}, SPINNER_INTERVAL_MS),
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
widget = state;
|
|
99
|
-
syncWidgetText(state, running);
|
|
100
|
-
|
|
101
|
-
return Object.assign(text, {
|
|
102
|
-
dispose() {
|
|
103
|
-
disposeWidget(state);
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
}
|