@fradser/pi-agent-teams 0.9.2 → 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/.memory/MEMORY.md +1 -0
- package/.memory/attempt-bound-authority.md +19 -0
- package/README.md +137 -58
- 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 +49 -37
- package/src/index.ts +149 -30
- package/src/recipient.ts +4 -4
- package/src/state.ts +163 -607
- package/src/statefile.ts +28 -244
- package/src/team-machine.ts +827 -535
- package/src/tool-copy.ts +26 -34
- package/src/tools.ts +57 -210
- package/src/types.ts +56 -157
- package/src/ui.ts +85 -59
- package/src/worker.ts +152 -79
- 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,148 +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
|
-
createdAt: number;
|
|
120
|
-
updatedAt: number;
|
|
121
|
-
completedAt?: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** A claim or submission intent expressed by a worker through marker files.
|
|
125
|
-
* Workers never write the board file itself. `status` applies to submissions. */
|
|
126
|
-
export interface TaskIntent {
|
|
127
|
-
taskId: string;
|
|
128
|
-
worker: string;
|
|
129
|
-
spawnId: string;
|
|
130
|
-
assignmentId?: string;
|
|
131
|
-
status?: "completed" | "failed";
|
|
132
|
-
result?: string;
|
|
133
|
-
timestamp: number;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Spawn ─────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
/** Token/cost usage reported by a teammate (accumulated across sequences). */
|
|
139
|
-
export interface WorkerUsage {
|
|
140
|
-
input: number;
|
|
141
|
-
output: number;
|
|
142
|
-
cacheRead: number;
|
|
143
|
-
cacheWrite: number;
|
|
144
|
-
totalTokens: number;
|
|
145
|
-
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;
|
|
146
37
|
}
|
|
147
38
|
|
|
148
39
|
// ── Mailbox ───────────────────────────────────────────────────────
|
|
@@ -216,13 +107,10 @@ export interface InboxMessage {
|
|
|
216
107
|
|
|
217
108
|
export const InlineAgentDefinitionParams = Type.Object({
|
|
218
109
|
description: Type.String({ minLength: 1, description: "Routing contract for the generated Agent" }),
|
|
219
|
-
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.` })),
|
|
220
111
|
model: Type.Optional(Type.String({ description: "Provider/model pin or inherit" })),
|
|
221
112
|
verify: Type.Optional(Type.String({ description: "Default Work verification gate" })),
|
|
222
|
-
worktree: Type.Optional(Type.Boolean({ description: "Dedicated Git worktree" })),
|
|
223
113
|
prompt: Type.String({ minLength: 1, description: "Agent role prompt" }),
|
|
224
|
-
persist: Type.Optional(Type.Boolean({ description: "Persist only when explicitly requested" })),
|
|
225
|
-
persistScope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("project-local")])),
|
|
226
114
|
}, { additionalProperties: false });
|
|
227
115
|
|
|
228
116
|
/** Some harnesses deliver object/array parameters as JSON strings and Pi
|
|
@@ -260,6 +148,10 @@ function mergeBranchSubSchemas(pool: any[]): any {
|
|
|
260
148
|
return { anyOf: pool };
|
|
261
149
|
}
|
|
262
150
|
|
|
151
|
+
/** The root also declares `type: "object"`. A schema that carries `properties`
|
|
152
|
+
* without an explicit object type is rejected by Google's GenerateContent API
|
|
153
|
+
* ("parameters.properties: only allowed for OBJECT type"), which the Gemini
|
|
154
|
+
* routes reach through their provider translators. */
|
|
263
155
|
function actionUnion<T extends TObject[]>(branches: [...T], options?: Record<string, unknown>): TUnion<T> {
|
|
264
156
|
const pools: Record<string, any[]> = {};
|
|
265
157
|
for (const branch of branches) {
|
|
@@ -273,7 +165,7 @@ function actionUnion<T extends TObject[]>(branches: [...T], options?: Record<str
|
|
|
273
165
|
}
|
|
274
166
|
const properties: Record<string, any> = {};
|
|
275
167
|
for (const [key, pool] of Object.entries(pools)) properties[key] = mergeBranchSubSchemas(pool);
|
|
276
|
-
return Type.Union(branches, { ...options, properties } as never) as TUnion<T>;
|
|
168
|
+
return Type.Union(branches, { type: "object", ...options, properties } as never) as TUnion<T>;
|
|
277
169
|
}
|
|
278
170
|
|
|
279
171
|
/** Tolerate harnesses that deliver object or array parameters as JSON strings. */
|
|
@@ -314,7 +206,7 @@ export const AgentActionParams = actionUnion([
|
|
|
314
206
|
], { description: "Delegate, start, inspect, or stop an Agent session" });
|
|
315
207
|
|
|
316
208
|
/** Shut down one living teammate. */
|
|
317
|
-
/** Create a
|
|
209
|
+
/** Create a task set task (leader-only). */
|
|
318
210
|
/** First public Work interface slice. Work creation reuses the current
|
|
319
211
|
* single-writer task record until every acquisition path shares one lifecycle. */
|
|
320
212
|
export const WorkToolParams = actionUnion([
|
|
@@ -353,12 +245,19 @@ export const WorkToolParams = actionUnion([
|
|
|
353
245
|
}, { additionalProperties: false }),
|
|
354
246
|
], { description: "Create, list, assign, release, reopen, or supersede current Work Items" });
|
|
355
247
|
|
|
356
|
-
/** Shared leader/worker read-only
|
|
357
|
-
/**
|
|
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
|
+
*/
|
|
358
257
|
export const AgentEventParams = Type.Object({
|
|
359
|
-
|
|
258
|
+
body: Type.String({ description: "Message content" }),
|
|
360
259
|
to: Type.Optional(Type.String({ minLength: 1, description: "Recipient Agent, leader, or exact session route" })),
|
|
361
|
-
|
|
260
|
+
kind: Type.Optional(Type.Union([Type.Literal("inform"), Type.Literal("request")])),
|
|
362
261
|
}, { additionalProperties: false });
|
|
363
262
|
|
|
364
263
|
/** The reserved recipient name for reports to the team leader. */
|
|
@@ -366,7 +265,7 @@ export const LEADER_RECIPIENT = "leader";
|
|
|
366
265
|
|
|
367
266
|
/** The single messaging primitive: addressed peer mail and leader reports.
|
|
368
267
|
* `status` is honored only for to="leader" terminal reports. */
|
|
369
|
-
/** Self-claim a pending
|
|
268
|
+
/** Self-claim a pending tasks task. */
|
|
370
269
|
/** Worker-only Work claim operation. It queues an intent; the single-writer
|
|
371
270
|
* harness remains the only authority that can grant ownership. */
|
|
372
271
|
export const WorkerWorkToolParams = actionUnion([
|
|
@@ -393,8 +292,8 @@ export const TEAM_RUNTIME_VERSION = 2;
|
|
|
393
292
|
|
|
394
293
|
export interface TeamState {
|
|
395
294
|
runtimeVersion: number;
|
|
396
|
-
|
|
397
|
-
tasks: Record<string,
|
|
295
|
+
agents: Record<string, Agent>;
|
|
296
|
+
tasks: Record<string, Task>;
|
|
398
297
|
/** Unified teammate model for this session; spawns without a role pin use it. */
|
|
399
298
|
defaultModel?: string;
|
|
400
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
|
}
|