pi-agent-replicate 0.0.0-stage → 1.4.1
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/AGENTS.md +27 -0
- package/CHANGELOG.md +152 -0
- package/CONTEXT.md +63 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/RELEASING.md +132 -0
- package/assets/banner.png +0 -0
- package/config.json.example +6 -0
- package/docs/README.md +23 -0
- package/docs/adr/0003-installable-role-packs.md +29 -0
- package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
- package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
- package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
- package/docs/configuration.md +14 -0
- package/docs/development.md +27 -0
- package/docs/public-subagent-tree-api.md +58 -0
- package/docs/retained-roles.md +71 -0
- package/docs/settled-lifecycle-contract.md +71 -0
- package/docs/worktree-subagents.md +58 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/index.d.ts +116 -0
- package/index.js +1064 -0
- package/index.ts +155 -0
- package/package.json +70 -4
- package/pi-extension/subagents/activity.js +575 -0
- package/pi-extension/subagents/activity.ts +980 -0
- package/pi-extension/subagents/completion.js +184 -0
- package/pi-extension/subagents/completion.ts +229 -0
- package/pi-extension/subagents/delivery-drain.js +15 -0
- package/pi-extension/subagents/delivery-drain.ts +20 -0
- package/pi-extension/subagents/delivery-files.js +86 -0
- package/pi-extension/subagents/delivery-files.ts +105 -0
- package/pi-extension/subagents/delivery-log.js +51 -0
- package/pi-extension/subagents/delivery-log.ts +51 -0
- package/pi-extension/subagents/delivery-payload.ts +55 -0
- package/pi-extension/subagents/delivery.ts +219 -0
- package/pi-extension/subagents/execution-owner.js +30 -0
- package/pi-extension/subagents/execution-owner.ts +38 -0
- package/pi-extension/subagents/herdr.js +645 -0
- package/pi-extension/subagents/herdr.ts +927 -0
- package/pi-extension/subagents/index.ts +3400 -0
- package/pi-extension/subagents/launch.js +774 -0
- package/pi-extension/subagents/launch.ts +1124 -0
- package/pi-extension/subagents/lifecycle.js +477 -0
- package/pi-extension/subagents/lifecycle.ts +582 -0
- package/pi-extension/subagents/lineage.js +806 -0
- package/pi-extension/subagents/lineage.ts +841 -0
- package/pi-extension/subagents/replica-background.js +32 -0
- package/pi-extension/subagents/replica-background.ts +38 -0
- package/pi-extension/subagents/replica-branch.ts +45 -0
- package/pi-extension/subagents/replica-directory-settings.ts +51 -0
- package/pi-extension/subagents/replica-history.js +22 -0
- package/pi-extension/subagents/replica-history.ts +21 -0
- package/pi-extension/subagents/replica-work.js +326 -0
- package/pi-extension/subagents/replica-work.ts +336 -0
- package/pi-extension/subagents/replica-worker.js +74 -0
- package/pi-extension/subagents/replica-worker.ts +71 -0
- package/pi-extension/subagents/replication.ts +99 -0
- package/pi-extension/subagents/result-delivery-queue.ts +33 -0
- package/pi-extension/subagents/role-catalog.js +123 -0
- package/pi-extension/subagents/role-catalog.ts +24 -0
- package/pi-extension/subagents/runtime-routing.js +154 -0
- package/pi-extension/subagents/runtime-routing.ts +239 -0
- package/pi-extension/subagents/session-binding.js +346 -0
- package/pi-extension/subagents/session-binding.ts +339 -0
- package/pi-extension/subagents/session-work.js +219 -0
- package/pi-extension/subagents/session-work.ts +167 -0
- package/pi-extension/subagents/session.js +318 -0
- package/pi-extension/subagents/session.ts +448 -0
- package/pi-extension/subagents/settled-contract.js +47 -0
- package/pi-extension/subagents/settled-contract.ts +175 -0
- package/pi-extension/subagents/source-state.js +180 -0
- package/pi-extension/subagents/source-state.ts +184 -0
- package/pi-extension/subagents/status.ts +493 -0
- package/pi-extension/subagents/subagent-done.js +805 -0
- package/pi-extension/subagents/subagent-done.ts +878 -0
- package/pi-extension/subagents/terminal.js +111 -0
- package/pi-extension/subagents/terminal.ts +208 -0
- package/pi-extension/subagents/watchdog.js +122 -0
- package/pi-extension/subagents/watchdog.ts +171 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import type { ActivityReadResult, SubagentActivityScope } from "./activity.ts";
|
|
2
|
+
import type { CompletionResult } from "./completion.ts";
|
|
3
|
+
import {
|
|
4
|
+
settledDeliveryKey,
|
|
5
|
+
type SettledDeliveryGate,
|
|
6
|
+
type SettledDeliveryIdentity,
|
|
7
|
+
} from "./settled-contract.ts";
|
|
8
|
+
|
|
9
|
+
export type HerdrAgentStatus =
|
|
10
|
+
| "idle"
|
|
11
|
+
| "working"
|
|
12
|
+
| "blocked"
|
|
13
|
+
| "done"
|
|
14
|
+
| "unknown";
|
|
15
|
+
|
|
16
|
+
export type PaneInspection =
|
|
17
|
+
| {
|
|
18
|
+
kind: "present";
|
|
19
|
+
agent?: string;
|
|
20
|
+
agentStatus: HerdrAgentStatus;
|
|
21
|
+
observedAt: number;
|
|
22
|
+
}
|
|
23
|
+
| { kind: "missing"; error?: string }
|
|
24
|
+
| { kind: "unavailable"; error?: string };
|
|
25
|
+
|
|
26
|
+
export type ProcessState =
|
|
27
|
+
| { kind: "starting"; startedAt: number }
|
|
28
|
+
| { kind: "running"; startedAt: number; confirmedAt: number }
|
|
29
|
+
| { kind: "finalizing"; startedAt: number; detectedAt: number; completion: CompletionResult }
|
|
30
|
+
| { kind: "completed"; startedAt: number; detectedAt: number; completedAt: number; completion: CompletionResult }
|
|
31
|
+
| { kind: "failed"; startedAt: number; detectedAt: number; completedAt: number; error: string; exitCode?: number };
|
|
32
|
+
|
|
33
|
+
export type ActivityDetail =
|
|
34
|
+
| { kind: "none"; observedAt: number }
|
|
35
|
+
| { kind: "scope"; scope: SubagentActivityScope; label?: string; since: number; observedAt: number; sequence: number };
|
|
36
|
+
|
|
37
|
+
export type TurnState =
|
|
38
|
+
| { kind: "unknown" }
|
|
39
|
+
| { kind: "starting"; observedAt: number }
|
|
40
|
+
| { kind: "active"; startedAt: number; source: "activity" | "herdr" | "fallback"; activity?: ActivityDetail }
|
|
41
|
+
| { kind: "blocked"; startedAt: number }
|
|
42
|
+
| { kind: "waiting"; startedAt: number }
|
|
43
|
+
| { kind: "interrupted"; requestedAt: number; previousActivitySequence: number | null };
|
|
44
|
+
|
|
45
|
+
export type ActivityHealth =
|
|
46
|
+
| { kind: "unseen" }
|
|
47
|
+
| { kind: "healthy"; observedAt: number }
|
|
48
|
+
| { kind: "problem"; reason: "missing" | "invalid" | "wrong-id"; since: number; error?: string };
|
|
49
|
+
|
|
50
|
+
export type PaneObservation =
|
|
51
|
+
| { kind: "unknown" }
|
|
52
|
+
| { kind: "present"; observedAt: number; agentStatus: HerdrAgentStatus }
|
|
53
|
+
| { kind: "read-error"; firstFailedAt: number; lastFailedAt: number; consecutiveFailures: number; error?: string }
|
|
54
|
+
| { kind: "missing"; detectedAt: number; error?: string };
|
|
55
|
+
|
|
56
|
+
export type CompletionDelivery = "pending" | "delivered" | "suppressed";
|
|
57
|
+
|
|
58
|
+
export interface SubagentLifecycle {
|
|
59
|
+
process: ProcessState;
|
|
60
|
+
turn: TurnState;
|
|
61
|
+
activityHealth: ActivityHealth;
|
|
62
|
+
/** Latest optional Pi detail, independent of Herdr coarse turn state. */
|
|
63
|
+
activityDetail: ActivityDetail | null;
|
|
64
|
+
pane: PaneObservation;
|
|
65
|
+
/** Durable across unavailable/missing observations. */
|
|
66
|
+
hasWorked: boolean;
|
|
67
|
+
lastActivitySequence: number | null;
|
|
68
|
+
delivery: CompletionDelivery;
|
|
69
|
+
/** Repeated settled-turn delivery state, independent from terminal delivery. */
|
|
70
|
+
settledDelivery?: SettledDeliveryGate;
|
|
71
|
+
/** Sequence of the settled boundary that consumed the latest interrupt intent. */
|
|
72
|
+
interruptSettledSequence?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LifecycleProjection {
|
|
76
|
+
kind: "starting" | "running" | "active" | "blocked" | "waiting" | "interrupted" | "stalled" | "finalizing" | "completed" | "failed";
|
|
77
|
+
label?: string;
|
|
78
|
+
runtimeEndedAt?: number;
|
|
79
|
+
stateDurationSince?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createLifecycle(startedAt: number): SubagentLifecycle {
|
|
83
|
+
return {
|
|
84
|
+
process: { kind: "starting", startedAt },
|
|
85
|
+
turn: { kind: "unknown" },
|
|
86
|
+
activityHealth: { kind: "unseen" },
|
|
87
|
+
activityDetail: null,
|
|
88
|
+
pane: { kind: "unknown" },
|
|
89
|
+
hasWorked: false,
|
|
90
|
+
lastActivitySequence: null,
|
|
91
|
+
delivery: "pending",
|
|
92
|
+
settledDelivery: {
|
|
93
|
+
lastActivitySequence: null,
|
|
94
|
+
delivered: new Set(),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isTerminal(process: ProcessState): boolean {
|
|
100
|
+
return process.kind === "completed" || process.kind === "failed";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function startedAt(process: ProcessState): number {
|
|
104
|
+
return process.startedAt;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function observePaneInspection(
|
|
108
|
+
lifecycle: SubagentLifecycle,
|
|
109
|
+
inspection: PaneInspection,
|
|
110
|
+
observedAt: number,
|
|
111
|
+
): SubagentLifecycle {
|
|
112
|
+
if (isTerminal(lifecycle.process)) return lifecycle;
|
|
113
|
+
if (lifecycle.process.kind === "finalizing") return lifecycle;
|
|
114
|
+
|
|
115
|
+
if (inspection.kind === "unavailable") {
|
|
116
|
+
const previous = lifecycle.pane.kind === "read-error" ? lifecycle.pane : null;
|
|
117
|
+
return {
|
|
118
|
+
...lifecycle,
|
|
119
|
+
pane: {
|
|
120
|
+
kind: "read-error",
|
|
121
|
+
firstFailedAt: previous?.firstFailedAt ?? observedAt,
|
|
122
|
+
lastFailedAt: observedAt,
|
|
123
|
+
consecutiveFailures: (previous?.consecutiveFailures ?? 0) + 1,
|
|
124
|
+
error: inspection.error,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (inspection.kind === "missing") {
|
|
130
|
+
return {
|
|
131
|
+
...lifecycle,
|
|
132
|
+
pane: { kind: "missing", detectedAt: observedAt, ...(inspection.error ? { error: inspection.error } : {}) },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const agentStatus = inspection.agentStatus;
|
|
137
|
+
const hasWorked =
|
|
138
|
+
lifecycle.hasWorked ||
|
|
139
|
+
agentStatus === "working" ||
|
|
140
|
+
agentStatus === "blocked" ||
|
|
141
|
+
agentStatus === "done";
|
|
142
|
+
|
|
143
|
+
const pane: PaneObservation = {
|
|
144
|
+
kind: "present",
|
|
145
|
+
observedAt,
|
|
146
|
+
agentStatus,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const process: ProcessState = lifecycle.process.kind === "starting"
|
|
150
|
+
? { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt: observedAt }
|
|
151
|
+
: lifecycle.process;
|
|
152
|
+
|
|
153
|
+
// A local interrupt has higher precedence than coarse Herdr status. Herdr
|
|
154
|
+
// can lag behind Escape; only newer Pi activity or completion clears it.
|
|
155
|
+
if (lifecycle.turn.kind === "interrupted") {
|
|
156
|
+
return { ...lifecycle, process, pane, hasWorked };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let turn: TurnState = lifecycle.turn;
|
|
160
|
+
if (agentStatus === "blocked") {
|
|
161
|
+
turn = hasWorked
|
|
162
|
+
? {
|
|
163
|
+
kind: "blocked",
|
|
164
|
+
startedAt: lifecycle.turn.kind === "blocked" ? lifecycle.turn.startedAt : observedAt,
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
kind: "starting",
|
|
168
|
+
observedAt: lifecycle.turn.kind === "starting" ? lifecycle.turn.observedAt : observedAt,
|
|
169
|
+
};
|
|
170
|
+
} else if (agentStatus === "working") {
|
|
171
|
+
turn = {
|
|
172
|
+
kind: "active",
|
|
173
|
+
startedAt: lifecycle.turn.kind === "active" ? lifecycle.turn.startedAt : observedAt,
|
|
174
|
+
source: "herdr",
|
|
175
|
+
...(lifecycle.activityDetail ? { activity: lifecycle.activityDetail } : {}),
|
|
176
|
+
};
|
|
177
|
+
} else if (agentStatus === "done" || agentStatus === "idle") {
|
|
178
|
+
turn = hasWorked
|
|
179
|
+
? {
|
|
180
|
+
kind: "waiting",
|
|
181
|
+
startedAt: lifecycle.turn.kind === "waiting" ? lifecycle.turn.startedAt : observedAt,
|
|
182
|
+
}
|
|
183
|
+
: {
|
|
184
|
+
kind: "starting",
|
|
185
|
+
observedAt: lifecycle.turn.kind === "starting" ? lifecycle.turn.observedAt : observedAt,
|
|
186
|
+
};
|
|
187
|
+
} else if (agentStatus === "unknown") {
|
|
188
|
+
// Keep existing process/turn; only record observation.
|
|
189
|
+
return { ...lifecycle, process, pane };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...lifecycle,
|
|
194
|
+
process,
|
|
195
|
+
turn,
|
|
196
|
+
pane,
|
|
197
|
+
hasWorked,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function observeActivity(
|
|
202
|
+
lifecycle: SubagentLifecycle,
|
|
203
|
+
read: ActivityReadResult,
|
|
204
|
+
observedAt: number,
|
|
205
|
+
): SubagentLifecycle {
|
|
206
|
+
if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
|
|
207
|
+
|
|
208
|
+
const detail: ActivityDetail | null = (() => {
|
|
209
|
+
if (!read.ok) return null;
|
|
210
|
+
const activity = read.activity;
|
|
211
|
+
if (lifecycle.lastActivitySequence != null && activity.sequence < lifecycle.lastActivitySequence) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (activity.phase !== "active") return null;
|
|
215
|
+
if (activity.activeScope === "compaction") {
|
|
216
|
+
const label = activity.compactionReason
|
|
217
|
+
? `compacting (${activity.compactionReason})`
|
|
218
|
+
: "compacting";
|
|
219
|
+
return {
|
|
220
|
+
kind: "scope",
|
|
221
|
+
scope: "compaction",
|
|
222
|
+
since: activity.activeSince ?? activity.updatedAt,
|
|
223
|
+
observedAt: activity.updatedAt,
|
|
224
|
+
sequence: activity.sequence,
|
|
225
|
+
label,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (activity.activeScope === "tool") {
|
|
229
|
+
return {
|
|
230
|
+
kind: "scope",
|
|
231
|
+
scope: "tool",
|
|
232
|
+
since: activity.toolStartedAt ?? activity.activeSince ?? activity.updatedAt,
|
|
233
|
+
observedAt: activity.updatedAt,
|
|
234
|
+
sequence: activity.sequence,
|
|
235
|
+
...(activity.toolName ? { label: activity.toolName } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (activity.activeScope === "provider") {
|
|
239
|
+
return { kind: "scope", scope: "provider", since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence, label: "provider" };
|
|
240
|
+
}
|
|
241
|
+
if (activity.activeScope === "streaming") {
|
|
242
|
+
return { kind: "scope", scope: "streaming", since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence, label: "streaming" };
|
|
243
|
+
}
|
|
244
|
+
if (activity.activeScope === "agent" || activity.activeScope === "turn") {
|
|
245
|
+
return { kind: "scope", scope: activity.activeScope, since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence };
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
})();
|
|
249
|
+
|
|
250
|
+
if (!read.ok) {
|
|
251
|
+
const since = lifecycle.activityHealth.kind === "problem"
|
|
252
|
+
? lifecycle.activityHealth.since
|
|
253
|
+
: observedAt;
|
|
254
|
+
return {
|
|
255
|
+
...lifecycle,
|
|
256
|
+
activityHealth: { kind: "problem", reason: read.reason, since, ...(read.error ? { error: read.error } : {}) },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!detail) {
|
|
261
|
+
// Reading succeeded but no enrichable detail; clear any stale label.
|
|
262
|
+
return {
|
|
263
|
+
...lifecycle,
|
|
264
|
+
activityDetail: null,
|
|
265
|
+
activityHealth: { kind: "healthy", observedAt },
|
|
266
|
+
lastActivitySequence: Math.max(lifecycle.lastActivitySequence ?? -1, read.activity.sequence),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let resumesInterruptedTurn = false;
|
|
271
|
+
if (lifecycle.turn.kind === "interrupted") {
|
|
272
|
+
const staleInterruptSnapshot = detail.observedAt < lifecycle.turn.requestedAt ||
|
|
273
|
+
(detail.observedAt === lifecycle.turn.requestedAt &&
|
|
274
|
+
lifecycle.turn.previousActivitySequence != null &&
|
|
275
|
+
detail.sequence <= lifecycle.turn.previousActivitySequence);
|
|
276
|
+
if (staleInterruptSnapshot) return lifecycle;
|
|
277
|
+
resumesInterruptedTurn = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const process: ProcessState = lifecycle.process.kind === "starting"
|
|
281
|
+
? { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt: observedAt }
|
|
282
|
+
: lifecycle.process;
|
|
283
|
+
|
|
284
|
+
// Herdr owns coarse turn state. Pi detail may enrich an authoritative
|
|
285
|
+
// Herdr-working turn, or provide a fallback only while pane status is unknown.
|
|
286
|
+
let turn = lifecycle.turn;
|
|
287
|
+
const sameDetail = lifecycle.activityDetail?.kind === "scope" &&
|
|
288
|
+
lifecycle.activityDetail.scope === detail.scope &&
|
|
289
|
+
lifecycle.activityDetail.label === detail.label;
|
|
290
|
+
const detailStartedAt = sameDetail && lifecycle.turn.kind === "active"
|
|
291
|
+
? lifecycle.turn.startedAt
|
|
292
|
+
: detail.since;
|
|
293
|
+
|
|
294
|
+
if (resumesInterruptedTurn) {
|
|
295
|
+
turn = {
|
|
296
|
+
kind: "active",
|
|
297
|
+
startedAt: detailStartedAt,
|
|
298
|
+
source: "activity",
|
|
299
|
+
activity: detail,
|
|
300
|
+
};
|
|
301
|
+
} else if (lifecycle.turn.kind !== "interrupted") {
|
|
302
|
+
if (lifecycle.pane.kind === "present" && lifecycle.pane.agentStatus === "working") {
|
|
303
|
+
turn = {
|
|
304
|
+
kind: "active",
|
|
305
|
+
startedAt: detailStartedAt,
|
|
306
|
+
source: "activity",
|
|
307
|
+
activity: detail,
|
|
308
|
+
};
|
|
309
|
+
} else if (lifecycle.pane.kind === "unknown" || lifecycle.pane.kind === "read-error") {
|
|
310
|
+
turn = {
|
|
311
|
+
kind: "active",
|
|
312
|
+
startedAt: detailStartedAt,
|
|
313
|
+
source: "fallback",
|
|
314
|
+
activity: detail,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
...lifecycle,
|
|
321
|
+
process,
|
|
322
|
+
turn,
|
|
323
|
+
activityDetail: detail,
|
|
324
|
+
activityHealth: { kind: "healthy", observedAt },
|
|
325
|
+
lastActivitySequence: detail.sequence,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function markProcessRunning(
|
|
330
|
+
lifecycle: SubagentLifecycle,
|
|
331
|
+
confirmedAt: number,
|
|
332
|
+
): SubagentLifecycle {
|
|
333
|
+
if (lifecycle.process.kind !== "starting") return lifecycle;
|
|
334
|
+
return {
|
|
335
|
+
...lifecycle,
|
|
336
|
+
process: { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function markInterruptRequested(
|
|
341
|
+
lifecycle: SubagentLifecycle,
|
|
342
|
+
requestedAt: number,
|
|
343
|
+
): SubagentLifecycle {
|
|
344
|
+
if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
|
|
345
|
+
return {
|
|
346
|
+
...lifecycle,
|
|
347
|
+
turn: {
|
|
348
|
+
kind: "interrupted",
|
|
349
|
+
requestedAt,
|
|
350
|
+
previousActivitySequence: lifecycle.lastActivitySequence,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Consume one interrupt intent at its first eligible settled boundary. */
|
|
356
|
+
export function consumeInterruptBoundary(
|
|
357
|
+
lifecycle: SubagentLifecycle,
|
|
358
|
+
sequence: number,
|
|
359
|
+
): SubagentLifecycle {
|
|
360
|
+
if (lifecycle.turn.kind !== "interrupted") return lifecycle;
|
|
361
|
+
return {
|
|
362
|
+
...lifecycle,
|
|
363
|
+
interruptSettledSequence: sequence,
|
|
364
|
+
turn: { kind: "waiting", startedAt: lifecycle.turn.requestedAt },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function markCompletionDetected(
|
|
369
|
+
lifecycle: SubagentLifecycle,
|
|
370
|
+
completion: CompletionResult,
|
|
371
|
+
detectedAt: number,
|
|
372
|
+
): SubagentLifecycle {
|
|
373
|
+
if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
|
|
374
|
+
return {
|
|
375
|
+
...lifecycle,
|
|
376
|
+
process: {
|
|
377
|
+
kind: "finalizing",
|
|
378
|
+
startedAt: startedAt(lifecycle.process),
|
|
379
|
+
detectedAt: Math.max(startedAt(lifecycle.process), detectedAt),
|
|
380
|
+
completion,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function markCompleted(lifecycle: SubagentLifecycle, completedAt: number): SubagentLifecycle {
|
|
386
|
+
if (isTerminal(lifecycle.process)) return lifecycle;
|
|
387
|
+
if (lifecycle.process.kind !== "finalizing") return lifecycle;
|
|
388
|
+
return {
|
|
389
|
+
...lifecycle,
|
|
390
|
+
process: {
|
|
391
|
+
kind: "completed",
|
|
392
|
+
startedAt: lifecycle.process.startedAt,
|
|
393
|
+
detectedAt: lifecycle.process.detectedAt,
|
|
394
|
+
completedAt: Math.max(lifecycle.process.detectedAt, completedAt),
|
|
395
|
+
completion: lifecycle.process.completion,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function markFailed(
|
|
401
|
+
lifecycle: SubagentLifecycle,
|
|
402
|
+
error: string,
|
|
403
|
+
detectedAt: number,
|
|
404
|
+
exitCode?: number,
|
|
405
|
+
): SubagentLifecycle {
|
|
406
|
+
if (isTerminal(lifecycle.process)) return lifecycle;
|
|
407
|
+
const start = startedAt(lifecycle.process);
|
|
408
|
+
const detected = lifecycle.process.kind === "finalizing"
|
|
409
|
+
? lifecycle.process.detectedAt
|
|
410
|
+
: Math.max(start, detectedAt);
|
|
411
|
+
return {
|
|
412
|
+
...lifecycle,
|
|
413
|
+
process: {
|
|
414
|
+
kind: "failed",
|
|
415
|
+
startedAt: start,
|
|
416
|
+
detectedAt: detected,
|
|
417
|
+
completedAt: Math.max(detected, detectedAt),
|
|
418
|
+
error,
|
|
419
|
+
...(exitCode == null ? {} : { exitCode }),
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function markDelivery(lifecycle: SubagentLifecycle, delivery: CompletionDelivery): SubagentLifecycle {
|
|
425
|
+
if (lifecycle.delivery !== "pending") return lifecycle;
|
|
426
|
+
return { ...lifecycle, delivery };
|
|
427
|
+
}
|
|
428
|
+
function settledGate(lifecycle: SubagentLifecycle): SettledDeliveryGate {
|
|
429
|
+
return lifecycle.settledDelivery ?? { lastActivitySequence: null, delivered: new Set() };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Accept only a newer settled activity sequence for this child. */
|
|
433
|
+
export function observeSettledActivity(
|
|
434
|
+
lifecycle: SubagentLifecycle,
|
|
435
|
+
activitySequence: number,
|
|
436
|
+
): SubagentLifecycle {
|
|
437
|
+
const gate = settledGate(lifecycle);
|
|
438
|
+
if (
|
|
439
|
+
gate.lastActivitySequence != null &&
|
|
440
|
+
activitySequence <= gate.lastActivitySequence
|
|
441
|
+
) {
|
|
442
|
+
return lifecycle;
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
...lifecycle,
|
|
446
|
+
settledDelivery: { ...gate, lastActivitySequence: activitySequence },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Return whether terminal content was already delivered as a settled turn. */
|
|
451
|
+
export function hasSettledAssistant(
|
|
452
|
+
lifecycle: SubagentLifecycle,
|
|
453
|
+
identity: SettledDeliveryIdentity,
|
|
454
|
+
): boolean {
|
|
455
|
+
return settledGate(lifecycle).delivered.has(settledDeliveryKey(identity));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Record one settled assistant after its parent enqueue succeeds. */
|
|
459
|
+
export function markSettledAssistantDelivered(
|
|
460
|
+
lifecycle: SubagentLifecycle,
|
|
461
|
+
identity: SettledDeliveryIdentity,
|
|
462
|
+
activitySequence: number,
|
|
463
|
+
): SubagentLifecycle {
|
|
464
|
+
const gate = settledGate(lifecycle);
|
|
465
|
+
const key = settledDeliveryKey(identity);
|
|
466
|
+
if (gate.delivered.has(key)) return lifecycle;
|
|
467
|
+
if (
|
|
468
|
+
gate.lastActivitySequence != null &&
|
|
469
|
+
activitySequence < gate.lastActivitySequence
|
|
470
|
+
) {
|
|
471
|
+
return lifecycle;
|
|
472
|
+
}
|
|
473
|
+
const delivered = new Set(gate.delivered);
|
|
474
|
+
delivered.add(key);
|
|
475
|
+
return {
|
|
476
|
+
...lifecycle,
|
|
477
|
+
settledDelivery: {
|
|
478
|
+
lastActivitySequence:
|
|
479
|
+
gate.lastActivitySequence == null
|
|
480
|
+
? activitySequence
|
|
481
|
+
: Math.max(gate.lastActivitySequence, activitySequence),
|
|
482
|
+
delivered,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function projectLifecycle(lifecycle: SubagentLifecycle, now: number): LifecycleProjection {
|
|
488
|
+
const process = lifecycle.process;
|
|
489
|
+
if (process.kind === "finalizing") return { kind: "finalizing", runtimeEndedAt: process.detectedAt };
|
|
490
|
+
if (process.kind === "completed") {
|
|
491
|
+
return lifecycle.delivery === "delivered" || lifecycle.delivery === "suppressed"
|
|
492
|
+
? { kind: "completed", runtimeEndedAt: process.completedAt }
|
|
493
|
+
: { kind: "finalizing", label: "delivery-pending", runtimeEndedAt: process.completedAt };
|
|
494
|
+
}
|
|
495
|
+
if (process.kind === "failed") {
|
|
496
|
+
return lifecycle.delivery === "delivered" || lifecycle.delivery === "suppressed"
|
|
497
|
+
? { kind: "failed", label: process.error, runtimeEndedAt: process.completedAt }
|
|
498
|
+
: { kind: "finalizing", label: "delivery-pending", runtimeEndedAt: process.completedAt };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Pi activity is optional enrichment. Only authoritative Herdr inspection
|
|
502
|
+
// unavailability may produce a stalled projection.
|
|
503
|
+
if (
|
|
504
|
+
lifecycle.pane.kind === "read-error" &&
|
|
505
|
+
now - lifecycle.pane.firstFailedAt >= 60_000
|
|
506
|
+
) {
|
|
507
|
+
return { kind: "stalled", stateDurationSince: lifecycle.pane.firstFailedAt };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const turn = lifecycle.turn;
|
|
511
|
+
switch (turn.kind) {
|
|
512
|
+
case "interrupted":
|
|
513
|
+
return { kind: "interrupted", stateDurationSince: turn.requestedAt };
|
|
514
|
+
case "active": {
|
|
515
|
+
if (turn.activity?.kind === "scope") {
|
|
516
|
+
const label = turn.activity.label ?? turn.activity.scope;
|
|
517
|
+
return { kind: "active", label, stateDurationSince: turn.startedAt };
|
|
518
|
+
}
|
|
519
|
+
return { kind: "active", label: turn.source === "herdr" ? "agent working" : "agent active", stateDurationSince: turn.startedAt };
|
|
520
|
+
}
|
|
521
|
+
case "blocked":
|
|
522
|
+
return { kind: "blocked", stateDurationSince: turn.startedAt };
|
|
523
|
+
case "waiting":
|
|
524
|
+
return { kind: "waiting", stateDurationSince: turn.startedAt };
|
|
525
|
+
case "starting":
|
|
526
|
+
return { kind: "starting", stateDurationSince: turn.observedAt };
|
|
527
|
+
case "unknown":
|
|
528
|
+
return process.kind === "running" ? { kind: "running" } : { kind: "starting" };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export type LifecycleTransition = "stalled" | "recovered" | null;
|
|
533
|
+
|
|
534
|
+
export function lifecycleTransition(
|
|
535
|
+
previous: LifecycleProjection["kind"] | undefined,
|
|
536
|
+
next: LifecycleProjection["kind"],
|
|
537
|
+
): LifecycleTransition {
|
|
538
|
+
if (previous !== "stalled" && next === "stalled") return "stalled";
|
|
539
|
+
if (
|
|
540
|
+
previous === "stalled" &&
|
|
541
|
+
(next === "active" ||
|
|
542
|
+
next === "blocked" ||
|
|
543
|
+
next === "waiting" ||
|
|
544
|
+
next === "interrupted" ||
|
|
545
|
+
next === "running" ||
|
|
546
|
+
next === "starting")
|
|
547
|
+
) {
|
|
548
|
+
return "recovered";
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function formatLifecycleTransitionLine(
|
|
554
|
+
name: string,
|
|
555
|
+
projection: LifecycleProjection,
|
|
556
|
+
transition: Exclude<LifecycleTransition, null>,
|
|
557
|
+
now: number,
|
|
558
|
+
startedAt: number,
|
|
559
|
+
formatElapsed: (ms: number) => string,
|
|
560
|
+
): string {
|
|
561
|
+
const runtime = formatElapsed(Math.max(0, now - startedAt));
|
|
562
|
+
const duration = projection.stateDurationSince == null
|
|
563
|
+
? ""
|
|
564
|
+
: ` ${formatElapsed(now - projection.stateDurationSince)}`;
|
|
565
|
+
if (transition === "stalled") {
|
|
566
|
+
return `${name} running ${runtime}, stalled${duration}.`;
|
|
567
|
+
}
|
|
568
|
+
if (projection.kind === "waiting") {
|
|
569
|
+
return `${name} running ${runtime}, recovered; waiting${duration}.`;
|
|
570
|
+
}
|
|
571
|
+
if (projection.kind === "active") {
|
|
572
|
+
const detail = projection.label ? ` (${projection.label}${duration})` : duration;
|
|
573
|
+
return `${name} running ${runtime}, recovered; active${detail}.`;
|
|
574
|
+
}
|
|
575
|
+
if (projection.kind === "blocked") {
|
|
576
|
+
return `${name} running ${runtime}, recovered; blocked${duration}.`;
|
|
577
|
+
}
|
|
578
|
+
if (projection.kind === "interrupted") {
|
|
579
|
+
return `${name} running ${runtime}, recovered; interrupted${duration}.`;
|
|
580
|
+
}
|
|
581
|
+
return `${name} running ${runtime}, recovered; running.`;
|
|
582
|
+
}
|