@fradser/pi-agent-teams 0.5.2 → 0.6.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,95 +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
+ }
17
55
 
18
- export const RunStatus = Type.Union(
56
+ // ── Task board ────────────────────────────────────────────────────
57
+
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
- /** Optional per-node hard wall-clock cap before the worker is killed. */
46
- timeoutMs?: number;
47
- /** 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. */
48
74
  dependsOn: string[];
49
- status: NodeStatus;
75
+ /** Deterministic completion gate; overrides the agent-role default. */
76
+ verify?: string;
77
+ status: TaskStatus;
78
+ claimedBy?: string;
50
79
  result?: string;
51
80
  errorMessage?: string;
52
- /** Real child-process execution info when this node was spawned. */
53
- spawn?: SpawnInfo;
54
81
  createdAt: number;
55
82
  updatedAt: number;
56
83
  completedAt?: number;
57
84
  }
58
85
 
59
- /** A dispatched dependency-aware task graph. */
60
- export interface Run {
61
- id: string;
62
- cwd: string;
63
- status: RunStatus;
64
- concurrency: number;
65
- /** Every node runs in its own git worktree when true. */
66
- worktree: boolean;
67
- /** background=true returns immediately; false gathers in the tool call. */
68
- background: boolean;
69
- /** Optional run-level hard wall-clock cap before the run fails. */
70
- timeoutMs?: number;
71
- /** Absolute deadline for timeoutMs, when set. */
72
- deadlineAt?: number;
73
- /** Run-level failure detail (e.g. run timeout). */
74
- errorMessage?: string;
75
- nodes: Record<string, Node>;
76
- createdAt: number;
77
- updatedAt: number;
78
- finishedAt?: number;
79
- /** True once the run-completion follow-up was delivered, or claimed by wait/foreground gather. */
80
- completionNotified?: boolean;
81
- /** True once the run-settled leader summary was sent (onRunSettled is idempotent). */
82
- settledMessageSent?: boolean;
83
- /** Synthesized final summary produced by the optional __summary node. */
84
- 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;
85
95
  }
86
96
 
87
97
  // ── Spawn ─────────────────────────────────────────────────────────
88
98
 
89
- /** Lifecycle of a spawned child Pi process running a node. */
90
- export type SpawnStatus = "running" | "completed" | "failed";
91
-
92
- /** Token/cost usage reported by a finished worker (from JSON-mode output). */
99
+ /** Token/cost usage reported by a teammate (accumulated across sequences). */
93
100
  export interface WorkerUsage {
94
101
  input: number;
95
102
  output: number;
@@ -99,38 +106,6 @@ export interface WorkerUsage {
99
106
  cost: number;
100
107
  }
101
108
 
102
- export interface SpawnInfo {
103
- /** Per-spawn capability identity, regenerated for every worker process. */
104
- spawnId: string;
105
- pid: number;
106
- status: SpawnStatus;
107
- startedAt: number;
108
- /** Live assistant text assembled from JSON-mode stream events. */
109
- liveText?: string;
110
- /** Current child tool name, if a tool is executing. */
111
- activeTool?: string;
112
- /** Live assistant reasoning streamed from the child (shown while no tool runs). */
113
- liveThinking?: string;
114
- /** Number of assistant turns observed from the child JSON stream. */
115
- turns?: number;
116
- /** The child emitted a final successful assistant response and should close promptly. */
117
- finalResponse?: boolean;
118
- finishedAt?: number;
119
- exitCode?: number;
120
- /** Truncated stdout of the child process. */
121
- stdout?: string;
122
- /** Truncated stderr of the child process. */
123
- stderr?: string;
124
- /** Spawn-level error (e.g. CLI resolution failure). */
125
- error?: string;
126
- /** Token/cost usage reported by the worker, when available. */
127
- usage?: WorkerUsage;
128
- /** True when the worker was killed by the spawn timeout. */
129
- timedOut?: boolean;
130
- /** Whether this spawn owns a dedicated Git worktree. */
131
- isolation?: "worktree" | "none";
132
- }
133
-
134
109
  // ── Mailbox ───────────────────────────────────────────────────────
135
110
 
136
111
  export interface MailboxMessage {
@@ -138,94 +113,121 @@ export interface MailboxMessage {
138
113
  from: string;
139
114
  subject: string;
140
115
  body: string;
141
- runId?: string;
116
+ status?: "in_progress" | "completed" | "failed";
142
117
  timestamp: number;
143
118
  }
144
119
 
145
- /** Append-only event emitted by a worker and applied by the team leader. */
146
- 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 {
147
123
  id: string;
148
124
  type: "message";
149
125
  worker: string;
150
126
  spawnId: string;
151
- subject: string;
152
127
  body: string;
153
128
  status?: "in_progress" | "completed" | "failed";
154
129
  }
155
130
 
156
- 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
+ }
157
144
 
158
- // ── 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
+ }
159
153
 
