fix: expose Workbench dispatch failures

This commit is contained in:
root
2026-07-20 14:50:06 +02:00
parent 867a4c6f58
commit f9c54ae7ea
11 changed files with 386 additions and 8 deletions
@@ -79,6 +79,7 @@ const RETRYABLE_AGENTRUN_TRANSPORT_FAILURE_KINDS = new Set([
"agentrun-timeout",
"timeout",
"fetch-failed",
"connection-refused",
"network-error"
]);
@@ -755,6 +756,7 @@ export function isRetryableProviderInterruptionPayload(payload = {}, options = {
export function normalizedFailureKind(value) {
const normalized = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-") || null;
if (normalized === "agentrun-unreachable") return "agentrun-connect-failed";
if (normalized === "connectionrefused" || normalized === "econnrefused") return "connection-refused";
return normalized;
}
+9
View File
@@ -33,6 +33,15 @@ export type WorkbenchActivityConfig = {
retryMaximumAttempts: number;
};
export interface WorkbenchEventPublisher {
publish(input: {
traceId: string;
sessionId: string;
event: Record<string, unknown>;
}): Promise<Record<string, unknown>>;
close?(): Promise<void>;
}
export interface WorkbenchApplication {
health(): Promise<Record<string, unknown>>;
createSession(input: { actor: WorkbenchActor; params: Record<string, unknown> }): Promise<Record<string, unknown>>;
@@ -0,0 +1,75 @@
import { expect, test } from "bun:test";
import { createWorkbenchKafkaEventPublisher } from "./kafka-event-publisher.ts";
test("L0 publisher writes semantic activity to hwlab.event.v1", async () => {
const sent: Array<Record<string, any>> = [];
const publisher = createWorkbenchKafkaEventPublisher({
env: kafkaEnvironment(),
kafkaFactory: (() => ({
producer() {
return {
async connect() {},
async disconnect() {},
async send(input: Record<string, any>) { sent.push(input); return [{ partition: 0, baseOffset: "7" }]; },
};
},
})) as never,
now: () => "2026-07-20T11:45:01.000Z",
});
await publisher.publish({
traceId: "trc_l0_publish",
sessionId: "ses_l0_publish",
event: {
type: "error",
failureDomain: "infrastructure",
component: "agentrun-manager",
code: "connection-refused",
failureKind: "connection-refused",
retryPhase: "retryExhausted",
retryable: true,
attempt: 1,
maxAttempts: 1,
summary: "AgentRun 服务暂时不可达",
},
});
await publisher.close?.();
expect(sent).toHaveLength(1);
expect(sent[0]?.topic).toBe("hwlab.event.v1");
const envelope = JSON.parse(sent[0]?.messages[0].value);
expect(envelope).toMatchObject({
schema: "hwlab.event.v1",
traceId: "trc_l0_publish",
sessionId: "ses_l0_publish",
event: {
type: "error",
status: "failed",
failureDomain: "infrastructure",
failureComponent: "agentrun-manager",
failureCode: "connection-refused",
retryPhase: "retryExhausted",
retryAttempt: 1,
retryMaxAttempts: 1,
terminal: false,
source: "hwlab.workbench.activity",
},
});
});
function kafkaEnvironment(): Record<string, string> {
return {
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false",
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true",
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false",
HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false",
HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "false",
HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false",
HWLAB_KAFKA_BOOTSTRAP_SERVERS: "127.0.0.1:9092",
HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC: "agentrun.event.v1",
HWLAB_KAFKA_EVENT_TOPIC: "hwlab.event.v1",
HWLAB_KAFKA_CLIENT_ID: "hwlab-workbench-l0",
HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID: "hwlab-workbench-l0-live",
};
}
@@ -0,0 +1,68 @@
import { randomUUID } from "node:crypto";
import {
buildHwlabKafkaProducerMessage,
defaultKafkaFactory,
kafkaEventBridgeConfig,
projectAgentRunKafkaEventToHwlabEvent,
} from "../cloud/kafka-event-bridge.ts";
import type { WorkbenchEventPublisher } from "./contracts.ts";
export function createWorkbenchKafkaEventPublisher(options: {
env: Record<string, string | undefined>;
kafkaFactory?: typeof defaultKafkaFactory;
now?: () => string;
}): WorkbenchEventPublisher {
const config = kafkaEventBridgeConfig(options.env);
if (!config) throw codedError("workbench_kafka_publisher_unconfigured", "Workbench event publisher requires Kafka realtime configuration.");
const kafka = (options.kafkaFactory ?? defaultKafkaFactory)(config);
const producer = kafka.producer({ allowAutoTopicCreation: false });
let ready: Promise<unknown> | null = null;
const now = options.now ?? (() => new Date().toISOString());
return {
async publish(input) {
ready ??= producer.connect();
await ready;
const createdAt = timestamp(input.event.createdAt) ?? now();
const sourceEventId = text(input.event.sourceEventId) || `workbench:${input.traceId}:${randomUUID()}`;
const sourceType = text(input.event.type) || "backend_status";
const source = {
schema: "hwlab.workbench.activity.v1",
eventId: sourceEventId,
traceId: input.traceId,
sessionId: input.sessionId,
event: {
id: sourceEventId,
type: sourceType,
traceId: input.traceId,
createdAt,
payload: {
...input.event,
type: sourceType,
traceId: input.traceId,
sessionId: input.sessionId,
},
},
};
const envelope = projectAgentRunKafkaEventToHwlabEvent(source, {
source: `${config.clientId}:workbench-activity`,
producedAt: createdAt,
});
if (!envelope) throw codedError("workbench_kafka_event_projection_failed", "Workbench event could not be projected to hwlab.event.v1.");
envelope.event.source = "hwlab.workbench.activity";
envelope.diagnostics.projectedBy = "hwlab-workbench-kafka-event-publisher";
await producer.send({ topic: config.hwlabTopic, messages: [buildHwlabKafkaProducerMessage(envelope)] });
return { published: true, topic: config.hwlabTopic, traceId: input.traceId, sessionId: input.sessionId, sourceEventId, valuesPrinted: false };
},
async close() {
if (!ready) return;
await ready.catch(() => undefined);
await producer.disconnect();
},
};
}
function text(value: unknown): string { return String(value ?? "").trim(); }
function timestamp(value: unknown): string | null { const result = text(value); return result && Number.isFinite(Date.parse(result)) ? new Date(Date.parse(result)).toISOString() : null; }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
@@ -89,6 +89,63 @@ test("L1 AgentRun application leaves dispatch failures to Temporal instead of pr
expect((await app.snapshot!()).turns[input.traceId]).toMatchObject({ status: "admitted", terminal: false });
});
test("L0 publishes admission, bounded ConnectionRefused retry, and semantic terminal failure to Kafka authority", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "hwlab-workbench-agentrun-visible-failure-"));
temporaryDirectories.push(directory);
const published: Array<Record<string, any>> = [];
let runCreateAttempts = 0;
const app = createNativeAgentRunWorkbenchApplication({
stateFile: path.join(directory, "state.json"),
env: agentRunEnvironment(),
eventPublisher: {
async publish(input) { published.push(input); return { published: true }; },
},
fetchImpl: (async (request) => {
const url = new URL(String(request));
if (url.pathname === "/api/v1/sessions") {
return Response.json({ ok: true, data: { session: { id: "ses_agentrun_l0_visible_failure", metadata: {} } } });
}
runCreateAttempts += 1;
throw Object.assign(new Error("Unable to connect. Is the computer able to access the url?"), { code: "ConnectionRefused" });
}) as typeof fetch,
now: () => "2026-07-20T11:45:01.000Z",
});
const created = await app.createSession({ actor: { id: "usr_native" }, params: { providerProfile: "gpt.pika" } });
const session = created.session as Record<string, any>;
const input = await app.admitTurn({
actor: { id: "usr_native" },
traceId: "trc_l0_connection_refused",
sessionId: session.sessionId,
params: { message: "hi", providerProfile: "gpt.pika" },
});
await expect(app.dispatchTurn(input)).rejects.toMatchObject({
failureKind: "connection-refused",
retryable: true,
retryExhausted: true,
retryAttempt: 1,
});
expect(runCreateAttempts).toBe(2);
expect(published.map((item) => item.event.type)).toEqual([
"backend_status",
"backend_status",
"error",
"terminal_status",
]);
expect(published[0]?.event).toMatchObject({ phase: "request-admitted", summary: "请求已接纳,正在启动 AgentRun" });
expect(published[1]?.event).toMatchObject({
retryPhase: "retryScheduled",
failureDomain: "infrastructure",
component: "agentrun-manager",
code: "connection-refused",
attempt: 1,
maxAttempts: 1,
});
expect(published[2]?.event).toMatchObject({ retryPhase: "retryExhausted", summary: "AgentRun 服务暂时不可达" });
expect(published[3]?.event).toMatchObject({ terminalStatus: "failed", failureKind: "connection-refused", terminal: true });
expect(published.every((item) => item.traceId === input.traceId && item.sessionId === input.sessionId)).toBe(true);
});
function agentRunEnvironment(): Record<string, string> {
return {
WORKBENCH_MODE: "agentrun-native",
@@ -8,7 +8,7 @@ import {
submitAgentRunChatTurn,
} from "../cloud/code-agent-agentrun-adapter.ts";
import { createCodeAgentTraceStore } from "../cloud/code-agent-trace-store.ts";
import type { WorkbenchApplication, WorkbenchTurnInput } from "./contracts.ts";
import type { WorkbenchApplication, WorkbenchEventPublisher, WorkbenchTurnInput } from "./contracts.ts";
type NativeState = {
sessions: Record<string, Record<string, any>>;
@@ -20,11 +20,13 @@ export function createNativeAgentRunWorkbenchApplication(options: {
env: Record<string, string | undefined>;
fetchImpl?: typeof fetch;
now?: () => string;
eventPublisher?: WorkbenchEventPublisher | null;
}): WorkbenchApplication {
const env = options.env;
const fetchImpl = options.fetchImpl ?? fetch;
const now = options.now ?? (() => new Date().toISOString());
const traceStore = createCodeAgentTraceStore();
const eventPublisher = options.eventPublisher ?? null;
const mutate = async <T>(fn: (state: NativeState) => T | Promise<T>) => {
const state = await load(options.stateFile);
const result = await fn(state);
@@ -74,7 +76,7 @@ export function createNativeAgentRunWorkbenchApplication(options: {
});
},
async admitTurn(input) {
return mutate((state) => {
const admitted = await mutate((state) => {
const session = ownedSession(state, input.sessionId, input.actor.id);
const timestamp = validTimestamp(input.params.submittedAt) ?? now();
const message = requiredText(input.params.message, "message");
@@ -95,6 +97,21 @@ export function createNativeAgentRunWorkbenchApplication(options: {
session.updatedAt = timestamp;
return { ...input, params: { ...input.params, message, submittedAt: timestamp, providerProfile: text(input.params.providerProfile) || session.providerProfile } };
});
await eventPublisher?.publish({
traceId: admitted.traceId,
sessionId: admitted.sessionId,
event: {
type: "backend_status",
phase: "request-admitted",
status: "running",
summary: "请求已接纳,正在启动 AgentRun",
message: "请求已接纳,正在启动 AgentRun",
createdAt: admitted.params.submittedAt,
terminal: false,
valuesPrinted: false,
},
});
return admitted;
},
async dispatchTurn(input: WorkbenchTurnInput) {
const state = await load(options.stateFile);
@@ -109,7 +126,15 @@ export function createNativeAgentRunWorkbenchApplication(options: {
providerProfile: text(input.params.providerProfile) || session.providerProfile,
shortConnection: true,
};
const admitted = await submitAgentRunChatTurn({ traceId: input.traceId, params, options: { env, fetchImpl }, traceStore });
const publishingTrace = publishingDispatchTraceStore(traceStore, eventPublisher, input);
let admitted;
try {
admitted = await submitAgentRunChatTurn({ traceId: input.traceId, params, options: { env, fetchImpl }, traceStore: publishingTrace.store });
} catch (error) {
await publishingTrace.flush().catch(() => undefined);
throw error;
}
await publishingTrace.flush().catch(() => undefined);
await projectRunning(admitted, input.traceId);
return {
status: "running",
@@ -122,6 +147,7 @@ export function createNativeAgentRunWorkbenchApplication(options: {
valuesPrinted: false,
};
},
async close() { await eventPublisher?.close?.(); },
async cancelTurn(input) {
const state = await load(options.stateFile);
const turn = state.turns[input.traceId];
@@ -157,6 +183,86 @@ export function createNativeAgentRunWorkbenchApplication(options: {
}
}
function publishingDispatchTraceStore(base: ReturnType<typeof createCodeAgentTraceStore>, publisher: WorkbenchEventPublisher | null, input: WorkbenchTurnInput) {
const pending: Promise<unknown>[] = [];
const store = {
...base,
append(traceId: string, event: Record<string, any> = {}, meta: Record<string, unknown> = {}) {
const normalized = base.append(traceId, event, meta);
if (publisher && dispatchVisibilityEvent(event)) {
pending.push(publishDispatchVisibility(publisher, input, { ...event, createdAt: normalized?.createdAt ?? event.createdAt }));
}
return normalized;
},
};
return { store, async flush() { await Promise.all(pending); } };
}
function dispatchVisibilityEvent(event: Record<string, any>): boolean {
const label = text(event.label);
const visible = label.startsWith("agentrun:dispatch-retry:")
|| label.startsWith("agentrun:dispatch-retry-exhausted:")
|| label.startsWith("agentrun:dispatch-failed:");
return visible && (event.willRetry === true || event.terminal === true);
}
async function publishDispatchVisibility(publisher: WorkbenchEventPublisher, input: WorkbenchTurnInput, event: Record<string, any>) {
const retrying = event.willRetry === true;
const exhausted = event.terminal === true;
const failureKind = text(event.failureKind || event.errorCode) || "agentrun-dispatch-error";
const summary = dispatchFailureSummary(failureKind);
await publisher.publish({
traceId: input.traceId,
sessionId: input.sessionId,
event: {
type: retrying ? "backend_status" : "error",
phase: retrying ? "dispatch-retry-scheduled" : "dispatch-retry-exhausted",
failureDomain: "infrastructure",
component: "agentrun-manager",
code: failureKind,
failureKind,
summary,
message: summary,
retryable: event.retryable === true,
willRetry: retrying,
retryPhase: retrying ? "retryScheduled" : "retryExhausted",
attempt: event.retryAttempt,
maxAttempts: event.retryMax,
backoffMs: event.retryDelayMs,
nextRetryAt: event.nextRetryAt,
createdAt: event.createdAt,
runId: event.runId ?? null,
commandId: event.commandId ?? null,
terminal: false,
valuesPrinted: false,
},
});
if (!exhausted) return;
await publisher.publish({
traceId: input.traceId,
sessionId: input.sessionId,
event: {
type: "terminal_status",
terminalStatus: "failed",
status: "failed",
failureKind,
errorCode: failureKind,
message: `${summary},有限重试已耗尽。`,
createdAt: event.createdAt,
runId: event.runId ?? null,
commandId: event.commandId ?? null,
terminal: true,
valuesPrinted: false,
},
});
}
function dispatchFailureSummary(failureKind: string): string {
if (failureKind === "connection-refused" || failureKind === "agentrun-connect-failed") return "AgentRun 服务暂时不可达";
if (failureKind.includes("timeout")) return "AgentRun 服务响应超时";
return "AgentRun 调度基础设施暂时不可用";
}
async function load(stateFile: string): Promise<NativeState> {
const parsed = JSON.parse(await readFile(stateFile, "utf8").catch(() => '{"sessions":{},"turns":{}}'));
return { sessions: object(parsed.sessions), turns: object(parsed.turns) };
+4 -2
View File
@@ -6,15 +6,17 @@ import { createNativeTestWorkbenchApplication } from "./native-application.ts";
import { createNativeAgentRunWorkbenchApplication } from "./native-agentrun-application.ts";
import { createNativeTestTemporalGateway } from "./native-temporal.ts";
import { createWorkbenchTemporalGateway } from "./temporal.ts";
import { createWorkbenchKafkaEventPublisher } from "./kafka-event-publisher.ts";
import type { WorkbenchEventPublisher } from "./contracts.ts";
export function workbenchRuntime(env: Record<string, string | undefined> = process.env) {
export function workbenchRuntime(env: Record<string, string | undefined> = process.env, options: { eventPublisher?: WorkbenchEventPublisher } = {}) {
const mode = String(env.WORKBENCH_MODE ?? "").trim();
if (mode !== "native-test" && mode !== "agentrun-native" && mode !== "temporal") throw codedError("workbench_mode_required", "WORKBENCH_MODE must be native-test, agentrun-native, or temporal");
const stateFile = path.resolve(String(env.WORKBENCH_NATIVE_STATE_FILE ?? ".state/workbench-native/state.json"));
const application = mode === "native-test"
? createNativeTestWorkbenchApplication({ stateFile })
: mode === "agentrun-native"
? createNativeAgentRunWorkbenchApplication({ stateFile, env })
? createNativeAgentRunWorkbenchApplication({ stateFile, env, eventPublisher: options.eventPublisher ?? createWorkbenchKafkaEventPublisher({ env }) })
: createCloudWorkbenchApplication({ baseUrl: String(env.WORKBENCH_CLOUD_API_URL ?? ""), authorization: String(env.WORKBENCH_CLOUD_API_AUTHORIZATION ?? "") });
const temporalAddress = String(env.WORKBENCH_TEMPORAL_ADDRESS ?? env.TEMPORAL_ADDRESS ?? "");
const temporalNamespace = String(env.WORKBENCH_TEMPORAL_NAMESPACE ?? "unidesk");
@@ -425,7 +425,7 @@ test("timeline model deduplicates authoritative messages with stable row keys",
test("timeline model preserves OpenCode turn gap, thinking, retry and error row parity", () => {
const rows = buildWorkbenchTimelineRows([
{ id: "msg_user_1", messageId: "msg_user_1", role: "user", title: "用户", text: "hi", status: "sent", createdAt: "2026-06-30T00:00:00.000Z", sessionId: "ses_a", traceId: "trc_a" },
{ id: "msg_agent_1", messageId: "msg_agent_1", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-30T00:00:01.000Z", sessionId: "ses_a", traceId: "trc_a" },
{ id: "msg_agent_1", messageId: "msg_agent_1", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-30T00:00:01.000Z", sessionId: "ses_a", traceId: "trc_a", runnerTrace: { traceId: "trc_a", events: [] } },
{ id: "msg_user_2", messageId: "msg_user_2", role: "user", title: "用户", text: "again", status: "sent", createdAt: "2026-06-30T00:00:02.000Z", sessionId: "ses_a", traceId: "trc_b" },
{ id: "msg_agent_2", messageId: "msg_agent_2", role: "agent", title: "Code Agent", text: "", status: "retry" as never, createdAt: "2026-06-30T00:00:03.000Z", sessionId: "ses_a", traceId: "trc_b", error: { message: "Error: {\"error\":{\"type\":\"Provider\",\"message\":\"offline\"}}" } }
]);
@@ -436,6 +436,26 @@ test("timeline model preserves OpenCode turn gap, thinking, retry and error row
assert.ok(rows.some((row) => row.type === "Error" && row.text === "Provider: offline"));
});
test("timeline model leaves pre-Kafka startup loading to the message card", () => {
const rows = buildWorkbenchTimelineRows([
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "hi", status: "sent", createdAt: "2026-06-30T00:00:00.000Z", sessionId: "ses_start", traceId: "trc_start" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-30T00:00:01.000Z", sessionId: "ses_start", traceId: "trc_start" }
]);
assert.equal(rows.filter((row) => row.type === "Thinking").length, 0);
});
test("timeline model labels Kafka admission as startup and suppresses thinking during infrastructure failure", () => {
const startupRows = buildWorkbenchTimelineRows([
{ id: "msg_agent_start", messageId: "msg_agent_start", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-30T00:00:01.000Z", sessionId: "ses_start", traceId: "trc_start", runnerTrace: { traceId: "trc_start", events: [{ type: "backend", label: "agentrun:backend:request-admitted", message: "请求已接纳,正在启动 AgentRun" }] } }
]);
assert.ok(startupRows.some((row) => row.type === "Thinking" && row.reasoningHeading === "启动中..."));
const failureRows = buildWorkbenchTimelineRows([
{ id: "msg_agent_fail", messageId: "msg_agent_fail", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-30T00:00:01.000Z", sessionId: "ses_fail", traceId: "trc_fail", runnerTrace: { traceId: "trc_fail", events: [{ type: "error", label: "agentrun:error:connection-refused", failureDomain: "infrastructure", retryPhase: "retryScheduled" }] } }
]);
assert.equal(failureRows.filter((row) => row.type === "Thinking").length, 0);
});
test("timeline model documents OpenCode row parity and preserves comment/diff rows", () => {
assert.ok(WORKBENCH_TIMELINE_OPENCODE_PARITY.some((row) => row.openCodeRows === "Permission" && row.status === "not-applicable"));
assert.ok(WORKBENCH_TIMELINE_OPENCODE_PARITY.some((row) => row.openCodeRows === "Tool" && row.status === "adapted"));
@@ -15,6 +15,37 @@ function componentStubs() {
};
}
describe("WorkbenchMessageCard startup visibility", () => {
it("shows one startup label before the first Kafka event", () => {
const wrapper = mount(WorkbenchMessageCard, {
props: {
detailEnabled: false,
message: {
id: "msg_starting_agent",
messageId: "msg_starting_agent",
role: "agent",
title: "Code Agent",
text: "",
status: "running",
traceId: "trc_starting",
sessionId: "ses_starting",
createdAt: "2026-07-20T10:00:00.000Z",
}
},
global: {
stubs: {
...componentStubs(),
LoadingState: { props: ["label"], template: "<div class='loading-state'>{{ label }}</div>" },
}
}
});
expect(wrapper.text()).toContain("启动中...");
expect(wrapper.text()).not.toContain("思考中...");
expect(wrapper.findAll(".loading-state")).toHaveLength(1);
wrapper.unmount();
});
});
describe("WorkbenchMessageCard semantic retry visibility", () => {
it("shows Git mirror clone progress as runtime preparation", () => {
const wrapper = mount(WorkbenchMessageCard, {
@@ -155,7 +155,7 @@ function traceStorageKey(message: ChatMessage): string {
<code v-if="semanticRuntimeStatus.code && semanticRuntimeStatus.emphasis === 'alert'">{{ semanticRuntimeStatus.code }}</code>
</section>
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :auto-expanded="autoExpanded" :storage-key="traceStorageKey(message)" final-response-placement="outer" :outer-final-response-text="visibleText" />
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="启动中..." compact />
<MessageMarkdown v-if="visibleText" class="message-text" :source="visibleText" />
<ApiErrorDiagnostic v-if="diagnostic.visible" class="message-diagnostic projection-diagnostic" :error="diagnostic.text" :api-error="diagnostic.apiError" :diagnostic="diagnostic.diagnostic" :show-message="diagnostic.showMessage" compact />
</article>
@@ -189,7 +189,13 @@ function messageHasCommentStrip(message: ChatMessage): boolean {
}
function messageIsThinking(message: ChatMessage): boolean {
return message.role === "agent" && messageIsActive(message) && !String(message.text ?? "").trim();
const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
const latest = events.at(-1);
return message.role === "agent"
&& Boolean(message.runnerTrace)
&& typeof latest?.failureDomain !== "string"
&& messageIsActive(message)
&& !String(message.text ?? "").trim();
}
function messageIsRetry(message: ChatMessage): boolean {
@@ -223,6 +229,8 @@ function summaryDiffs(message: ChatMessage): Array<Record<string, unknown>> {
function reasoningHeading(message: ChatMessage): string | null {
const traceEvents = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
const latest = traceEvents.at(-1);
if (String(latest?.label ?? "") === "agentrun:backend:request-admitted") return "启动中...";
const reasoning = traceEvents.map((event) => firstNonEmptyString(event.text, event.message, event.outputSummary)).find(Boolean) ?? null;
return reasoning ? extractHeading(reasoning) : null;
}