Files
tutti/packages/agent/gui/shared/workspaceAgentTurnErrorProjection.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

125 lines
3.7 KiB
TypeScript

import type { AgentActivityTurn } from "@tutti-os/agent-activity-core";
import type {
WorkspaceAgentSessionDetailMessage,
WorkspaceAgentSessionDetailTurn
} from "./workspaceAgentSessionDetailViewModel";
/**
* Enriches Turns that the hydrated transcript has already projected, plus an
* errored latest Turn that has not emitted any transcript item yet.
*
* `sessionTurns` can describe the full session while `turns` contains only the
* current message window. Historical canonical lifecycle metadata therefore
* must not create transcript membership or alter transcript order. The exact
* latest Turn is the one exception because its canonical error is the current
* conversation result rather than paginated history.
*/
export function enrichProjectedTurnsWithCanonicalErrors({
turns,
latestTurnId,
sessionTurns,
provider,
agentSessionId
}: {
turns: Map<string, WorkspaceAgentSessionDetailTurn>;
latestTurnId: string | null;
sessionTurns: readonly AgentActivityTurn[];
provider: string;
agentSessionId: string;
}): void {
for (const canonicalTurn of sessionTurns) {
if (
canonicalTurn.outcome !== "failed" &&
canonicalTurn.outcome !== "interrupted"
) {
continue;
}
const detail = canonicalTurn.error?.message.trim() ?? "";
if (!detail) {
continue;
}
const turn =
turns.get(canonicalTurn.turnId) ??
(canonicalTurn.turnId === latestTurnId
? createProjectedTurn(turns, canonicalTurn.turnId)
: null);
if (!turn) {
continue;
}
const existingErrorMessage = turn.agentMessages.find(
(message) => message.visibleError
);
if (existingErrorMessage) {
const explicitDetail = canonicalTurn.error?.detail ?? "";
if (explicitDetail.trim() && existingErrorMessage.visibleError) {
existingErrorMessage.visibleError = {
...existingErrorMessage.visibleError,
detail: explicitDetail,
detailAvailable: true
};
}
continue;
}
const matchingMessage = turn.agentMessages.find(
(message) => message.body.trim() === detail
);
if (matchingMessage) {
matchingMessage.status = "failed";
matchingMessage.statusKind = "failed";
matchingMessage.visibleError = visibleErrorFromCanonicalTurn(
canonicalTurn,
provider
);
continue;
}
const message: WorkspaceAgentSessionDetailMessage = {
id: `turn-error:${agentSessionId}:${canonicalTurn.turnId}`,
body: detail,
status: "failed",
statusKind: "failed",
turnId: canonicalTurn.turnId,
occurredAtUnixMs:
canonicalTurn.settledAtUnixMs ?? canonicalTurn.updatedAtUnixMs,
visibleError: visibleErrorFromCanonicalTurn(canonicalTurn, provider)
};
turn.agentMessages.push(message);
turn.agentItems.push({ kind: "message", message });
}
}
function createProjectedTurn(
turns: Map<string, WorkspaceAgentSessionDetailTurn>,
turnId: string
): WorkspaceAgentSessionDetailTurn {
const turn: WorkspaceAgentSessionDetailTurn = {
id: turnId,
userMessage: null,
userMessages: [],
agentMessages: [],
toolCalls: [],
toolCallCount: 0,
hasFailedToolCall: false,
agentItems: []
};
turns.set(turnId, turn);
return turn;
}
function visibleErrorFromCanonicalTurn(
turn: AgentActivityTurn,
provider: string
): NonNullable<WorkspaceAgentSessionDetailMessage["visibleError"]> {
const explicitDetail = turn.error?.detail ?? "";
return {
code: turn.error?.code?.trim() || null,
phase: "turn",
provider: provider.trim() || null,
detail: explicitDetail || turn.error?.message.trim() || null,
...(explicitDetail.trim() ? { detailAvailable: true } : {}),
retryable: null
};
}