160
- const NodeAccess = Type.Union(
161
- [Type.Literal("read"), Type.Literal("write")],
162
- { description: "read permits overlapping analysis; write protects overlapping shared-workspace changes. Default: read." },
163
- );
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
+ }
164
162
 
165
- /** One task inside teammate_run. */
166
- export const RunTaskSpec = Type.Object({
167
- id: Type.String({ description: "Node id, unique within this run" }),
168
- agent: Type.String({ description: "Agent definition name (bundled, user, or project scope)" }),
169
- prompt: Type.String({ minLength: 1, description: "The specific task text handed to this worker" }),
170
- dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Node ids that must complete before this node starts" })),
171
- paths: Type.Array(Type.String({ description: "Repository-relative path this node may inspect or modify" }), {
172
- minItems: 1,
173
- description: "Repository-relative paths used to coordinate this node",
174
- }),
175
- access: Type.Optional(NodeAccess),
176
- model: Type.Optional(Type.String({ description: "Optional per-node provider/model pin" })),
177
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional per-node hard wall-clock cap before the worker is killed (default: 30 minutes; do not set overly short timeouts for multi-file or reasoning tasks)" })),
178
- });
163
+ // ── Tool parameter schemas (typebox) ──────────────────────────────
179
164
 
180
- /** Dispatch a dependency-aware task graph in one call. */
181
- export const TeammateRunParams = Type.Object({
182
- tasks: Type.Array(RunTaskSpec, {
183
- minItems: 1,
184
- maxItems: 32,
185
- description: "Task graph to dispatch; dependsOn edges must form a DAG",
186
- }),
187
- concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 32, description: "Max nodes running at once (default: 4)" })),
188
- worktree: Type.Optional(Type.Boolean({ description: "Run every node in its own git worktree (default: false)" })),
189
- 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." })),
190
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Run-level hard wall-clock cap; the run fails when exceeded (default: none, nodes have their own caps)" })),
191
- 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." })),
192
- summaryAgent: Type.Optional(Type.String({ description: "Agent used for the summary node when summarize is on (default: observer)" })),
193
- 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 (bundled, user, or project scope); 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" })),
194
170
  });
195
171
 
196
- /** Explicit cancel of a run or single node. */
197
- export const TeammateCancelParams = Type.Object({
198
- runId: Type.String({ description: "Run id to cancel" }),
199
- 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" }),
200
175
  });
201
176
 
202
- /** Retry the failed/cancelled nodes of a settled run. */
203
- export const TeammateRetryParams = Type.Object({
204
- runId: Type.String({ description: "Run id of a settled (failed/cancelled/completed) run" }),
205
- 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." })),
206
184
  });
207
185
 
208
- /** Worker progress message or final report to the team leader. */
209
- export const TeammateMessageParams = Type.Object({
210
- subject: Type.String({ description: "Concise report subject" }),
211
- 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" }),
212
197
  status: Type.Optional(Type.Union([
213
198
  Type.Literal("in_progress"),
214
199
  Type.Literal("completed"),
215
200
  Type.Literal("failed"),
216
- ], { description: "Optional worker status; use completed or failed for the final deliverable" })),
201
+ ], { description: 'Only for to="leader": completed or failed ends the current assignment and triggers immediate follow-up delivery' })),
202
+ });
203
+
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" })),
207
+ });
208
+
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" })),
217
214
  });
218
215
 
219
216
  // ── State snapshot for persistence ────────────────────────────────
220
217
 
221
- export interface TeammateState {
222
- runs: Record<string, Run>;
223
- /** 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. */
224
222
  leaderMailbox: MailboxMessage[];
225
223
  messageCounter: number;
226
- runCounter: number;
227
- /** 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. */
228
226
  workerEventOffsets: Record<string, number>;
229
- /** Event IDs already applied by the parent, keyed to their worker spawn for compaction. */
227
+ /** Report event ids already applied, keyed by teammate spawn. */
230
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[]>;
231
233
  }