pi-crew 0.11.11 → 0.11.13
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/CHANGELOG.md +102 -0
- package/dist/index.mjs +551 -519
- package/package.json +1 -1
- package/src/config/config-validation.ts +9 -0
- package/src/config/defaults.ts +7 -0
- package/src/config/env-vars.ts +24 -0
- package/src/config/types.ts +11 -0
- package/src/extension/registration/tool-registration.ts +7 -0
- package/src/extension/registration/verify-gate-tool.ts +263 -0
- package/src/extension/team-tool/run-deadline.ts +22 -1
- package/src/extension/team-tool/run.ts +38 -2
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +31 -3
- package/src/prompt/inbox-poll.ts +141 -3
- package/src/runtime/broker/crew-broker.ts +48 -2
- package/src/runtime/broker/protocol/lock-busy.ts +47 -1
- package/src/runtime/broker/protocol/request-parsers.ts +8 -1
- package/src/runtime/budget-enforcement.ts +36 -20
- package/src/runtime/child-pi/child-pi-constants.ts +24 -0
- package/src/runtime/child-pi/child-pi-kill.ts +59 -2
- package/src/runtime/child-pi/child-pi-rpc.ts +1021 -0
- package/src/runtime/child-pi/child-pi-steering.ts +10 -8
- package/src/runtime/child-pi/child-pi-streams.ts +69 -19
- package/src/runtime/child-pi/child-pi.ts +29 -30
- package/src/runtime/child-pi/owned-process.ts +434 -0
- package/src/runtime/child-pi/postmortem-registry.ts +134 -0
- package/src/runtime/classifier/ambient-noise-gate.ts +126 -0
- package/src/runtime/classifier/classifier-service.ts +277 -61
- package/src/runtime/dispatch-batch.ts +32 -0
- package/src/runtime/live-session/live-agent-control.ts +2 -0
- package/src/runtime/live-session/live-agent-manager.ts +72 -2
- package/src/runtime/live-session/live-session-runtime.ts +435 -40
- package/src/runtime/merge-loop.ts +63 -35
- package/src/runtime/recovery/crash-recovery.ts +141 -3
- package/src/runtime/recovery/retry-executor.ts +29 -0
- package/src/runtime/run-tracker.ts +28 -0
- package/src/runtime/stale-reconciler.ts +36 -1
- package/src/runtime/task-runner.ts +103 -0
- package/src/runtime/verification/classifier-pre-gate.ts +262 -0
- package/src/runtime/verification/verification-gates.ts +5 -1
- package/src/runtime/verification/verify-gate.ts +394 -0
- package/src/schema/config-schema.ts +13 -0
- package/src/state/contracts.ts +18 -0
- package/src/state/coordination/mailbox.ts +203 -55
- package/src/state/stores/sqlite-run-state.ts +378 -0
- package/src/state/stores/state-store.ts +134 -6
- package/src/ui/agents-jobs-browser.ts +154 -8
- package/src/ui/events-state-source.ts +358 -0
- package/src/ui/run-snapshot-cache.ts +87 -3
- package/src/ui/widget/widget-model.ts +13 -2
|
@@ -21,6 +21,8 @@ import type { TaskAttemptState, TeamRunManifest, TeamTaskState } from "../state/
|
|
|
21
21
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
22
22
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
23
23
|
import { isDelegateShadowTask } from "./broker/delegate/shadow-lifecycle.ts";
|
|
24
|
+
import { classifyAmbientNoiseGate } from "./classifier/ambient-noise-gate.ts";
|
|
25
|
+
import { resolveClassifierEnabled, resolveClassifierModel } from "./classifier/classifier-service.ts";
|
|
24
26
|
import { readCrewAgents, recordFromTask, saveCrewAgents } from "./crew-agent-records.ts";
|
|
25
27
|
import { appendDeadletter } from "./deadletter.ts";
|
|
26
28
|
import { classifyHeartbeat, DEFAULT_GRADIENT_THRESHOLDS } from "./heartbeat/heartbeat-gradient.ts";
|
|
@@ -764,6 +766,17 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
|
|
|
764
766
|
// so the counter accumulates across retry attempts × model fallbacks.
|
|
765
767
|
const policy = retryPolicyFromConfig(input.reliability);
|
|
766
768
|
const spawnBudget: SpawnBudget = { count: 0, max: policy.maxTotalSpawns ?? 0 };
|
|
769
|
+
// U6A (spec 2026-10-09 §U6 Phase A): ambient-noise vs real-failure
|
|
770
|
+
// classifier gate on the retry loop. Dormant unless
|
|
771
|
+
// runtime.classifierEnabled (default FALSE — same flag as child-executor's
|
|
772
|
+
// retry-triage; env PI_CREW_CLASSIFIER_ENABLED beats config). Disabled
|
|
773
|
+
// path passes NO hook at all: zero behavior change, zero registry touch.
|
|
774
|
+
// Every classifier failure mode (opencode unconfigured, classify throw,
|
|
775
|
+
// stopReason error) falls back inside classifyAmbientNoiseGate to
|
|
776
|
+
// proceed-with-retry — the gate can only SKIP a queued retry, never fail
|
|
777
|
+
// the task with a new error.
|
|
778
|
+
const noiseGateEnabled = resolveClassifierEnabled(input.runtimeConfig?.classifierEnabled);
|
|
779
|
+
const noiseGateModel = resolveClassifierModel(input.runtimeConfig?.classifierModel);
|
|
767
780
|
const baseInput = {
|
|
768
781
|
manifest: ctx.manifest,
|
|
769
782
|
tasks: ctx.tasks,
|
|
@@ -890,6 +903,25 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
|
|
|
890
903
|
policy,
|
|
891
904
|
{
|
|
892
905
|
signal: runController.signal,
|
|
906
|
+
// U6A: only consulted between attempts (never on attempt 1) and only
|
|
907
|
+
// when the gate is enabled — see noiseGateEnabled note above. The
|
|
908
|
+
// gate never rejects (soft-fail inside classifyAmbientNoiseGate and
|
|
909
|
+
// inside executeWithRetry's shouldRetry guard).
|
|
910
|
+
shouldRetry: noiseGateEnabled
|
|
911
|
+
? (attempt, error) =>
|
|
912
|
+
classifyAmbientNoiseGate({
|
|
913
|
+
enabled: true,
|
|
914
|
+
modelRegistry: input.modelRegistry,
|
|
915
|
+
classifierModel: noiseGateModel,
|
|
916
|
+
failureSummary: error.message,
|
|
917
|
+
attempt,
|
|
918
|
+
maxAttempts: policy.maxAttempts,
|
|
919
|
+
eventsPath: ctx.manifest.eventsPath,
|
|
920
|
+
runId: ctx.manifest.runId,
|
|
921
|
+
taskId: task.id,
|
|
922
|
+
metricRegistry: input.metricRegistry,
|
|
923
|
+
}).then((gate) => gate.proceedWithRetry)
|
|
924
|
+
: undefined,
|
|
893
925
|
attemptId: (attempt) => `${ctx.manifest.runId}:${task.id}:attempt-${attempt}`,
|
|
894
926
|
onAttemptFailed: (attempt, error, delayMs, info) => {
|
|
895
927
|
lastAttemptId = info.attemptId;
|
|
@@ -101,11 +101,13 @@ export async function applyLiveAgentControlRequest(input: {
|
|
|
101
101
|
await session.prompt?.(request.message ?? "Please continue with the follow-up request.", {
|
|
102
102
|
source: "api",
|
|
103
103
|
expandPromptTemplates: false,
|
|
104
|
+
streamingBehavior: "followUp",
|
|
104
105
|
});
|
|
105
106
|
else if (request.operation === "resume")
|
|
106
107
|
await session.prompt?.(request.message ?? "Please resume and report final status.", {
|
|
107
108
|
source: "api",
|
|
108
109
|
expandPromptTemplates: false,
|
|
110
|
+
streamingBehavior: "followUp",
|
|
109
111
|
});
|
|
110
112
|
else if (request.operation === "stop") await session.abort?.();
|
|
111
113
|
return true;
|
|
@@ -62,6 +62,14 @@ export interface LiveAgentHandle {
|
|
|
62
62
|
status: CrewAgentRecord["status"];
|
|
63
63
|
pendingSteers: string[];
|
|
64
64
|
pendingFollowUps: string[];
|
|
65
|
+
/**
|
|
66
|
+
* U4 dispose normalization: once the underlying session has been disposed
|
|
67
|
+
* (task finished / hung-tool escape / terminate), the handle is TERMINAL —
|
|
68
|
+
* steer/followUp/resume are rejected clearly instead of silently queueing
|
|
69
|
+
* messages into a dead session (the old asymmetric-dispose bug) or letting
|
|
70
|
+
* registerLiveAgent replay pending queues into it.
|
|
71
|
+
*/
|
|
72
|
+
terminated?: boolean;
|
|
65
73
|
/** Phase 7: Pending IRC messages for this agent. */
|
|
66
74
|
pendingMessages: IrcMessage[];
|
|
67
75
|
/** G1-G6: Real-time activity tracking (in-memory only). */
|
|
@@ -100,6 +108,11 @@ export function registerLiveAgent(
|
|
|
100
108
|
pendingSteers: existing?.pendingSteers ?? [],
|
|
101
109
|
pendingFollowUps: existing?.pendingFollowUps ?? [],
|
|
102
110
|
pendingMessages: existing?.pendingMessages ?? [],
|
|
111
|
+
// U4: a re-register with a FRESH session revives the agentId (e.g. the
|
|
112
|
+
// D4 planning→execute handoff: a mock/placeholder handle registered first,
|
|
113
|
+
// the real session later). Any pending steer/followUp queued against the
|
|
114
|
+
// earlier handle below is routed to the NEW session on replay.
|
|
115
|
+
terminated: false,
|
|
103
116
|
activity: existing?.activity ?? {
|
|
104
117
|
activeTools: new Map(),
|
|
105
118
|
toolUses: 0,
|
|
@@ -161,9 +174,13 @@ export function registerLiveAgent(
|
|
|
161
174
|
handle.pendingFollowUps.length = 0;
|
|
162
175
|
for (const message of pending)
|
|
163
176
|
void handle.session
|
|
177
|
+
// U4: streamingBehavior is MANDATORY on every prompt() — without it a
|
|
178
|
+
// prompt that lands while the session is already streaming throws
|
|
179
|
+
// "Agent is already processing" (pi agent-session.js:1548).
|
|
164
180
|
.prompt(message, {
|
|
165
181
|
source: "api",
|
|
166
182
|
expandPromptTemplates: false,
|
|
183
|
+
streamingBehavior: "followUp",
|
|
167
184
|
})
|
|
168
185
|
.catch((error) => logInternalError("live-agent-manager.prompt", error, `agentId=${handle.agentId}`));
|
|
169
186
|
}
|
|
@@ -217,10 +234,49 @@ async function killLiveSessionProcessIfAlive(handle: LiveAgentHandle): Promise<v
|
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
236
|
|
|
237
|
+
/**
|
|
238
|
+
* U4 dispose normalization: dispose the underlying session AND make the
|
|
239
|
+
* handle terminal — drain any queued pendingSteers/pendingFollowUps (they can
|
|
240
|
+
* never be delivered to a disposed session) and set `terminated` so later
|
|
241
|
+
* steer/followUp calls are rejected clearly instead of queueing into a dead
|
|
242
|
+
* session. The handle itself stays in the registry for status/health/UI.
|
|
243
|
+
*/
|
|
220
244
|
export function disposeLiveAgentSession(agentIdOrTaskId: string): void {
|
|
221
245
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
222
246
|
if (!handle) return;
|
|
247
|
+
markLiveAgentHandleTerminated(handle);
|
|
248
|
+
safeDisposeLiveSession(handle);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Drain pending queues and mark a handle terminal (idempotent). */
|
|
252
|
+
function markLiveAgentHandleTerminated(handle: LiveAgentHandle): void {
|
|
253
|
+
// Drain: queued messages must not replay into the disposed session later
|
|
254
|
+
// (registerLiveAgent preserves pending* across re-registrations).
|
|
255
|
+
handle.pendingSteers.length = 0;
|
|
256
|
+
handle.pendingFollowUps.length = 0;
|
|
257
|
+
handle.terminated = true;
|
|
258
|
+
handle.updatedAt = new Date().toISOString();
|
|
259
|
+
invalidateSortedLiveAgents();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* U4 hung-tool escape hatch — drop-reference.
|
|
264
|
+
*
|
|
265
|
+
* Synchronous last resort for a session whose prompt() will never settle
|
|
266
|
+
* (tool/provider holds the turn and ignores AbortSignal): terminate the
|
|
267
|
+
* handle and REMOVE it from the registry WITHOUT awaiting abort().
|
|
268
|
+
* terminateLiveAgent's `await session.abort()` would itself hang on such a
|
|
269
|
+
* session; here only synchronous cleanup runs (drain → mark → dispose →
|
|
270
|
+
* unregister). Any background abort that eventually settles is harmless.
|
|
271
|
+
*/
|
|
272
|
+
export function dropLiveAgentReference(agentIdOrTaskId: string): LiveAgentHandle | undefined {
|
|
273
|
+
const handle = getLiveAgent(agentIdOrTaskId);
|
|
274
|
+
if (!handle) return undefined;
|
|
275
|
+
markLiveAgentHandleTerminated(handle);
|
|
223
276
|
safeDisposeLiveSession(handle);
|
|
277
|
+
liveAgents.delete(handle.agentId);
|
|
278
|
+
invalidateSortedLiveAgents();
|
|
279
|
+
return handle;
|
|
224
280
|
}
|
|
225
281
|
|
|
226
282
|
export async function terminateLiveAgent(
|
|
@@ -254,6 +310,7 @@ export async function terminateLiveAgent(
|
|
|
254
310
|
try {
|
|
255
311
|
await handle.session.abort?.();
|
|
256
312
|
} finally {
|
|
313
|
+
markLiveAgentHandleTerminated(handle);
|
|
257
314
|
safeDisposeLiveSession(handle);
|
|
258
315
|
liveAgents.delete(handle.agentId); // Move AFTER abort completes to prevent race
|
|
259
316
|
invalidateSortedLiveAgents();
|
|
@@ -377,6 +434,10 @@ export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | nu
|
|
|
377
434
|
export async function steerLiveAgent(agentIdOrTaskId: string, message: string): Promise<LiveAgentHandle> {
|
|
378
435
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
379
436
|
if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
|
|
437
|
+
// U4 dispose normalization: explicit rejection — never silently queue into
|
|
438
|
+
// (or double-deliver via replay of) a disposed session.
|
|
439
|
+
if (handle.terminated)
|
|
440
|
+
throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); steer is no longer accepted.`);
|
|
380
441
|
if (typeof handle.session.steer !== "function") {
|
|
381
442
|
handle.pendingSteers.push(message);
|
|
382
443
|
return handle;
|
|
@@ -390,6 +451,8 @@ export async function steerLiveAgent(agentIdOrTaskId: string, message: string):
|
|
|
390
451
|
export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
|
|
391
452
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
392
453
|
if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
|
|
454
|
+
if (handle.terminated)
|
|
455
|
+
throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); follow-up is no longer accepted.`);
|
|
393
456
|
if (typeof handle.session.prompt !== "function") {
|
|
394
457
|
handle.pendingFollowUps.push(prompt);
|
|
395
458
|
return handle;
|
|
@@ -397,6 +460,8 @@ export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string)
|
|
|
397
460
|
await handle.session.prompt(prompt, {
|
|
398
461
|
source: "api",
|
|
399
462
|
expandPromptTemplates: false,
|
|
463
|
+
// U4: mandatory streamingBehavior — see registerLiveAgent replay note.
|
|
464
|
+
streamingBehavior: "followUp",
|
|
400
465
|
});
|
|
401
466
|
handle.updatedAt = new Date().toISOString();
|
|
402
467
|
invalidateSortedLiveAgents();
|
|
@@ -412,11 +477,15 @@ export async function stopLiveAgent(agentIdOrTaskId: string): Promise<LiveAgentH
|
|
|
412
477
|
export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
|
|
413
478
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
414
479
|
if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
|
|
480
|
+
if (handle.terminated)
|
|
481
|
+
throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); resume requires a fresh session.`);
|
|
415
482
|
if (typeof handle.session.prompt !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose prompt().`);
|
|
416
483
|
handle.status = "running";
|
|
417
484
|
await handle.session.prompt(prompt, {
|
|
418
485
|
source: "api",
|
|
419
486
|
expandPromptTemplates: false,
|
|
487
|
+
// U4: mandatory streamingBehavior — see registerLiveAgent replay note.
|
|
488
|
+
streamingBehavior: "followUp",
|
|
420
489
|
});
|
|
421
490
|
handle.status = "completed";
|
|
422
491
|
handle.updatedAt = new Date().toISOString();
|
|
@@ -514,7 +583,7 @@ export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void
|
|
|
514
583
|
if (typeof handle.session.prompt === "function") {
|
|
515
584
|
const ircPrompt = `[Message from ${message.from}] ${message.content}`;
|
|
516
585
|
void handle.session
|
|
517
|
-
.prompt(ircPrompt, { source: "api", expandPromptTemplates: false })
|
|
586
|
+
.prompt(ircPrompt, { source: "api", expandPromptTemplates: false, streamingBehavior: "followUp" })
|
|
518
587
|
.catch((error) => logInternalError("live-agent-manager.irc-deliver", error, `agentId=${handle.agentId}`));
|
|
519
588
|
}
|
|
520
589
|
}
|
|
@@ -559,6 +628,7 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
|
|
|
559
628
|
.prompt(ircPrompt, {
|
|
560
629
|
source: "api",
|
|
561
630
|
expandPromptTemplates: false,
|
|
631
|
+
streamingBehavior: "followUp",
|
|
562
632
|
})
|
|
563
633
|
.catch((error) => logInternalError("live-agent-manager.irc-broadcast", error, `agentId=${handle.agentId}`));
|
|
564
634
|
}
|
|
@@ -657,7 +727,7 @@ export async function respondAsBackground(
|
|
|
657
727
|
if (!delivered && typeof handle.session.prompt === "function") {
|
|
658
728
|
const promptText = `${deliveredTag}${awaitReply ? ` (reply correlation: ${corrId})` : ""}`;
|
|
659
729
|
void handle.session
|
|
660
|
-
.prompt(promptText, { source: "api", expandPromptTemplates: false })
|
|
730
|
+
.prompt(promptText, { source: "api", expandPromptTemplates: false, streamingBehavior: "followUp" })
|
|
661
731
|
.catch((error) => logInternalError("live-agent-manager.respondAsBackground", error, `agentId=${handle.agentId}`));
|
|
662
732
|
delivered = true;
|
|
663
733
|
}
|