pi-extended-teams 2.2.6 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/docs/access.md +2 -2
- package/extensions/agents/read-agent.ts +103 -3
- package/extensions/events/register-events.ts +15 -3
- package/extensions/index.ts +1 -0
- package/extensions/runtime/herdr.ts +13 -0
- package/extensions/runtime/types.ts +1 -0
- package/extensions/team/lifecycle.ts +6 -2
- package/extensions/tools/agent-status-tool.ts +15 -69
- package/extensions/tools/coordination-tools.ts +2 -0
- package/extensions/tools/task-runtime-tools.ts +7 -0
- package/extensions/tools/team-tools.ts +141 -152
- package/extensions/ui/agent-follow-view.ts +12 -3
- package/extensions/ui/read-agent-status.ts +2 -1
- package/package.json +2 -2
- package/skills/teams.md +2 -2
- package/src/orchestration/index.ts +21 -33
- package/src/orchestration/status-projection.ts +89 -0
- package/src/orchestration/types.ts +4 -1
- package/src/utils/models.ts +2 -1
|
@@ -6,6 +6,8 @@ import * as runtime from "../utils/runtime";
|
|
|
6
6
|
import * as writeQueue from "../utils/write-queue";
|
|
7
7
|
import * as reports from "../utils/report-events";
|
|
8
8
|
import type { Member } from "../utils/models";
|
|
9
|
+
import { readLifecycleTombstone } from "../utils/lifecycle-tombstone";
|
|
10
|
+
import { isAgentActivity, projectAgentStatus } from "./status-projection";
|
|
9
11
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel, roleForFavoriteModelSlot } from "../utils/settings";
|
|
10
12
|
import type {
|
|
11
13
|
BroadcastMessageOnceRequest,
|
|
@@ -19,7 +21,6 @@ import type {
|
|
|
19
21
|
SpawnTeammatesOnceResponse,
|
|
20
22
|
TeamObservation,
|
|
21
23
|
TeammateResolutionDetails,
|
|
22
|
-
TeammateHealth,
|
|
23
24
|
TeammateObservation,
|
|
24
25
|
} from "./types";
|
|
25
26
|
|
|
@@ -41,11 +42,6 @@ export async function appendTeamReportEvent(teamName: string, event: reports.New
|
|
|
41
42
|
return await next;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function readAgentIsKnownRunning(teamName: string, agentName: string, options: ObserveRuntimeOptions): boolean {
|
|
45
|
-
const key = options.readAgentKey?.(teamName, agentName) || `${teamName}:${agentName}`;
|
|
46
|
-
return !!options.runningReadAgents?.has(key);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
45
|
function operationValue(source: { operationId?: string; workflowRunId?: string; metadata?: Record<string, any> }, key: "operationId" | "workflowRunId"): string | undefined {
|
|
50
46
|
return source[key] || source.metadata?.[key] || source.metadata?.orchestration?.[key];
|
|
51
47
|
}
|
|
@@ -268,40 +264,32 @@ async function observeKnownTeammate(
|
|
|
268
264
|
const unreadCount = (await messaging.peekInbox(teamName, member.name, true).catch(() => [])).length;
|
|
269
265
|
const runtimeStatus = member.name === "team-lead" ? null : await runtime.readRuntimeStatus(teamName, member.name).catch(() => null);
|
|
270
266
|
const now = options.now ?? Date.now();
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const startupStalled = alive === true && unreadCount > 0 && (now - member.joinedAt) > runtime.STARTUP_STALL_MS && !(runtimeStatus?.ready);
|
|
285
|
-
let health: TeammateHealth;
|
|
286
|
-
if (member.name === "team-lead") health = "lead";
|
|
287
|
-
else if (alive === null) health = "unknown";
|
|
288
|
-
else if (!alive) health = "dead";
|
|
289
|
-
else if (startupStalled) health = "stalled";
|
|
290
|
-
else if (runtimeStatus?.ready) health = hasRecentHeartbeat ? "healthy" : "idle";
|
|
291
|
-
else health = "starting";
|
|
267
|
+
const key = options.readAgentKey?.(teamName, member.name) || `${teamName}:${member.name}`;
|
|
268
|
+
const candidate = options.runningReadAgents?.get(key);
|
|
269
|
+
const fence = member.name === "team-lead" ? { status: "absent" as const }
|
|
270
|
+
: await readLifecycleTombstone(teamName, member.name).catch(error => ({
|
|
271
|
+
status: "corrupt" as const, error: error instanceof Error ? error.message : String(error),
|
|
272
|
+
}));
|
|
273
|
+
const projected = projectAgentStatus({
|
|
274
|
+
member, activity: isAgentActivity(candidate) ? candidate : undefined,
|
|
275
|
+
runtime: runtimeStatus, fence, now, unreadCount,
|
|
276
|
+
terminalAlive: member.tmuxPaneId && options.terminal?.isAlive ? options.terminal.isAlive(member.tmuxPaneId) : null,
|
|
277
|
+
});
|
|
292
278
|
|
|
293
279
|
return {
|
|
294
280
|
teamName,
|
|
295
281
|
agentName: member.name,
|
|
296
282
|
member,
|
|
297
283
|
role,
|
|
298
|
-
alive,
|
|
299
|
-
health,
|
|
284
|
+
alive: member.name === "team-lead" ? true : projected.alive,
|
|
285
|
+
health: member.name === "team-lead" ? "lead" : projected.health,
|
|
286
|
+
phase: member.name === "team-lead" ? "lead" : projected.phase,
|
|
287
|
+
error: projected.error,
|
|
300
288
|
unreadCount,
|
|
301
|
-
agentLoopReady:
|
|
302
|
-
hasRecentHeartbeat,
|
|
303
|
-
startupStalled,
|
|
304
|
-
runtime:
|
|
289
|
+
agentLoopReady: projected.agentLoopReady,
|
|
290
|
+
hasRecentHeartbeat: projected.hasRecentHeartbeat,
|
|
291
|
+
startupStalled: projected.startupStalled,
|
|
292
|
+
runtime: projected.runtime,
|
|
305
293
|
};
|
|
306
294
|
}
|
|
307
295
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Member } from "../utils/models";
|
|
2
|
+
import type { LifecycleTombstoneReadResult } from "../utils/lifecycle-tombstone";
|
|
3
|
+
import { isHeartbeatFresh, STARTUP_STALL_MS, type AgentRuntimeStatus } from "../utils/runtime";
|
|
4
|
+
|
|
5
|
+
export const AGENT_HANGING_MS = 15 * 60_000;
|
|
6
|
+
|
|
7
|
+
export type ActiveAgentPhase = "starting" | "thinking" | "working" | "finishing"
|
|
8
|
+
| "stalled" | "stopping" | "quarantined" | "persistence-failed";
|
|
9
|
+
|
|
10
|
+
export interface AgentActivity {
|
|
11
|
+
runId: string;
|
|
12
|
+
status: "starting" | "thinking" | "working" | "finishing";
|
|
13
|
+
startedAt: number;
|
|
14
|
+
role?: string;
|
|
15
|
+
lastActivityAt?: number;
|
|
16
|
+
teardownState?: string;
|
|
17
|
+
handoffDetached?: boolean;
|
|
18
|
+
latestProgress?: string;
|
|
19
|
+
progressUpdatedAt?: number;
|
|
20
|
+
activeToolName?: string;
|
|
21
|
+
lastError?: { message: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isAgentActivity(value: unknown): value is AgentActivity {
|
|
25
|
+
return typeof value === "object" && value !== null
|
|
26
|
+
&& "runId" in value && typeof value.runId === "string"
|
|
27
|
+
&& "startedAt" in value && typeof value.startedAt === "number"
|
|
28
|
+
&& "status" in value && typeof value.status === "string"
|
|
29
|
+
&& ["starting", "thinking", "working", "finishing"].includes(value.status);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lifecyclePhase(member: Member, fence: LifecycleTombstoneReadResult): { phase: ActiveAgentPhase; error?: string } | undefined {
|
|
33
|
+
if (fence.status === "absent") return;
|
|
34
|
+
if (fence.status === "corrupt") return { phase: "quarantined", error: fence.error };
|
|
35
|
+
const { tombstone } = fence;
|
|
36
|
+
const mismatch = member.lifecycleRunId && tombstone.runId !== member.lifecycleRunId
|
|
37
|
+
? `Lifecycle fence belongs to run ${tombstone.runId}, not roster run ${member.lifecycleRunId}.`
|
|
38
|
+
: undefined;
|
|
39
|
+
return {
|
|
40
|
+
phase: tombstone.phase === "cleanup_failed" || tombstone.phase === "timed_out" ? "quarantined" : "stopping",
|
|
41
|
+
error: mismatch || tombstone.error,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function projectAgentStatus(input: {
|
|
46
|
+
member: Member;
|
|
47
|
+
activity?: AgentActivity;
|
|
48
|
+
runtime: AgentRuntimeStatus | null;
|
|
49
|
+
fence: LifecycleTombstoneReadResult;
|
|
50
|
+
terminalAlive: boolean | null;
|
|
51
|
+
now: number;
|
|
52
|
+
unreadCount?: number;
|
|
53
|
+
}) {
|
|
54
|
+
const { member, now } = input;
|
|
55
|
+
const state = input.activity?.runId === member.lifecycleRunId ? input.activity : undefined;
|
|
56
|
+
const runtime = input.runtime?.lifecycleRunId === member.lifecycleRunId ? input.runtime : null;
|
|
57
|
+
const persisted = lifecyclePhase(member, input.fence);
|
|
58
|
+
const hasRecentHeartbeat = isHeartbeatFresh(runtime, now);
|
|
59
|
+
const inProcess = !!state && state.teardownState !== "finalized" && !state.handoffDetached;
|
|
60
|
+
const externalAlive = member.isActive !== false && (hasRecentHeartbeat || input.terminalAlive === true);
|
|
61
|
+
const alive = inProcess || externalAlive ? true
|
|
62
|
+
: member.isActive === false || input.terminalAlive === false || member.role === "read" ? false : null;
|
|
63
|
+
const startupStalled = alive === true && (input.unreadCount ?? 0) > 0
|
|
64
|
+
&& now - member.joinedAt > STARTUP_STALL_MS && !runtime?.ready;
|
|
65
|
+
const teardownPhase = state?.teardownState === "persistence_failed" ? "persistence-failed"
|
|
66
|
+
: state?.teardownState === "quarantined" ? "quarantined"
|
|
67
|
+
: state?.teardownState === "stopping" ? "stopping" : undefined;
|
|
68
|
+
const fencedPhase = teardownPhase ?? persisted?.phase;
|
|
69
|
+
let phase: ActiveAgentPhase;
|
|
70
|
+
if (fencedPhase) phase = fencedPhase;
|
|
71
|
+
else if (inProcess) phase = now - (state.lastActivityAt || state.startedAt) >= AGENT_HANGING_MS ? "stalled" : state.status;
|
|
72
|
+
else if (!externalAlive) phase = member.isActive !== false && !runtime?.ready && now - member.joinedAt <= STARTUP_STALL_MS ? "starting" : "stalled";
|
|
73
|
+
else phase = runtime?.currentAction === "done" ? "finishing" : runtime?.currentAction ?? (runtime?.ready ? "working" : "starting");
|
|
74
|
+
|
|
75
|
+
const health = fencedPhase === "quarantined" ? "quarantined"
|
|
76
|
+
: fencedPhase === "persistence-failed" ? "persistence-failed"
|
|
77
|
+
: fencedPhase ? "stopping"
|
|
78
|
+
: startupStalled || (phase === "stalled" && alive) ? "stalled"
|
|
79
|
+
: alive === null ? "unknown"
|
|
80
|
+
: !alive ? "dead"
|
|
81
|
+
: runtime?.ready ? hasRecentHeartbeat ? "healthy" : "idle"
|
|
82
|
+
: inProcess && state.status !== "starting" ? "healthy" : "starting";
|
|
83
|
+
return {
|
|
84
|
+
phase, alive, health, hasRecentHeartbeat, startupStalled,
|
|
85
|
+
agentLoopReady: !fencedPhase && alive === true && (!!runtime?.ready || (inProcess && state.status !== "starting")),
|
|
86
|
+
state, runtime,
|
|
87
|
+
error: persisted?.error || state?.lastError?.message || runtime?.lastError?.message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentRuntimeStatus } from "../utils/runtime";
|
|
2
|
+
import type { ActiveAgentPhase, projectAgentStatus } from "./status-projection";
|
|
2
3
|
import type { QueuedWriteSpawn } from "../utils/write-queue";
|
|
3
4
|
import type { FileClaim } from "../utils/claims";
|
|
4
5
|
import type { InboxMessage, Member, TaskFile, TeamConfig, TeamReportEvent, ThinkingLevel } from "../utils/models";
|
|
@@ -12,7 +13,7 @@ export interface OrchestrationOperationMetadata {
|
|
|
12
13
|
metadata?: Record<string, any>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export type TeammateHealth = "lead" |
|
|
16
|
+
export type TeammateHealth = "lead" | ReturnType<typeof projectAgentStatus>["health"];
|
|
16
17
|
|
|
17
18
|
export interface TeammateObservation {
|
|
18
19
|
teamName: string;
|
|
@@ -21,6 +22,8 @@ export interface TeammateObservation {
|
|
|
21
22
|
role: string;
|
|
22
23
|
alive: boolean | null;
|
|
23
24
|
health: TeammateHealth;
|
|
25
|
+
phase?: ActiveAgentPhase | "lead";
|
|
26
|
+
error?: string;
|
|
24
27
|
unreadCount: number;
|
|
25
28
|
agentLoopReady: boolean;
|
|
26
29
|
hasRecentHeartbeat: boolean;
|
package/src/utils/models.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Member {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
joinedAt: number;
|
|
12
12
|
tmuxPaneId: string;
|
|
13
|
+
herdrPaneId?: string;
|
|
13
14
|
windowId?: string;
|
|
14
15
|
cwd: string;
|
|
15
16
|
subscriptions: any[];
|
|
@@ -33,7 +34,7 @@ export interface Member {
|
|
|
33
34
|
parentAgentName?: string;
|
|
34
35
|
/** Runtime-owned parent lifecycle identity for a restricted nested read child. */
|
|
35
36
|
parentLifecycleRunId?: string;
|
|
36
|
-
/**
|
|
37
|
+
/** Tool role; execution may be in-process or in a terminal pane. */
|
|
37
38
|
role?: "read" | "write";
|
|
38
39
|
/** Optional category preset name from settings.json. */
|
|
39
40
|
category?: string;
|