pi-extensible-workflows 3.3.0 → 3.4.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/README.md +2 -2
- package/dist/src/agent-execution.d.ts +63 -9
- package/dist/src/agent-execution.js +250 -92
- package/dist/src/cli.js +11 -2
- package/dist/src/doctor-cleanup.js +66 -31
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +5 -5
- package/dist/src/host.js +160 -76
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/persistence.d.ts +4 -8
- package/dist/src/persistence.js +3 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.js +14 -22
- package/dist/src/types.d.ts +123 -60
- package/dist/src/validation.js +20 -5
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +213 -90
- package/src/cli.ts +14 -3
- package/src/doctor-cleanup.ts +36 -3
- package/src/execution.ts +27 -15
- package/src/host.ts +136 -62
- package/src/index.ts +3 -3
- package/src/persistence.ts +5 -3
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +13 -22
- package/src/types.ts +45 -27
- package/src/validation.ts +13 -4
- package/src/workflow-artifacts.ts +1 -0
package/src/cli.ts
CHANGED
|
@@ -323,6 +323,8 @@ class CliProgress {
|
|
|
323
323
|
#frame = 0;
|
|
324
324
|
#run: PersistedRun | undefined;
|
|
325
325
|
#runId: string | undefined;
|
|
326
|
+
#runtimeStartedAt = 0;
|
|
327
|
+
#runtimeBaseMs = 0;
|
|
326
328
|
#timer: ReturnType<typeof setInterval> | undefined;
|
|
327
329
|
#interactive: boolean;
|
|
328
330
|
#styles: WorkflowProgressStyles;
|
|
@@ -331,7 +333,15 @@ class CliProgress {
|
|
|
331
333
|
this.#styles = terminalProgressStyles(this.#interactive);
|
|
332
334
|
}
|
|
333
335
|
update(run: PersistedRun): void {
|
|
334
|
-
if (this.#runId !== run.id) {
|
|
336
|
+
if (this.#runId !== run.id) {
|
|
337
|
+
this.#runId = run.id;
|
|
338
|
+
this.onRunId(run.id);
|
|
339
|
+
this.#runtimeStartedAt = Date.now();
|
|
340
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
341
|
+
} else if (this.#run && this.#run.state !== "running" && run.state === "running") {
|
|
342
|
+
this.#runtimeStartedAt = Date.now();
|
|
343
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
344
|
+
}
|
|
335
345
|
this.#run = run;
|
|
336
346
|
if (!this.#interactive) {
|
|
337
347
|
this.#timer ??= setInterval(() => { this.render(); }, 1000);
|
|
@@ -345,14 +355,15 @@ class CliProgress {
|
|
|
345
355
|
}
|
|
346
356
|
render(): void {
|
|
347
357
|
if (!this.#run) return;
|
|
358
|
+
const run = this.#run.state !== "running" ? this.#run : { ...this.#run, usage: { ...(this.#run.usage ?? { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 }), durationMs: Math.max(this.#run.usage?.durationMs ?? 0, this.#runtimeBaseMs + Date.now() - this.#runtimeStartedAt) } };
|
|
348
359
|
if (!this.#interactive) {
|
|
349
|
-
const stable = formatWorkflowProgress(
|
|
360
|
+
const stable = formatWorkflowProgress(run, "◇", this.#styles);
|
|
350
361
|
if (stable !== this.#lastStable) { this.#lastStable = stable; this.stderr(`${stable}\n`); }
|
|
351
362
|
return;
|
|
352
363
|
}
|
|
353
364
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
354
365
|
const width = process.stderr.columns || 80;
|
|
355
|
-
const text = truncateWorkflowProgress(formatWorkflowProgress(
|
|
366
|
+
const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
|
|
356
367
|
this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
|
|
357
368
|
this.#lines = text.split("\n").length;
|
|
358
369
|
}
|
package/src/doctor-cleanup.ts
CHANGED
|
@@ -46,12 +46,43 @@ function accounting(value: unknown, label: string): void { if (!object(value)) t
|
|
|
46
46
|
function resourceExclusions(value: unknown, label: string): void { if (value === undefined) return; if (!object(value)) throw new Error(`${label} is invalid`); if (value.skills !== undefined) stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined) stringList(value.extensions, `${label}.extensions`); }
|
|
47
47
|
function agentDefinition(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined) stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
|
|
48
48
|
function validateScheduledOptions(value: unknown, label: string): void { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); optionalString(value.role, `${label}.role`); if (value.schema !== undefined) validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined) nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null) positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions))) throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) { if (!object(value.agentIdentity) || !Array.isArray(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string") throw new Error(`${label}.agentIdentity is invalid`); positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`); optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`); optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`); } }
|
|
49
|
-
function
|
|
49
|
+
function validateSessionReference(value: unknown, label: string): void {
|
|
50
|
+
if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator))) throw new Error(`${label} is invalid`);
|
|
51
|
+
}
|
|
52
|
+
function validateAgentSetup(value: unknown, label: string): void {
|
|
53
|
+
if (!object(value)) throw new Error(`${label} is invalid`);
|
|
54
|
+
stringList(value.hookNames, `${label}.hookNames`);
|
|
55
|
+
model(value.model, `${label}.model`);
|
|
56
|
+
stringList(value.tools, `${label}.tools`);
|
|
57
|
+
if (typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label}.cwd is invalid`);
|
|
58
|
+
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
59
|
+
}
|
|
60
|
+
function validateAgent(value: unknown, label: string): void {
|
|
61
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || typeof value.state !== "string" || !AGENT_STATES.has(value.state)) throw new Error(`${label} is invalid`);
|
|
62
|
+
optionalString(value.systemPrompt, `${label}.systemPrompt`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.label, `${label}.label`); optionalString(value.parentId, `${label}.parentId`);
|
|
63
|
+
if (value.structuralPath !== undefined) stringList(value.structuralPath, `${label}.structuralPath`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.role, `${label}.role`); optionalString(value.requestedModel, `${label}.requestedModel`);
|
|
64
|
+
model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`);
|
|
65
|
+
if (value.attemptDetails !== undefined) {
|
|
66
|
+
if (!Array.isArray(value.attemptDetails)) throw new Error(`${label}.attemptDetails is invalid`);
|
|
67
|
+
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
68
|
+
const at = `${label}.attemptDetails[${String(index)}]`;
|
|
69
|
+
if (!object(attempt) || !Number.isSafeInteger(attempt.attempt) || Number(attempt.attempt) < 1 || typeof attempt.transport !== "string" || !attempt.transport || Object.hasOwn(attempt, "sessionId") || Object.hasOwn(attempt, "sessionFile")) throw new Error(`${at} is invalid`);
|
|
70
|
+
validateAgentSetup(attempt.setup, `${at}.setup`);
|
|
71
|
+
if (attempt.session !== undefined) { validateSessionReference(attempt.session, `${at}.session`); if ((attempt.session as Record<string, unknown>).transport !== attempt.transport) throw new Error(`${at}.session transport does not match attempt transport`); }
|
|
72
|
+
accounting(attempt.accounting, `${at}.accounting`);
|
|
73
|
+
if (attempt.error !== undefined && (!object(attempt.error) || typeof attempt.error.code !== "string" || typeof attempt.error.message !== "string")) throw new Error(`${at}.error is invalid`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (value.accounting !== undefined) accounting(value.accounting, `${label}.accounting`);
|
|
77
|
+
if (value.toolCalls !== undefined) { if (!Array.isArray(value.toolCalls)) throw new Error(`${label}.toolCalls is invalid`); for (const [index, call] of value.toolCalls.entries()) if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !["running", "completed", "failed"].includes(call.state as string)) throw new Error(`${label}.toolCalls[${String(index)}] is invalid`); }
|
|
78
|
+
if (value.activity !== undefined) { if (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind as string) || typeof value.activity.text !== "string") throw new Error(`${label}.activity is invalid`); }
|
|
79
|
+
if (value.lastEventAt !== undefined) finiteNumber(value.lastEventAt, `${label}.lastEventAt`);
|
|
80
|
+
}
|
|
50
81
|
function validateUsage(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"]) finiteNumber(value[key], `${label}.${key}`); }
|
|
51
82
|
function validateBudgetEvents(value: unknown): void { if (value === undefined) return; if (!Array.isArray(value)) throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) { const label = `budgetEvents[${String(index)}]`; if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !Array.isArray(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined) throw new Error(`${label} is invalid`); validateUsage(event.usage, `${label}.usage`); validateBudget(event.limits); } }
|
|
52
83
|
function validateRunRecord(run: PersistedRun): void {
|
|
53
84
|
const value = run as unknown;
|
|
54
|
-
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.nativeSessions)) throw new Error("Persisted run state is invalid");
|
|
85
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions")) throw new Error("Persisted run state is invalid");
|
|
55
86
|
const agents = value.agents as Record<string, unknown>[];
|
|
56
87
|
agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
|
|
57
88
|
const agentIds = new Set<string>();
|
|
@@ -72,7 +103,8 @@ function validateRunRecord(run: PersistedRun): void {
|
|
|
72
103
|
parent = typeof parentAgent?.parentId === "string" ? parentAgent.parentId : undefined;
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
|
-
|
|
106
|
+
const sessions = value.agentSessions as unknown[];
|
|
107
|
+
for (const [index, session] of sessions.entries()) validateSessionReference(session, `agentSessions[${String(index)}]`);
|
|
76
108
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
77
109
|
optionalString(value.failedAt, "Persisted failed path");
|
|
78
110
|
if (value.retry !== undefined) {
|
|
@@ -146,6 +178,7 @@ async function validateRunArtifacts(store: RunStore, workflowScript: string, sta
|
|
|
146
178
|
if (!object(record) || typeof record.id !== "string" || !record.id || ownershipIds.has(record.id) || typeof record.label !== "string" || !record.label || typeof record.state !== "string" || !SCHEDULER_STATES.has(record.state)) throw new Error(`${label} is invalid`);
|
|
147
179
|
ownershipIds.add(record.id);
|
|
148
180
|
optionalString(record.parentId, `${label}.parentId`);
|
|
181
|
+
optionalString(record.prompt, `${label}.prompt`);
|
|
149
182
|
validateScheduledOptions(record.options, `${label}.options`);
|
|
150
183
|
}
|
|
151
184
|
for (const record of ownership) if (object(record) && record.parentId !== undefined && (typeof record.parentId !== "string" || !ownershipIds.has(record.parentId))) throw new Error("Persisted ownership parent is missing");
|
package/src/execution.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
7
7
|
import type { AgentAttempt } from "./agent-execution.js";
|
|
8
|
-
import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
|
|
8
|
+
import type { AgentIdentity, AgentAttemptSummary, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
|
|
9
9
|
import { WorkflowError } from "./types.js";
|
|
10
10
|
import { asWorkflowError, errorText, fail, isWorkflowAuthored, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
|
|
11
11
|
import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
|
|
@@ -13,6 +13,7 @@ import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validat
|
|
|
13
13
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
14
14
|
type WorkerErrorShape = { code?: string; message: string; authored?: boolean; failedAt?: string };
|
|
15
15
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
16
|
+
const HEARTBEAT_INTERVAL_MS = 1000;
|
|
16
17
|
|
|
17
18
|
const OUTCOME_ERRORS = new Set<string>(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
|
|
18
19
|
const WORK_RESULT_BRAND = "__workResult";
|
|
@@ -64,7 +65,7 @@ process.on("message", raw => {
|
|
|
64
65
|
if (message.ok) request.resolve(message.value);
|
|
65
66
|
else request.reject(workflowError(message.error));
|
|
66
67
|
});
|
|
67
|
-
const heartbeat = setInterval(() => send({ type: "heartbeat" }),
|
|
68
|
+
const heartbeat = setInterval(() => send({ type: "heartbeat" }), ${HEARTBEAT_INTERVAL_MS});
|
|
68
69
|
send({ type: "heartbeat" });
|
|
69
70
|
const BRAND = "${WORK_RESULT_BRAND}";
|
|
70
71
|
const workError = (code, message) => Object.assign(new Error(message), { code });
|
|
@@ -416,7 +417,16 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
416
417
|
const controller = new AbortController();
|
|
417
418
|
let settled = false;
|
|
418
419
|
let rejectResult: (error: WorkflowError) => void = () => undefined;
|
|
419
|
-
let
|
|
420
|
+
let lastHeartbeatAt = performance.now();
|
|
421
|
+
let lastWatchdogCheckAt = lastHeartbeatAt;
|
|
422
|
+
const watchdog = setInterval(() => {
|
|
423
|
+
const now = performance.now();
|
|
424
|
+
// A check delayed enough to bridge the normal heartbeat margin makes elapsed time unreliable.
|
|
425
|
+
const watchdogCheckWasDelayed = now - lastWatchdogCheckAt >= HEARTBEAT_TIMEOUT_MS - HEARTBEAT_INTERVAL_MS;
|
|
426
|
+
lastWatchdogCheckAt = now;
|
|
427
|
+
if (watchdogCheckWasDelayed) { lastHeartbeatAt = now; return; }
|
|
428
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_TIMEOUT_MS) stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat");
|
|
429
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
420
430
|
const result = new Promise<JsonValue>((resolve, reject) => {
|
|
421
431
|
rejectResult = reject;
|
|
422
432
|
child.on("message", (raw: unknown) => {
|
|
@@ -424,7 +434,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
424
434
|
if (typeof raw !== "string" || Buffer.byteLength(raw) > RPC_LIMIT_BYTES) fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
|
|
425
435
|
const message = JSON.parse(raw) as { type?: string; id?: number; method?: string; args?: JsonValue[]; ok?: boolean; value?: JsonValue; error?: WorkerErrorShape };
|
|
426
436
|
if (!jsonValue(message)) fail("RPC_LIMIT_EXCEEDED", "Worker RPC must contain JSON-compatible values");
|
|
427
|
-
if (message.type === "heartbeat") {
|
|
437
|
+
if (message.type === "heartbeat") { lastHeartbeatAt = performance.now(); return; }
|
|
428
438
|
if (message.type === "result") { encoded(message.value); finish(); resolve(message.value ?? null); return; }
|
|
429
439
|
if (message.type === "error") { finish(); reject(workflowErrorFromWorker(message.error ?? { code: "INTERNAL_ERROR", message: "Worker failed" })); return; }
|
|
430
440
|
if (message.type === "rpc" && message.id !== undefined) void handleRpc(message.id, message.method ?? "", message.args ?? []);
|
|
@@ -439,7 +449,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
439
449
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 1000).unref();
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
|
-
function finish() { settled = true;
|
|
452
|
+
function finish() { settled = true; clearInterval(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
|
|
443
453
|
function stop(code: WorkflowErrorCode, message: string) { if (settled) return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
|
|
444
454
|
function branded(result: Record<string, JsonValue>): JsonValue { return { ...result, [WORK_RESULT_BRAND]: true }; }
|
|
445
455
|
async function handleRpc(id: number, method: string, values: JsonValue[]) {
|
|
@@ -520,18 +530,19 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
520
530
|
if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
|
|
521
531
|
return { result, cancel };
|
|
522
532
|
}
|
|
523
|
-
function
|
|
524
|
-
return {
|
|
533
|
+
function attemptSummary(attempt: AgentAttempt, accounting = attempt.accounting): AgentAttemptSummary {
|
|
534
|
+
return { attempt: attempt.attempt, transport: attempt.transport, ...(attempt.session ? { session: attempt.session } : {}), ...(attempt.error ? { error: attempt.error } : {}), accounting, setup: attempt.setup };
|
|
525
535
|
}
|
|
526
|
-
|
|
527
|
-
export async function persistActiveAgentAttempt(store: RunStore, id: string, active: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">): Promise<void> {
|
|
536
|
+
export async function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void> {
|
|
528
537
|
await store.updateState((run) => {
|
|
529
538
|
const agent = run.agents.find((candidate) => candidate.id === id);
|
|
530
539
|
if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
531
540
|
const accounting = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
541
|
+
const detail = attemptSummary(active, accounting);
|
|
542
|
+
const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), detail];
|
|
543
|
+
const session = active.session;
|
|
544
|
+
const agentSessions = session && !run.agentSessions.some(({ transport, sessionId }) => transport === session.transport && sessionId === session.sessionId) ? [...run.agentSessions, session] : run.agentSessions;
|
|
545
|
+
return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), agentSessions };
|
|
535
546
|
});
|
|
536
547
|
}
|
|
537
548
|
|
|
@@ -540,9 +551,10 @@ export async function persistAgentAttempts(store: RunStore, id: string, attempts
|
|
|
540
551
|
const agent = run.agents.find((candidate) => candidate.id === id);
|
|
541
552
|
if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
542
553
|
const total = attempts.reduce((sum, attempt) => ({ input: sum.input + attempt.accounting.input, output: sum.output + attempt.accounting.output, cacheRead: sum.cacheRead + attempt.accounting.cacheRead, cacheWrite: sum.cacheWrite + attempt.accounting.cacheWrite, cost: sum.cost + attempt.accounting.cost }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
543
|
-
const attemptDetails = attempts.map((
|
|
544
|
-
const
|
|
545
|
-
|
|
554
|
+
const attemptDetails = attempts.map((attempt) => attemptSummary(attempt));
|
|
555
|
+
const sessions = attempts.map((attempt) => attempt.session).filter((session): session is WorkflowAgentSessionReference => session !== undefined);
|
|
556
|
+
const sessionKeys = new Set(sessions.map(({ transport, sessionId }) => `${transport}:${sessionId}`));
|
|
557
|
+
return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), agentSessions: [...run.agentSessions.filter(({ transport, sessionId }) => !sessionKeys.has(`${transport}:${sessionId}`)), ...sessions] };
|
|
546
558
|
});
|
|
547
559
|
}
|
|
548
560
|
|