@fradser/pi-agent-teams 0.5.3 → 0.7.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/src/types.ts CHANGED
@@ -1,103 +1,102 @@
1
1
  import type { Static } from "typebox";
2
2
  import { Type } from "typebox";
3
3
 
4
- // ── Run / Node ────────────────────────────────────────────────────
4
+ // ── Teammate ──────────────────────────────────────────────────────
5
5
 
6
- export const NodeStatus = Type.Union(
6
+ export const TeammateStatus = Type.Union(
7
7
  [
8
- Type.Literal("pending"),
9
- Type.Literal("running"),
10
- Type.Literal("completed"),
11
- Type.Literal("failed"),
12
- Type.Literal("cancelled"),
8
+ Type.Literal("starting"),
9
+ Type.Literal("idle"),
10
+ Type.Literal("working"),
11
+ Type.Literal("stopped"),
13
12
  ],
14
- { description: "Current status of a run node" },
13
+ { description: "Lifecycle of a resident teammate" },
15
14
  );
16
- export type NodeStatus = Static<typeof NodeStatus>;
15
+ export type TeammateStatus = Static<typeof TeammateStatus>;
16
+
17
+ /** A named, long-lived child Pi process on the team roster. */
18
+ export interface Teammate {
19
+ /** Unique among living teammates; also the mailbox and roster key. */
20
+ name: string;
21
+ /** Resolved agent definition name. */
22
+ agent: string;
23
+ /** Per-spawn capability identity, regenerated for every process. */
24
+ spawnId: string;
25
+ pid: number;
26
+ status: TeammateStatus;
27
+ /** Working directory (the worktree root when isolated). */
28
+ cwd?: string;
29
+ /** Whether this teammate owns a dedicated Git worktree. */
30
+ isolation: "worktree" | "none";
31
+ /** Board task currently claimed by this teammate, if any. */
32
+ currentTaskId?: string;
33
+ /** Live assistant text assembled from the RPC stream. */
34
+ liveText?: string;
35
+ /** Current child tool name, if a tool is executing. */
36
+ activeTool?: string;
37
+ /** Live assistant reasoning streamed while no tool runs. */
38
+ liveThinking?: string;
39
+ /** Assistant turns observed in the current wake-up sequence. */
40
+ turns?: number;
41
+ /** The child finished its current sequence and awaits the next prompt. */
42
+ sequenceEnded?: boolean;
43
+ /** When the harness last sent a claimable-task notice to this teammate. */
44
+ lastNoticeAt?: number;
45
+ usage?: WorkerUsage;
46
+ error?: string;
47
+ createdAt: number;
48
+ updatedAt: number;
49
+ stoppedAt?: number;
50
+ /** Last wall-clock time output was observed (any stream event or prompt delivery). */
51
+ lastOutputAt?: number;
52
+ /** When the current stall episode notice was sent (one per episode). */
53
+ stallNoticeSentAt?: number;
54
+ }
55
+
56
+ // ── Task board ────────────────────────────────────────────────────
17
57
 
18
- export const RunStatus = Type.Union(
58
+ export const TaskStatus = Type.Union(
19
59
  [
20
- Type.Literal("running"),
60
+ Type.Literal("pending"),
61
+ Type.Literal("claimed"),
21
62
  Type.Literal("completed"),
22
- Type.Literal("failed"),
23
- Type.Literal("cancelled"),
24
63
  ],
25
- { description: "Current status of a run" },
64
+ { description: "Board lifecycle of a task" },
26
65
  );
27
- export type RunStatus = Static<typeof RunStatus>;
66
+ export type TaskStatus = Static<typeof TaskStatus>;
28
67
 
29
- /** One task in a dispatched run: a bounded child-process unit of work. */
30
- export interface Node {
31
- /** User-chosen id, unique within the run. */
68
+ /** One task on the shared board. Only the leader process writes board state. */
69
+ export interface BoardTask {
32
70
  id: string;
33
- /** Stable node identity: `${runId}:${nodeId}`. */
34
- workerKey: string;
35
- /** Resolved agent definition name. */
36
- agent: string;
37
- /** The specific task text handed to the worker. */
38
- prompt: string;
39
- /** Repository-relative paths used to coordinate this node. */
40
- paths: string[];
41
- /** Read nodes may overlap; write nodes need shared-workspace conflict protection. */
42
- access: "read" | "write";
43
- /** Optional per-node model pin (provider/model). */
44
- model?: string;
45
- /** RPC mode keeps stdin available for runtime steering. */
46
- mode?: "json" | "rpc";
47
- /** Optional maximum number of assistant turns before the worker is stopped. */
48
- turnBudget?: number;
49
- /** Node ids that must complete before this node may start. */
71
+ subject: string;
72
+ description?: string;
73
+ /** Task ids that must complete before this task is claimable. */
50
74
  dependsOn: string[];
51
- /** Named upstream node ids whose results are included as fork context. */
52
- forkContext?: string[];
53
- /** Named input bindings using `nodeId#/json/pointer` sources. */
54
- inputBindings?: Record<string, string>;
55
- status: NodeStatus;
75
+ /** Deterministic completion gate; overrides the agent-role default. */
76
+ verify?: string;
77
+ status: TaskStatus;
78
+ claimedBy?: string;
56
79
  result?: string;
57
- /** Named structured outputs emitted through teammate_message. */
58
- namedOutputs?: Record<string, string>;
59
- /** Bounded JSON output emitted through teammate_message for downstream data flow. */
60
- structuredOutput?: unknown;
61
80
  errorMessage?: string;
62
- /** True once the terminal node follow-up has been delivered. */
63
- nodeFollowUpSent?: boolean;
64
- /** Real child-process execution info when this node was spawned. */
65
- spawn?: SpawnInfo;
66
81
  createdAt: number;
67
82
  updatedAt: number;
68
83
  completedAt?: number;
69
84
  }
70
85
 
71
- /** A dispatched dependency-aware task graph. */
72
- export interface Run {
73
- id: string;
74
- cwd: string;
75
- status: RunStatus;
76
- concurrency: number;
77
- /** Every node runs in its own git worktree when true. */
78
- worktree: boolean;
79
- /** background=true returns immediately; false gathers in the tool call. */
80
- background: boolean;
81
- /** Run-level failure detail. */
82
- errorMessage?: string;
83
- nodes: Record<string, Node>;
84
- createdAt: number;
85
- updatedAt: number;
86
- finishedAt?: number;
87
- /** True once the run-completion follow-up was delivered, or claimed by wait/foreground gather. */
88
- completionNotified?: boolean;
89
- /** True once the run-settled leader summary was sent (onRunSettled is idempotent). */
90
- settledMessageSent?: boolean;
91
- /** Synthesized final summary produced by the optional __summary node. */
92
- summary?: string;
86
+ /** A claim or submission intent expressed by a worker through marker files.
87
+ * Workers never write the board file itself. `status` applies to submissions. */
88
+ export interface TaskIntent {
89
+ taskId: string;
90
+ worker: string;
91
+ spawnId: string;
92
+ status?: "completed" | "failed";
93
+ result?: string;
94
+ timestamp: number;
93
95
  }
94
96
 
95
97
  // ── Spawn ─────────────────────────────────────────────────────────
96
98
 
97
- /** Lifecycle of a spawned child Pi process running a node. */
98
- export type SpawnStatus = "running" | "completed" | "failed";
99
-
100
- /** Token/cost usage reported by a finished worker (from JSON-mode output). */
99
+ /** Token/cost usage reported by a teammate (accumulated across sequences). */
101
100
  export interface WorkerUsage {
102
101
  input: number;
103
102
  output: number;
@@ -107,38 +106,6 @@ export interface WorkerUsage {
107
106
  cost: number;
108
107
  }
109
108
 
110
- export interface SpawnInfo {
111
- /** Per-spawn capability identity, regenerated for every worker process. */
112
- spawnId: string;
113
- mode?: "json" | "rpc";
114
- pid: number;
115
- status: SpawnStatus;
116
- startedAt: number;
117
- /** Live assistant text assembled from JSON-mode stream events. */
118
- liveText?: string;
119
- /** Current child tool name, if a tool is executing. */
120
- activeTool?: string;
121
- /** Live assistant reasoning streamed from the child (shown while no tool runs). */
122
- liveThinking?: string;
123
- /** Number of assistant turns observed from the child JSON stream. */
124
- turns?: number;
125
- /** The child emitted a final successful assistant response and should close promptly. */
126
- finalResponse?: boolean;
127
- finishedAt?: number;
128
- exitCode?: number;
129
- /** Truncated stdout of the child process. */
130
- stdout?: string;
131
- /** Truncated stderr of the child process. */
132
- stderr?: string;
133
- /** Spawn-level error (e.g. CLI resolution failure). */
134
- error?: string;
135
- /** Token/cost usage reported by the worker, when available. */
136
- usage?: WorkerUsage;
137
- timedOut?: boolean;
138
- /** Whether this spawn owns a dedicated Git worktree. */
139
- isolation?: "worktree" | "none";
140
- }
141
-
142
109
  // ── Mailbox ───────────────────────────────────────────────────────
143
110
 
144
111
  export interface MailboxMessage {
@@ -146,124 +113,121 @@ export interface MailboxMessage {
146
113
  from: string;
147
114
  subject: string;
148
115
  body: string;
149
- runId?: string;
116
+ status?: "in_progress" | "completed" | "failed";
150
117
  timestamp: number;
151
118
  }
152
119
 
153
- /** Append-only event emitted by a worker and applied by the team leader. */
154
- export interface WorkerMessageEvent {
120
+ /** Append-only report event emitted by a teammate to the leader. The title
121
+ * is derived by the harness from the first line of the body. */
122
+ export interface WorkerReportEvent {
155
123
  id: string;
156
124
  type: "message";
157
125
  worker: string;
158
126
  spawnId: string;
159
- subject: string;
160
127
  body: string;
161
128
  status?: "in_progress" | "completed" | "failed";
162
- data?: {
163
- kind?: "named_output" | "output";
164
- name?: string;
165
- value?: string;
166
- output?: unknown;
167
- message?: string;
168
- };
169
129
  }
170
130
 
171
- export type WorkerEvent = WorkerMessageEvent;
131
+ export type WorkerEvent = WorkerReportEvent;
132
+
133
+ /** Structural guard applied to every outbox record before it is trusted. */
134
+ export function isWorkerEvent(value: unknown): value is WorkerReportEvent {
135
+ if (!value || typeof value !== "object") return false;
136
+ const event = value as Partial<WorkerReportEvent>;
137
+ return typeof event.id === "string"
138
+ && event.type === "message"
139
+ && typeof event.worker === "string"
140
+ && typeof event.spawnId === "string"
141
+ && typeof event.body === "string"
142
+ && (event.status === undefined || ["in_progress", "completed", "failed"].includes(event.status));
143
+ }
172
144
 
173
- // ── Tool parameter schemas (typebox) ──────────────────────────────
145
+ /** Derive a display title from the first non-empty line of a message. */
146
+ export function messageTitle(body: string): string {
147
+ for (const line of body.split("\n")) {
148
+ const trimmed = line.replace(/\s+/g, " ").trim();
149
+ if (trimmed) return trimmed.slice(0, 120);
150
+ }
151
+ return "(no content)";
152
+ }
174
153
 
175
- const NodeAccess = Type.Union(
176
- [Type.Literal("read"), Type.Literal("write")],
177
- { description: "read permits overlapping analysis; write protects overlapping shared-workspace changes. Default: read." },
178
- );
154
+ /** One message in a teammate inbox (peer-to-peer or harness feedback). */
155
+ export interface InboxMessage {
156
+ id: string;
157
+ from: string;
158
+ subject: string;
159
+ body: string;
160
+ timestamp: number;
161
+ }
179
162
 
180
- /** One task inside teammate_run. */
181
- export const RunTaskSpec = Type.Object({
182
- id: Type.String({ description: "Node id, unique within this run" }),
183
- agent: Type.String({ description: "Agent definition name (bundled, user, or project scope)" }),
184
- prompt: Type.String({ minLength: 1, description: "The specific task text handed to this worker" }),
185
- dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Node ids that must complete before this node starts" })),
186
- paths: Type.Array(Type.String({ description: "Repository-relative path this node may inspect or modify" }), {
187
- minItems: 1,
188
- description: "Repository-relative paths used to coordinate this node",
189
- }),
190
- access: Type.Optional(NodeAccess),
191
- model: Type.Optional(Type.String({ description: "Optional per-node provider/model pin" })),
192
- mode: Type.Optional(Type.Union([Type.Literal("json"), Type.Literal("rpc")], { description: "Worker process mode; rpc enables runtime steering" })),
193
- turnBudget: Type.Optional(Type.Integer({ minimum: 1, description: "Optional maximum assistant turns before the worker is stopped (default: 100; high safety cap for edge cases)" })),
194
- forkContext: Type.Optional(Type.Array(Type.String(), { description: "Named upstream node ids whose results are included as fork context" })),
195
- inputBindings: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Named inputs bound from dependency outputs using nodeId#/json/pointer" })),
196
- });
163
+ // ── Tool parameter schemas (typebox) ──────────────────────────────
197
164
 
198
- /** Dispatch a dependency-aware task graph in one call. */
199
- export const TeammateRunParams = Type.Object({
200
- tasks: Type.Array(RunTaskSpec, {
201
- minItems: 1,
202
- maxItems: 32,
203
- description: "Task graph to dispatch; dependsOn edges must form a DAG",
204
- }),
205
- concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 32, description: "Max nodes running at once (default: 4)" })),
206
- worktree: Type.Optional(Type.Boolean({ description: "Run every node in its own git worktree (default: false)" })),
207
- background: Type.Optional(Type.Boolean({ default: true, description: "Return immediately and deliver one completion follow-up. Default: true — teammates always run in the background; workers message team-leader with deliverables upon completion." })),
208
- summarize: Type.Optional(Type.Boolean({ description: "Append a __summary node after all leaf nodes. Default: true when the run has more than one user task, false for a single task." })),
209
- summaryAgent: Type.Optional(Type.String({ description: "Agent used for the summary node when summarize is on (default: observer)" })),
210
- cwd: Type.Optional(Type.String({ description: "Working directory for this run (default: the session cwd)" })),
165
+ /** Spawn one named resident teammate. */
166
+ export const TeammateSpawnParams = Type.Object({
167
+ name: Type.String({ minLength: 1, description: "Teammate name, unique among living teammates; used for messaging and claiming" }),
168
+ agent: Type.String({ description: "Agent definition name (from user, project, or project-local scopes); its frontmatter pins model and optional worktree isolation" }),
169
+ prompt: Type.Optional(Type.String({ description: "Optional kickoff prompt delivered as the teammate's first turn; omit to let it wait for messages or board claims" })),
211
170
  });
212
171
 
213
- /** Explicit cancel of a run or single node. */
214
- export const TeammateCancelParams = Type.Object({
215
- runId: Type.String({ description: "Run id to cancel" }),
216
- nodeId: Type.Optional(Type.String({ description: "Cancel one node (and its not-yet-started dependents) instead of the whole run; the run continues" })),
172
+ /** Shut down one living teammate. */
173
+ export const TeammateShutdownParams = Type.Object({
174
+ name: Type.String({ description: "Teammate name on the roster" }),
217
175
  });
218
176
 
219
- /** Retry the failed/cancelled nodes of a settled run. */
220
- export const TeammateRetryParams = Type.Object({
221
- runId: Type.String({ description: "Run id of a settled (failed/cancelled/completed) run" }),
222
- nodeIds: Type.Optional(Type.Array(Type.String(), { description: "Node ids to reset and re-run; defaults to all failed and cancelled nodes" })),
177
+ /** Create a board task (leader-only). */
178
+ export const TaskCreateParams = Type.Object({
179
+ subject: Type.String({ minLength: 1, description: "Short task title shown on the board" }),
180
+ description: Type.Optional(Type.String({ description: "Full task description for the claiming teammate" }),
181
+ ),
182
+ dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Task ids that must complete before this task is claimable" })),
183
+ verify: Type.Optional(Type.String({ description: "Deterministic completion gate command; zero exit completes the task. Overrides any agent-role default verify." })),
223
184
  });
224
185
 
225
- /** Worker progress message or final report to the team leader. */
226
- export const TeammateMessageParams = Type.Object({
227
- subject: Type.String({ description: "Concise report subject" }),
228
- body: Type.String({ description: "Progress note or full final deliverable for the team leader" }),
186
+ /** Shared leader/worker read-only board view. */
187
+ export const TaskListParams = Type.Object({});
188
+
189
+ /** The reserved recipient name for reports to the team leader. */
190
+ export const LEADER_RECIPIENT = "leader";
191
+
192
+ /** The single messaging primitive: addressed peer mail and leader reports.
193
+ * `status` is honored only for to="leader" terminal reports. */
194
+ export const SendMessageParams = Type.Object({
195
+ to: Type.String({ minLength: 1, description: 'Recipient: a teammate name on the roster, or "leader" to report to the team leader' }),
196
+ message: Type.String({ description: "Message content; the first line becomes the title shown in the console" }),
229
197
  status: Type.Optional(Type.Union([
230
198
  Type.Literal("in_progress"),
231
199
  Type.Literal("completed"),
232
200
  Type.Literal("failed"),
233
- ], { description: "Optional worker status; use completed or failed for the final deliverable" })),
234
- data: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Structured teammate data: named_output or output" })),
201
+ ], { description: 'Only for to="leader": completed or failed ends the current assignment and triggers immediate follow-up delivery' })),
235
202
  });
236
203
 
237
- /** Leader-only operation that fans out a completed node's structured array output. */
238
- export const TeammateFanoutParams = Type.Object({
239
- runId: Type.String({ description: "Source run id" }),
240
- nodeId: Type.String({ description: "Completed source node whose structured output is an array" }),
241
- agent: Type.String({ description: "Agent to run for each item" }),
242
- prompt: Type.String({ minLength: 1, description: "Task prompt; each item is appended as JSON" }),
243
- paths: Type.Array(Type.String(), { minItems: 1, description: "Repository-relative paths for each child run" }),
244
- access: Type.Optional(NodeAccess),
245
- model: Type.Optional(Type.String()),
246
- turnBudget: Type.Optional(Type.Integer({ minimum: 1 })),
247
- concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 32 })),
248
- background: Type.Optional(Type.Boolean({ default: true })),
204
+ /** Self-claim a pending board task. */
205
+ export const TaskClaimParams = Type.Object({
206
+ taskId: Type.Optional(Type.String({ description: "Specific task id to claim; omit to claim the first claimable task" })),
249
207
  });
250
208
 
251
- /** Leader-only runtime steer sent through the existing teammate_message name. */
252
- export const TeammateLeaderMessageParams = Type.Object({
253
- target: Type.String({ description: "Run-qualified worker key, for example run_1:inspect" }),
254
- body: Type.String({ minLength: 1, description: "Steering message for the running RPC worker" }),
209
+ /** Submit a claimed task outcome. Completion passes through the verify gate. */
210
+ export const TaskSubmitParams = Type.Object({
211
+ taskId: Type.String({ description: "The claimed task id" }),
212
+ status: Type.Union([Type.Literal("completed"), Type.Literal("failed")], { description: "Outcome of the claimed task" }),
213
+ result: Type.Optional(Type.String({ description: "Result summary recorded on the board when completed" })),
255
214
  });
256
215
 
257
216
  // ── State snapshot for persistence ────────────────────────────────
258
217
 
259
- export interface TeammateState {
260
- runs: Record<string, Run>;
261
- /** Single leader inbox for worker terminal reports and leader-bound messages. */
218
+ export interface TeamState {
219
+ teammates: Record<string, Teammate>;
220
+ tasks: Record<string, BoardTask>;
221
+ /** Single leader inbox for teammate reports and harness diagnostics. */
262
222
  leaderMailbox: MailboxMessage[];
263
223
  messageCounter: number;
264
- runCounter: number;
265
- /** Byte offsets consumed by the parent from each worker's append-only outbox. */
224
+ taskCounter: number;
225
+ /** Byte offsets consumed by the parent from each teammate's outbox. */
266
226
  workerEventOffsets: Record<string, number>;
267
- /** Event IDs already applied by the parent, keyed to their worker spawn for compaction. */
227
+ /** Report event ids already applied, keyed by teammate spawn. */
268
228
  workerEventIds: Record<string, string>;
229
+ /** Consumed byte offsets per peer inbox file. */
230
+ peerInboxOffsets: Record<string, number>;
231
+ /** Delivered peer message ids per inbox, capped FIFO for deduplication. */
232
+ peerDeliveredIds: Record<string, string[]>;
269
233
  }