@fradser/pi-agent-teams 0.10.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -64
- package/package.json +13 -5
- package/references/agent-roles.md +23 -13
- package/src/activity.ts +20 -11
- package/src/agent-actions.ts +61 -36
- package/src/agent-host.ts +64 -0
- package/src/capability-tools.ts +34 -0
- package/src/guidance.ts +44 -38
- package/src/index.ts +70 -36
- package/src/recipient.ts +4 -4
- package/src/state.ts +163 -618
- package/src/statefile.ts +28 -244
- package/src/team-machine.ts +825 -534
- package/src/tool-copy.ts +26 -34
- package/src/tools.ts +57 -210
- package/src/types.ts +51 -159
- package/src/ui.ts +85 -59
- package/src/worker.ts +88 -66
- package/src/agents.ts +0 -296
- package/src/leader-reports.ts +0 -92
- package/src/spawner.ts +0 -751
- package/src/work-context.ts +0 -88
- package/src/worker-tools.ts +0 -4
- package/src/worktree.ts +0 -164
package/src/types.ts
CHANGED
|
@@ -1,151 +1,39 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TSchema, TString, TObject, TUnion } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
// ──
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
context?: "fresh" | "fork";
|
|
38
|
-
pid: number;
|
|
39
|
-
status: TeammateStatus;
|
|
40
|
-
/** Working directory (the worktree root when isolated). */
|
|
41
|
-
cwd?: string;
|
|
42
|
-
/** Whether this teammate owns a dedicated Git worktree. */
|
|
43
|
-
isolation: "worktree" | "none";
|
|
44
|
-
/** Board task currently claimed by this teammate, if any. */
|
|
45
|
-
currentTaskId?: string;
|
|
46
|
-
/** The one assignment this worker is allowed to execute. Direct assignments
|
|
47
|
-
* and board claims are mutually exclusive until the leader opens another. */
|
|
48
|
-
assignment?: WorkerAssignment;
|
|
49
|
-
/** Most recent assignment retained for successor handoff after release or shutdown. */
|
|
50
|
-
lastAssignment?: WorkerAssignment;
|
|
51
|
-
lastTaskId?: string;
|
|
52
|
-
/** Effective launch model reference ("provider/model"); absent when Pi picks its default. */
|
|
53
|
-
model?: string;
|
|
54
|
-
/** Live assistant text assembled from the RPC stream. */
|
|
55
|
-
liveText?: string;
|
|
56
|
-
/** Current child tool name, if a tool is executing. */
|
|
57
|
-
activeTool?: string;
|
|
58
|
-
/** Live assistant reasoning streamed while no tool runs. */
|
|
59
|
-
liveThinking?: string;
|
|
60
|
-
/** Assistant turns observed in the current wake-up sequence. */
|
|
61
|
-
turns?: number;
|
|
62
|
-
/** The child finished its current sequence and awaits the next prompt. */
|
|
63
|
-
sequenceEnded?: boolean;
|
|
64
|
-
/** Leader reports are closed until a new prompt starts after a terminal report. */
|
|
65
|
-
reportSequenceEnded?: boolean;
|
|
66
|
-
/** When the harness last sent a claimable-task notice to this teammate. */
|
|
67
|
-
lastNoticeAt?: number;
|
|
68
|
-
/** Claimable task ids already announced to this teammate; one notice per id until it re-arms. */
|
|
69
|
-
noticedTaskIds?: string[];
|
|
70
|
-
usage?: WorkerUsage;
|
|
71
|
-
error?: string;
|
|
72
|
-
/** Effective tool allowlist granted to the child process (role tools plus
|
|
73
|
-
* the capability set). Absent only for legacy snapshots. */
|
|
74
|
-
tools?: string[];
|
|
75
|
-
/** True once recognized model/stream activity was observed for this
|
|
76
|
-
* incarnation; console telemetry only, never a leader notification. */
|
|
77
|
-
modelOutputSeen?: boolean;
|
|
78
|
-
createdAt: number;
|
|
79
|
-
updatedAt: number;
|
|
80
|
-
stoppedAt?: number;
|
|
81
|
-
/** Last wall-clock time output was observed (any stream event or prompt delivery). */
|
|
82
|
-
lastOutputAt?: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Task board ────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
export const TaskStatus = Type.Union(
|
|
88
|
-
[
|
|
89
|
-
Type.Literal("pending"),
|
|
90
|
-
Type.Literal("claimed"),
|
|
91
|
-
Type.Literal("completed"),
|
|
92
|
-
Type.Literal("superseded"),
|
|
93
|
-
],
|
|
94
|
-
{ description: "Board lifecycle of a task" },
|
|
95
|
-
);
|
|
96
|
-
export type TaskStatus = Static<typeof TaskStatus>;
|
|
97
|
-
|
|
98
|
-
/** One task on the shared board. Only the leader process writes board state. */
|
|
99
|
-
export interface BoardTask {
|
|
100
|
-
id: string;
|
|
101
|
-
subject: string;
|
|
102
|
-
description?: string;
|
|
103
|
-
/** Task ids that must complete before this task is claimable. */
|
|
104
|
-
dependsOn: string[];
|
|
105
|
-
/** Completion gate: a review prompt a fresh one-shot reviewer answers with
|
|
106
|
-
* VERDICT: PASS or FAIL; overrides the agent-role default. */
|
|
107
|
-
verify?: string;
|
|
108
|
-
/** Mutating work must use stable resource tags such as `firmware/sub-node`.
|
|
109
|
-
* A resource conflicts with itself and any slash-prefixed descendant. */
|
|
110
|
-
resources: string[];
|
|
111
|
-
status: TaskStatus;
|
|
112
|
-
claimedBy?: string;
|
|
113
|
-
/** Replacement task that made this task obsolete. */
|
|
114
|
-
supersededBy?: string;
|
|
115
|
-
result?: string;
|
|
116
|
-
/** Deferred mail archived when an accepted result closes its Work Item. */
|
|
117
|
-
deferredMessages?: Array<{ from: string; subject: string; body: string; timestamp: number }>;
|
|
118
|
-
errorMessage?: string;
|
|
119
|
-
/** Failed/interrupted Work stays pending for inspection but cannot be
|
|
120
|
-
* autonomously claimed until the Leader deliberately authorizes recovery. */
|
|
121
|
-
recoveryRequired?: boolean;
|
|
122
|
-
createdAt: number;
|
|
123
|
-
updatedAt: number;
|
|
124
|
-
completedAt?: number;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** A claim or submission intent expressed by a worker through marker files.
|
|
128
|
-
* Workers never write the board file itself. `status` applies to submissions. */
|
|
129
|
-
export interface TaskIntent {
|
|
130
|
-
taskId: string;
|
|
131
|
-
worker: string;
|
|
132
|
-
spawnId: string;
|
|
133
|
-
assignmentId?: string;
|
|
134
|
-
status?: "completed" | "failed";
|
|
135
|
-
result?: string;
|
|
136
|
-
timestamp: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Spawn ─────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
/** Token/cost usage reported by a teammate (accumulated across sequences). */
|
|
142
|
-
export interface WorkerUsage {
|
|
143
|
-
input: number;
|
|
144
|
-
output: number;
|
|
145
|
-
cacheRead: number;
|
|
146
|
-
cacheWrite: number;
|
|
147
|
-
totalTokens: number;
|
|
148
|
-
cost: number;
|
|
3
|
+
import { AGENT_BUILTIN_TOOLS } from "@fradser/pi-subagents";
|
|
4
|
+
|
|
5
|
+
// ── Roster ───────────────────────────────────────────────────────
|
|
6
|
+
// A child process on the roster is an execution fact, so the type lives in
|
|
7
|
+
// @fradser/pi-subagents with the code that manages it. Re-exported here because
|
|
8
|
+
// the coordination vocabulary below (TeamState) references it and callers read
|
|
9
|
+
// this module as the package's type surface.
|
|
10
|
+
|
|
11
|
+
export type { Agent, AgentStatus, AgentUsage } from "@fradser/pi-subagents";
|
|
12
|
+
import type { Agent } from "@fradser/pi-subagents";
|
|
13
|
+
|
|
14
|
+
// ── Task tasks ────────────────────────────────────────────────────
|
|
15
|
+
// The Work Item data model lives in @fradser/pi-tasks. Re-exported here because the
|
|
16
|
+
// coordination vocabulary below (Agent, TeamState) references it and callers
|
|
17
|
+
// read this module as the package's type surface.
|
|
18
|
+
|
|
19
|
+
export type { Task, TaskIntent, TaskStatus } from "@fradser/pi-tasks";
|
|
20
|
+
// The assignment a teammate holds is the task domain's own shape. Re-exported
|
|
21
|
+
// under the historical name so the coordination vocabulary stays readable.
|
|
22
|
+
export type { WorkAssignment as WorkerAssignment } from "@fradser/pi-tasks";
|
|
23
|
+
import type { Task } from "@fradser/pi-tasks";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The result shape every coordination tool returns.
|
|
27
|
+
*
|
|
28
|
+
* Declared because `additionalProperties: false` and a single `required` on a
|
|
29
|
+
* flat schema cannot express per-action requirements, so refusals are reported
|
|
30
|
+
* here rather than thrown. Every tool reports failure this way, and a caller
|
|
31
|
+
* reads one shape whatever went wrong.
|
|
32
|
+
*/
|
|
33
|
+
export interface CoordinationToolResult {
|
|
34
|
+
content: Array<{ type: "text"; text: string }>;
|
|
35
|
+
details: Record<string, unknown>;
|
|
36
|
+
isError?: boolean;
|
|
149
37
|
}
|
|
150
38
|
|
|
151
39
|
// ── Mailbox ───────────────────────────────────────────────────────
|
|
@@ -219,13 +107,10 @@ export interface InboxMessage {
|
|
|
219
107
|
|
|
220
108
|
export const InlineAgentDefinitionParams = Type.Object({
|
|
221
109
|
description: Type.String({ minLength: 1, description: "Routing contract for the generated Agent" }),
|
|
222
|
-
tools: Type.Optional(Type.Array(Type.String(), { description: `Explicit minimal tool grant. Canonical built-ins: ${
|
|
110
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: `Explicit minimal tool grant. Canonical built-ins: ${AGENT_BUILTIN_TOOLS.join(", ")}. Omitted or [] means coordination-only (message and task), with no file or shell access. No aliases or inherited leader extension tools.` })),
|
|
223
111
|
model: Type.Optional(Type.String({ description: "Provider/model pin or inherit" })),
|
|
224
112
|
verify: Type.Optional(Type.String({ description: "Default Work verification gate" })),
|
|
225
|
-
worktree: Type.Optional(Type.Boolean({ description: "Dedicated Git worktree" })),
|
|
226
113
|
prompt: Type.String({ minLength: 1, description: "Agent role prompt" }),
|
|
227
|
-
persist: Type.Optional(Type.Boolean({ description: "Persist only when explicitly requested" })),
|
|
228
|
-
persistScope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("project-local")])),
|
|
229
114
|
}, { additionalProperties: false });
|
|
230
115
|
|
|
231
116
|
/** Some harnesses deliver object/array parameters as JSON strings and Pi
|
|
@@ -321,7 +206,7 @@ export const AgentActionParams = actionUnion([
|
|
|
321
206
|
], { description: "Delegate, start, inspect, or stop an Agent session" });
|
|
322
207
|
|
|
323
208
|
/** Shut down one living teammate. */
|
|
324
|
-
/** Create a
|
|
209
|
+
/** Create a task set task (leader-only). */
|
|
325
210
|
/** First public Work interface slice. Work creation reuses the current
|
|
326
211
|
* single-writer task record until every acquisition path shares one lifecycle. */
|
|
327
212
|
export const WorkToolParams = actionUnion([
|
|
@@ -360,12 +245,19 @@ export const WorkToolParams = actionUnion([
|
|
|
360
245
|
}, { additionalProperties: false }),
|
|
361
246
|
], { description: "Create, list, assign, release, reopen, or supersede current Work Items" });
|
|
362
247
|
|
|
363
|
-
/** Shared leader/worker read-only
|
|
364
|
-
/**
|
|
248
|
+
/** Shared leader/worker read-only tasks view. */
|
|
249
|
+
/** The one communication primitive, across Leader, Worker, and Peers.
|
|
250
|
+
*
|
|
251
|
+
* `body` rather than `message`: the field is the text of a message, and a
|
|
252
|
+
* parameter named after the tool is a small way of making every caller write
|
|
253
|
+
* `message: { message: ... }`. `kind` rather than `intent`: the two values are
|
|
254
|
+
* what the recipient owes the sender — nothing, or a decision — and that is a
|
|
255
|
+
* kind of message, not an intent the tool acts on.
|
|
256
|
+
*/
|
|
365
257
|
export const AgentEventParams = Type.Object({
|
|
366
|
-
|
|
258
|
+
body: Type.String({ description: "Message content" }),
|
|
367
259
|
to: Type.Optional(Type.String({ minLength: 1, description: "Recipient Agent, leader, or exact session route" })),
|
|
368
|
-
|
|
260
|
+
kind: Type.Optional(Type.Union([Type.Literal("inform"), Type.Literal("request")])),
|
|
369
261
|
}, { additionalProperties: false });
|
|
370
262
|
|
|
371
263
|
/** The reserved recipient name for reports to the team leader. */
|
|
@@ -373,7 +265,7 @@ export const LEADER_RECIPIENT = "leader";
|
|
|
373
265
|
|
|
374
266
|
/** The single messaging primitive: addressed peer mail and leader reports.
|
|
375
267
|
* `status` is honored only for to="leader" terminal reports. */
|
|
376
|
-
/** Self-claim a pending
|
|
268
|
+
/** Self-claim a pending tasks task. */
|
|
377
269
|
/** Worker-only Work claim operation. It queues an intent; the single-writer
|
|
378
270
|
* harness remains the only authority that can grant ownership. */
|
|
379
271
|
export const WorkerWorkToolParams = actionUnion([
|
|
@@ -400,8 +292,8 @@ export const TEAM_RUNTIME_VERSION = 2;
|
|
|
400
292
|
|
|
401
293
|
export interface TeamState {
|
|
402
294
|
runtimeVersion: number;
|
|
403
|
-
|
|
404
|
-
tasks: Record<string,
|
|
295
|
+
agents: Record<string, Agent>;
|
|
296
|
+
tasks: Record<string, Task>;
|
|
405
297
|
/** Unified teammate model for this session; spawns without a role pin use it. */
|
|
406
298
|
defaultModel?: string;
|
|
407
299
|
/** Single leader inbox for teammate reports and harness diagnostics. */
|
package/src/ui.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isTaskReplaced } from "@fradser/pi-tasks";
|
|
1
2
|
/** Passive team widget and the /agent-teams management console
|
|
2
|
-
* (session
|
|
3
|
+
* (session agents + persistent agent roles + tasks). */
|
|
3
4
|
|
|
4
5
|
import { truncateToWidth, Key, matchesKey, fuzzyFilter } from "@earendil-works/pi-tui";
|
|
5
6
|
import {
|
|
@@ -21,20 +22,20 @@ import { type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
|
21
22
|
import {
|
|
22
23
|
clampConsoleScroll, consoleScrollRange, maxConsoleBody, scrollConsoleDetail, wrapConsoleDetail,
|
|
23
24
|
} from "./console-viewport.ts";
|
|
24
|
-
import {
|
|
25
|
-
import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "
|
|
26
|
-
import { getPeerDeliveryState, getState,
|
|
25
|
+
import { formatAgentLabel, runningTeammateActivity } from "./activity.ts";
|
|
26
|
+
import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "@fradser/pi-subagents";
|
|
27
|
+
import { getPeerDeliveryState, getState, getAgent, getTeamDefaultModel, listTasks, listAgents, livingAgents, setTeamDefaultModel } from "./state.ts";
|
|
27
28
|
import {
|
|
28
29
|
currentLeaderModelRef,
|
|
29
30
|
ensureLivePoll,
|
|
30
31
|
formatSilenceDuration,
|
|
31
32
|
resolveSpawnModel,
|
|
32
33
|
runtimeDirPath,
|
|
33
|
-
|
|
34
|
+
shutdownAgent,
|
|
34
35
|
stallSilenceMs,
|
|
35
36
|
stallThresholdMs,
|
|
36
37
|
} from "./team-machine.ts";
|
|
37
|
-
import type {
|
|
38
|
+
import type { Agent } from "./types.ts";
|
|
38
39
|
import { mapPickerKey } from "./picker-keys.ts";
|
|
39
40
|
import { inboxPath } from "./statefile.ts";
|
|
40
41
|
import * as fs from "node:fs";
|
|
@@ -44,14 +45,14 @@ let spinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
|
44
45
|
let spinnerFrame = 0;
|
|
45
46
|
|
|
46
47
|
const teamActivityWidget = createLiveActivityWidget({
|
|
47
|
-
key: "
|
|
48
|
+
key: "agent",
|
|
48
49
|
placement: "aboveEditor",
|
|
49
50
|
fit: truncateToWidth,
|
|
50
51
|
activityFormat: "markdown",
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
function ensureSpinner(): void {
|
|
54
|
-
const working =
|
|
55
|
+
const working = livingAgents().some(isWorking);
|
|
55
56
|
if (working && !spinnerTimer) {
|
|
56
57
|
spinnerTimer = setInterval(() => {
|
|
57
58
|
spinnerFrame = (spinnerFrame + 1) % PI_SPINNER_FRAMES.length;
|
|
@@ -71,20 +72,20 @@ export function stopUiTimers(): void {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function ensureTeamWidget(ctx?: { ui?: ExtensionUIContext; mode?: string }): void {
|
|
74
|
-
const working =
|
|
75
|
-
teamActivityWidget.update(ctx, working.map((
|
|
76
|
-
id:
|
|
77
|
-
identity:
|
|
78
|
-
activity: runningTeammateActivity(
|
|
75
|
+
const working = livingAgents().filter(agent => !agent.standalone && isWorking(agent));
|
|
76
|
+
teamActivityWidget.update(ctx, working.map((agent) => ({
|
|
77
|
+
id: agent.name,
|
|
78
|
+
identity: agent.name,
|
|
79
|
+
activity: runningTeammateActivity(agent) + stallSuffix(agent),
|
|
79
80
|
})));
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function isWorking(
|
|
83
|
-
return
|
|
83
|
+
function isWorking(agent: { status: string }): boolean {
|
|
84
|
+
return agent.status === "working" || agent.status === "starting";
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/** Silence marker appended once a working teammate passes its stall threshold. */
|
|
87
|
-
function stallSuffix(teammate:
|
|
88
|
+
function stallSuffix(teammate: Agent): string {
|
|
88
89
|
if (!isWorking(teammate)) return "";
|
|
89
90
|
const silence = stallSilenceMs(teammate);
|
|
90
91
|
const threshold = stallThresholdMs(teammate);
|
|
@@ -166,18 +167,43 @@ function displaySource(sourcePath: string | undefined): string {
|
|
|
166
167
|
return relative.startsWith("..") ? sourcePath : `./${relative}`;
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
/** Passive console telemetry for the spawn environment policy: names and counts
|
|
171
|
+
* only, never a withheld value. Per ADR-0002 observation lives here rather than
|
|
172
|
+
* in a leader notification, so a routine spawn never interrupts the leader.
|
|
173
|
+
* child-env.ts already bounds the name list, so this renders it as given and
|
|
174
|
+
* the console cannot drift from the diagnostic. */
|
|
175
|
+
function buildEnvPolicyDetail(policy: Agent["envPolicy"]): string[] {
|
|
176
|
+
if (!policy || policy.withheldCount === 0) return [];
|
|
177
|
+
const extra = policy.withheldSecretCount - policy.withheldSecretNames.length;
|
|
178
|
+
const names = policy.withheldSecretNames.length > 0
|
|
179
|
+
? ` (credential-shaped: ${policy.withheldSecretNames.join(", ")}${extra > 0 ? `, +${extra} more` : ""})`
|
|
180
|
+
: "";
|
|
181
|
+
const lines = [
|
|
182
|
+
` Env: ${policy.withheldCount} leader variable${policy.withheldCount === 1 ? "" : "s"} withheld${names}`,
|
|
183
|
+
];
|
|
184
|
+
if (policy.missingAllowed.length > 0) {
|
|
185
|
+
lines.push(` Env allow: requested but absent from the leader: ${policy.missingAllowed.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
if (policy.flaggedAllowOverrides.length > 0) {
|
|
188
|
+
lines.push(` Env allow: operator passes credential-shaped variables: ${policy.flaggedAllowOverrides.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
function buildTeammateDetail(name: string): string[] {
|
|
170
|
-
const teammate =
|
|
194
|
+
const teammate = getAgent(name);
|
|
171
195
|
if (!teammate) return ["(teammate removed from the roster)"];
|
|
172
196
|
const reports = getState().leaderMailbox.filter((message) => message.from === name);
|
|
173
197
|
const lines: string[] = [
|
|
174
|
-
`@${teammate.name} (${teammate.
|
|
198
|
+
`@${teammate.name} (${teammate.role}) [${teammate.status}]`,
|
|
175
199
|
"",
|
|
176
200
|
"== teammate ==",
|
|
177
201
|
` Status: ${teammate.status}${teammate.currentTaskId ? ` | Task: ${teammate.currentTaskId}` : ""}`,
|
|
178
|
-
` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"}
|
|
202
|
+
` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"}`,
|
|
203
|
+
...(teammate.cwd ? [` Launch cwd: ${teammate.cwd}`] : []),
|
|
179
204
|
...(teammate.model ? [` Launch model: ${teammate.model}`] : []),
|
|
180
205
|
...(teammate.tools?.length ? [` Tools: ${teammate.tools.join(", ")}`] : []),
|
|
206
|
+
...buildEnvPolicyDetail(teammate.envPolicy),
|
|
181
207
|
` Created: ${new Date(teammate.createdAt).toLocaleString()}`,
|
|
182
208
|
];
|
|
183
209
|
if (teammate.stoppedAt) lines.push(` Stopped: ${new Date(teammate.stoppedAt).toLocaleString()}`);
|
|
@@ -213,7 +239,7 @@ function buildTeammateDetail(name: string): string[] {
|
|
|
213
239
|
function buildTaskDetail(taskId: string): string[] {
|
|
214
240
|
const tasks = listTasks();
|
|
215
241
|
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
216
|
-
if (!task) return ["(task not
|
|
242
|
+
if (!task) return ["(task not found)"];
|
|
217
243
|
const byId = new Map(tasks.map((candidate) => [candidate.id, candidate]));
|
|
218
244
|
return [
|
|
219
245
|
`[${task.id}] ${task.subject}`,
|
|
@@ -227,7 +253,6 @@ function buildTaskDetail(taskId: string): string[] {
|
|
|
227
253
|
: []),
|
|
228
254
|
` Verify: ${task.verify ?? "(none)"}`,
|
|
229
255
|
...(task.result ? [" Result:", ...indent(cap(task.result))] : []),
|
|
230
|
-
...(task.errorMessage ? [" Error:", ...indent(cap(task.errorMessage))] : []),
|
|
231
256
|
` Created: ${new Date(task.createdAt).toLocaleString()}`,
|
|
232
257
|
...(task.completedAt ? [` Completed: ${new Date(task.completedAt).toLocaleString()}`] : []),
|
|
233
258
|
];
|
|
@@ -245,8 +270,8 @@ function buildRoleDetail(name: string): string[] {
|
|
|
245
270
|
` Tools: ${def.tools.join(", ") || "(role defaults)"}`,
|
|
246
271
|
` Model: ${describeSpawnModel(def.model)}`,
|
|
247
272
|
` Verify: ${def.verify ?? "(none)"}`,
|
|
248
|
-
`
|
|
249
|
-
` Living instances: ${
|
|
273
|
+
` Data: ${def.scope === "session" ? "temporary; no-session, no Memory" : "durable; private Memory, scratch workspace and project history"}`,
|
|
274
|
+
` Living instances: ${livingAgents().filter((teammate) => teammate.role === def.name).length}`,
|
|
250
275
|
"",
|
|
251
276
|
"== role prompt ==",
|
|
252
277
|
...indent(cap(def.prompt)),
|
|
@@ -266,21 +291,21 @@ function describeSpawnModel(pinned: string | undefined): string {
|
|
|
266
291
|
|
|
267
292
|
// ── Full-screen console ───────────────────────────────────────────
|
|
268
293
|
|
|
269
|
-
type ConsolePage = "roster" | "
|
|
294
|
+
type ConsolePage = "roster" | "tasks";
|
|
270
295
|
|
|
271
296
|
interface RosterRow {
|
|
272
297
|
key: string;
|
|
273
|
-
kind: "
|
|
298
|
+
kind: "agent";
|
|
274
299
|
}
|
|
275
300
|
interface RoleRow {
|
|
276
301
|
key: string;
|
|
277
302
|
kind: "role";
|
|
278
303
|
}
|
|
279
|
-
interface
|
|
304
|
+
interface TaskRow {
|
|
280
305
|
key: string;
|
|
281
306
|
kind: "task";
|
|
282
307
|
}
|
|
283
|
-
type ConsoleRow = RosterRow | RoleRow |
|
|
308
|
+
type ConsoleRow = RosterRow | RoleRow | TaskRow;
|
|
284
309
|
|
|
285
310
|
/** SGR mouse-wheel events carry a button code; return signed line delta. */
|
|
286
311
|
function wheelDelta(data: string): number | undefined {
|
|
@@ -298,7 +323,7 @@ function wheelDelta(data: string): number | undefined {
|
|
|
298
323
|
const LIST_CHROME_LINES = 5;
|
|
299
324
|
|
|
300
325
|
/** Full-screen Team Console — owns input via ctx.ui.custom. Pages: roster /
|
|
301
|
-
*
|
|
326
|
+
* tasks; each row opens a scrolling detail view; m opens the searchable
|
|
302
327
|
* teammate-model picker. */
|
|
303
328
|
export function openTeamConsole(ctx: {
|
|
304
329
|
ui: ExtensionUIContext;
|
|
@@ -308,9 +333,9 @@ export function openTeamConsole(ctx: {
|
|
|
308
333
|
let page: ConsolePage = "roster";
|
|
309
334
|
let mode: "list" | "detail" | "picker" = "list";
|
|
310
335
|
let selectedRoster = 0;
|
|
311
|
-
let
|
|
336
|
+
let selectedTasks = 0;
|
|
312
337
|
let listOffset = 0;
|
|
313
|
-
let detailKind: ConsoleRow["kind"] = "
|
|
338
|
+
let detailKind: ConsoleRow["kind"] = "agent";
|
|
314
339
|
let detailKey = "";
|
|
315
340
|
let offset = 0;
|
|
316
341
|
let confirmName: string | undefined;
|
|
@@ -338,7 +363,7 @@ export function openTeamConsole(ctx: {
|
|
|
338
363
|
if (renderTimer) return;
|
|
339
364
|
renderTimer = setInterval(() => {
|
|
340
365
|
// Redraw only while something can animate (spinner/activity/stall).
|
|
341
|
-
if (
|
|
366
|
+
if (livingAgents().some(isWorking)) requestRender();
|
|
342
367
|
}, PI_SPINNER_INTERVAL_MS);
|
|
343
368
|
renderTimer.unref?.();
|
|
344
369
|
};
|
|
@@ -351,7 +376,7 @@ export function openTeamConsole(ctx: {
|
|
|
351
376
|
|
|
352
377
|
const style = createPiThemeStyle(theme);
|
|
353
378
|
|
|
354
|
-
// ──
|
|
379
|
+
// ── Agent-model picker (type-to-filter) ──────────────────
|
|
355
380
|
let picker: SearchPicker<{ provider: string; id: string; name?: string }> | undefined;
|
|
356
381
|
let pickerOffset = 0;
|
|
357
382
|
|
|
@@ -443,10 +468,10 @@ export function openTeamConsole(ctx: {
|
|
|
443
468
|
if (!picked) return;
|
|
444
469
|
if (isClearEntry(picked)) {
|
|
445
470
|
setTeamDefaultModel(undefined);
|
|
446
|
-
notifyPi(ctx.ui, "
|
|
471
|
+
notifyPi(ctx.ui, "Agent model cleared — Pi picks its default", "info");
|
|
447
472
|
} else {
|
|
448
473
|
setTeamDefaultModel(modelLabel(picked));
|
|
449
|
-
notifyPi(ctx.ui, `
|
|
474
|
+
notifyPi(ctx.ui, `Agent model set to ${modelLabel(picked)} for this session`, "info");
|
|
450
475
|
}
|
|
451
476
|
closeModelPicker();
|
|
452
477
|
return;
|
|
@@ -463,7 +488,7 @@ export function openTeamConsole(ctx: {
|
|
|
463
488
|
// The console is the management surface: idle and stopped stay
|
|
464
489
|
// visible, and persistent agent roles are listed after the session.
|
|
465
490
|
? [
|
|
466
|
-
...
|
|
491
|
+
...listAgents().map((t) => ({ key: t.name, kind: "agent" as const })),
|
|
467
492
|
...getRoles().map((def) => ({ key: def.name, kind: "role" as const })),
|
|
468
493
|
]
|
|
469
494
|
: listTasks().map((task) => ({ key: task.id, kind: "task" as const }));
|
|
@@ -479,9 +504,9 @@ export function openTeamConsole(ctx: {
|
|
|
479
504
|
};
|
|
480
505
|
|
|
481
506
|
const headerLine = (): string => {
|
|
482
|
-
const alive =
|
|
507
|
+
const alive = livingAgents();
|
|
483
508
|
const tasks = listTasks();
|
|
484
|
-
return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles ·
|
|
509
|
+
return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · tasks ${tasks.filter((task) => !isTaskReplaced(task) && task.status === "pending").length}p/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "in_progress").length}c/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "blocked").length}b/${tasks.filter((task) => !isTaskReplaced(task) && task.status === "cancelled").length}x/${tasks.filter((task) => task.status === "completed").length}d/${tasks.filter((task) => isTaskReplaced(task)).length}s · model ${getTeamDefaultModel() ?? "auto"} · ${page}`;
|
|
485
510
|
};
|
|
486
511
|
|
|
487
512
|
interface ContentLine {
|
|
@@ -491,14 +516,14 @@ export function openTeamConsole(ctx: {
|
|
|
491
516
|
}
|
|
492
517
|
|
|
493
518
|
const teammateRowText = (key: string, selected: boolean): string => {
|
|
494
|
-
const teammate =
|
|
519
|
+
const teammate = getAgent(key)!;
|
|
495
520
|
const marker = selected ? style.accent("❯ ") : " ";
|
|
496
521
|
const name = renderLiveActivityIdentity(teammate.name, theme, "@");
|
|
497
522
|
if (isWorking(teammate)) {
|
|
498
523
|
const prefix = `${marker}${name} `;
|
|
499
524
|
const width = tui.terminal.columns;
|
|
500
525
|
const available = Math.max(0, width - prefix.length - PI_SPINNER_FRAMES[spinnerFrame].length - 2);
|
|
501
|
-
const status = theme.fg("warning",
|
|
526
|
+
const status = theme.fg("warning", formatAgentLabel(PI_SPINNER_FRAMES[spinnerFrame], runningTeammateActivity(teammate) + stallSuffix(teammate), available));
|
|
502
527
|
return `${prefix}${status}`;
|
|
503
528
|
}
|
|
504
529
|
if (teammate.status === "idle") {
|
|
@@ -511,7 +536,7 @@ export function openTeamConsole(ctx: {
|
|
|
511
536
|
const def = getRoles().find((candidate) => candidate.name === key)!;
|
|
512
537
|
const marker = selected ? style.accent("❯ ") : " ";
|
|
513
538
|
const name = renderLiveActivityIdentity(key, theme, "@");
|
|
514
|
-
const live =
|
|
539
|
+
const live = livingAgents().filter((teammate) => teammate.role === key && teammate.status !== "stopped").length;
|
|
515
540
|
const liveTag = live > 0 ? style.success(`[${live} live]`) : style.dim("[0 live]");
|
|
516
541
|
const provenance = def.scope === "session" ? "in-memory" : displaySource(def.source);
|
|
517
542
|
return `${marker}${name} ${style.dim(def.scope)} ${style.dim(provenance)} ${liveTag}`;
|
|
@@ -524,38 +549,39 @@ export function openTeamConsole(ctx: {
|
|
|
524
549
|
const holder = task.claimedBy ? style.dim(` @${task.claimedBy}`) : "";
|
|
525
550
|
const statusText = task.status === "completed"
|
|
526
551
|
? style.success("✓")
|
|
527
|
-
: task
|
|
528
|
-
? style.dim("
|
|
529
|
-
: task.status === "
|
|
530
|
-
? theme.fg("warning", "
|
|
531
|
-
: style.dim("
|
|
552
|
+
: isTaskReplaced(task)
|
|
553
|
+
? style.dim("replaced")
|
|
554
|
+
: task.status === "in_progress"
|
|
555
|
+
? theme.fg("warning", "in progress")
|
|
556
|
+
: task.status === "blocked" ? style.dim("blocked")
|
|
557
|
+
: task.status === "cancelled" ? style.dim("cancelled") : style.dim("pending");
|
|
532
558
|
return `${marker}${label} ${statusText} ${theme.fg("customMessageText", task.subject)}${holder}`;
|
|
533
559
|
};
|
|
534
560
|
|
|
535
561
|
const buildContent = (): ContentLine[] => {
|
|
536
562
|
const rows = currentRows();
|
|
537
563
|
const lines: ContentLine[] = [];
|
|
538
|
-
if (page === "
|
|
564
|
+
if (page === "tasks") {
|
|
539
565
|
if (rows.length === 0) {
|
|
540
566
|
lines.push({ text: style.dim("The Work list is empty. Create Work with work action=create."), select: -1 });
|
|
541
567
|
}
|
|
542
568
|
rows.forEach((row, index) => lines.push({ text: taskRowText(row.key, index === currentPageSelection(rows.length)), select: index }));
|
|
543
569
|
return lines;
|
|
544
570
|
}
|
|
545
|
-
const
|
|
571
|
+
const agents = rows.filter((row): row is RosterRow => row.kind === "agent");
|
|
546
572
|
const roles = rows.filter((row): row is RoleRow => row.kind === "role");
|
|
547
573
|
const selection = currentPageSelection(rows.length);
|
|
548
|
-
lines.push({ text: style.dim("==
|
|
549
|
-
if (
|
|
550
|
-
lines.push({ text: style.dim("No
|
|
574
|
+
lines.push({ text: style.dim("== agents (this session) =="), select: -1 });
|
|
575
|
+
if (agents.length === 0) {
|
|
576
|
+
lines.push({ text: style.dim("No agents this session. Ask the model to spawn one."), select: -1 });
|
|
551
577
|
}
|
|
552
|
-
|
|
578
|
+
agents.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
|
|
553
579
|
lines.push({ text: "", select: -1 });
|
|
554
580
|
lines.push({ text: style.dim("== agent roles (persistent and session definitions) =="), select: -1 });
|
|
555
581
|
if (roles.length === 0) {
|
|
556
582
|
lines.push({ text: style.dim("No agent definitions discovered."), select: -1 });
|
|
557
583
|
}
|
|
558
|
-
roles.forEach((row, index) => lines.push({ text: roleRowText(row.key,
|
|
584
|
+
roles.forEach((row, index) => lines.push({ text: roleRowText(row.key, agents.length + index === selection), select: agents.length + index }));
|
|
559
585
|
return lines;
|
|
560
586
|
};
|
|
561
587
|
|
|
@@ -572,9 +598,9 @@ export function openTeamConsole(ctx: {
|
|
|
572
598
|
}
|
|
573
599
|
listOffset = clampConsoleScroll(listOffset, content.length, viewport);
|
|
574
600
|
const footer = confirmName
|
|
575
|
-
? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the
|
|
601
|
+
? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the tasks · y yes / n no`)
|
|
576
602
|
: style.dim(page === "roster"
|
|
577
|
-
? "↑↓ select · enter open · tab
|
|
603
|
+
? "↑↓ select · enter open · tab tasks · x shutdown · m model · esc/q close"
|
|
578
604
|
: "↑↓ select · enter open · tab roster · esc/q close");
|
|
579
605
|
return renderPiPanel({
|
|
580
606
|
width,
|
|
@@ -587,7 +613,7 @@ export function openTeamConsole(ctx: {
|
|
|
587
613
|
};
|
|
588
614
|
|
|
589
615
|
const currentPageSelection = (rowCount: number): number => {
|
|
590
|
-
const selection = page === "roster" ? selectedRoster :
|
|
616
|
+
const selection = page === "roster" ? selectedRoster : selectedTasks;
|
|
591
617
|
return rowCount === 0 ? 0 : Math.min(selection, rowCount - 1);
|
|
592
618
|
};
|
|
593
619
|
|
|
@@ -638,7 +664,7 @@ export function openTeamConsole(ctx: {
|
|
|
638
664
|
}
|
|
639
665
|
const rows = currentRows();
|
|
640
666
|
if (matchesKey(data, Key.tab)) {
|
|
641
|
-
page = page === "roster" ? "
|
|
667
|
+
page = page === "roster" ? "tasks" : "roster";
|
|
642
668
|
listOffset = 0;
|
|
643
669
|
return;
|
|
644
670
|
}
|
|
@@ -658,7 +684,7 @@ export function openTeamConsole(ctx: {
|
|
|
658
684
|
if (data === "x" || data === "X") {
|
|
659
685
|
const selection = currentPageSelection(rows.length);
|
|
660
686
|
const row = rows[selection];
|
|
661
|
-
if (page === "roster" && row?.kind === "
|
|
687
|
+
if (page === "roster" && row?.kind === "agent" && getAgent(row.key)?.status !== "stopped") {
|
|
662
688
|
confirmName = row.key;
|
|
663
689
|
}
|
|
664
690
|
return;
|
|
@@ -701,7 +727,7 @@ export function openTeamConsole(ctx: {
|
|
|
701
727
|
function bumpSelection(delta: number, rowCount: number): void {
|
|
702
728
|
if (rowCount === 0) return;
|
|
703
729
|
if (page === "roster") selectedRoster = clampIndex(selectedRoster + delta, rowCount);
|
|
704
|
-
else
|
|
730
|
+
else selectedTasks = clampIndex(selectedTasks + delta, rowCount);
|
|
705
731
|
}
|
|
706
732
|
|
|
707
733
|
function scrollList(delta: number): void {
|
|
@@ -737,7 +763,7 @@ export function openTeamConsole(ctx: {
|
|
|
737
763
|
}
|
|
738
764
|
|
|
739
765
|
async function shutdownFromConsole(ctx: { ui: ExtensionUIContext }, name: string): Promise<void> {
|
|
740
|
-
const result = await
|
|
766
|
+
const result = await shutdownAgent(name);
|
|
741
767
|
if (result.ok) notifyPi(ctx.ui, result.body, "info");
|
|
742
768
|
else notifyPi(ctx.ui, result.error, "error");
|
|
743
769
|
}
|