From 2e5fd9f2b45b1c61c9e205e0de6328f2f37e35c4 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Thu, 12 Mar 2026 04:29:50 +0900 Subject: [PATCH] fix: persist startRun and stopRunByError events for failed delegations When a delegate's subprocess failed to start (e.g., MCP connection failure, executable not found), no events were persisted because the state machine never started. The error was silently converted to a tool result text and returned to the parent expert. Now the catch block in DelegationExecutor.executeSingleDelegation() emits startRun + stopRunByError events via the parent's storeEvent and eventListener callbacks, ensuring failed delegations are recorded in the event store and visible in log viewers. Co-Authored-By: Claude Opus 4.6 --- .changeset/persist-failed-delegation.md | 5 ++ .../src/orchestration/delegation-executor.ts | 56 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 .changeset/persist-failed-delegation.md diff --git a/.changeset/persist-failed-delegation.md b/.changeset/persist-failed-delegation.md new file mode 100644 index 00000000..347f2a68 --- /dev/null +++ b/.changeset/persist-failed-delegation.md @@ -0,0 +1,5 @@ +--- +"@perstack/runtime": patch +--- + +Persist startRun and stopRunByError events for failed delegations diff --git a/packages/runtime/src/orchestration/delegation-executor.ts b/packages/runtime/src/orchestration/delegation-executor.ts index 318d112e..ad7d3fa1 100644 --- a/packages/runtime/src/orchestration/delegation-executor.ts +++ b/packages/runtime/src/orchestration/delegation-executor.ts @@ -13,7 +13,12 @@ import type { ToolResult, Usage, } from "@perstack/core" -import { resolveModelTier, resolveModelTierWithFallback } from "@perstack/core" +import { + resolveModelTier, + resolveModelTierWithFallback, + startRun, + stopRunByError, +} from "@perstack/core" /** Reference to the parent Expert that delegated */ type DelegatedBy = NonNullable @@ -247,12 +252,57 @@ export class DelegationExecutor { { ...parentOptions, returnOnDelegationComplete: true }, ) } catch (error) { - // Child run crashed (e.g., MCP connection failure) - return error to parent + // Child run crashed before state machine started (e.g., MCP connection failure). + // Emit startRun + stopRunByError events so the failure is persisted in the event store. + const errorMessage = error instanceof Error ? error.message : String(error) + const errorName = error instanceof Error ? error.name : "Error" + + const startRunEvent = startRun(delegateSetting, delegateCheckpoint, { + initialCheckpoint: delegateCheckpoint, + inputMessages: [], + model: delegateSetting.model ?? "", + }) + + const errorCheckpoint: Checkpoint = { + ...delegateCheckpoint, + status: "stoppedByError", + error: { + name: errorName, + message: errorMessage, + isRetryable: false, + }, + } + + const stopRunByErrorEvent = stopRunByError(delegateSetting, errorCheckpoint, { + checkpoint: errorCheckpoint, + step: { + stepNumber: 1, + newMessages: [], + usage: createEmptyUsage(), + startedAt: Date.now(), + }, + error: { + name: errorName, + message: errorMessage, + isRetryable: false, + }, + }) + + // Persist events via parent's callbacks + if (parentOptions?.storeEvent) { + await parentOptions.storeEvent(startRunEvent).catch(() => {}) + await parentOptions.storeEvent(stopRunByErrorEvent).catch(() => {}) + } + if (parentOptions?.eventListener) { + parentOptions.eventListener(startRunEvent) + parentOptions.eventListener(stopRunByErrorEvent) + } + return { toolCallId, toolName, expertKey: expert.key, - text: `Delegation to ${expert.key} failed: ${error instanceof Error ? error.message : String(error)}`, + text: `Delegation to ${expert.key} failed: ${errorMessage}`, stepNumber: parentContext.stepNumber, deltaUsage: createEmptyUsage(), }