Merge pull request #1415 from pikasTech/issue-1397-agentrun-event-visible-p4
#1392/P4 AgentRun event-to-visible 延迟贯通
This commit is contained in:
@@ -1770,6 +1770,7 @@ function mapAgentRunEvent(event, mapping = {}) {
|
||||
createdAt: event.createdAt ?? null,
|
||||
source: "agentrun",
|
||||
sourceSeq: event.seq ?? null,
|
||||
backend: backendForBackendProfile(mapping.backendProfile ?? payload.backendProfile ?? payload.providerProfile ?? "deepseek"),
|
||||
runId: event.runId ?? mapping.runId ?? null,
|
||||
commandId: payload.commandId ?? mapping.commandId ?? null,
|
||||
attemptId: payload.attemptId ?? mapping.attemptId ?? null,
|
||||
@@ -1783,6 +1784,7 @@ function mapAgentRunEvent(event, mapping = {}) {
|
||||
return {
|
||||
...base,
|
||||
type: "backend",
|
||||
eventType: "backend",
|
||||
status: "running",
|
||||
label: `agentrun:backend:${phase}`,
|
||||
message: textPayload(payload, phase),
|
||||
@@ -1795,6 +1797,7 @@ function mapAgentRunEvent(event, mapping = {}) {
|
||||
return {
|
||||
...base,
|
||||
type: "assistant",
|
||||
eventType: "assistant",
|
||||
status: terminal ? "completed" : "running",
|
||||
label: "agentrun:assistant:message",
|
||||
message: textPayload(payload, "assistant message"),
|
||||
@@ -1827,6 +1830,7 @@ function mapAgentRunEvent(event, mapping = {}) {
|
||||
return {
|
||||
...base,
|
||||
type: "tool_call",
|
||||
eventType: "tool_call",
|
||||
status: String(payload.status ?? preview.status ?? (payload.method === "item/completed" ? "completed" : "running")),
|
||||
label: `agentrun:tool:${String(toolName)}`,
|
||||
itemId: payload.item?.id ?? payload.itemId ?? preview.itemId ?? null,
|
||||
@@ -1843,19 +1847,19 @@ function mapAgentRunEvent(event, mapping = {}) {
|
||||
}
|
||||
if (event.type === "command_output") {
|
||||
const stream = payload.stream === "stderr" ? "stderr" : "stdout";
|
||||
return { ...base, type: "output", status: "running", label: `agentrun:output:${stream}`, stream, message: textPayload(payload, ""), outputTruncated: payload.outputTruncated === true };
|
||||
return { ...base, type: "output", eventType: "status", status: "running", label: `agentrun:output:${stream}`, stream, message: textPayload(payload, ""), outputTruncated: payload.outputTruncated === true };
|
||||
}
|
||||
if (event.type === "diff") {
|
||||
return { ...base, type: "diff", status: "running", label: "agentrun:diff", message: textPayload(payload, "diff") };
|
||||
return { ...base, type: "diff", eventType: "status", status: "running", label: "agentrun:diff", message: textPayload(payload, "diff") };
|
||||
}
|
||||
if (event.type === "error") {
|
||||
return { ...base, type: "error", status: "failed", label: `agentrun:error:${String(payload.failureKind ?? "backend")}`, errorCode: payload.failureKind ?? "agentrun_error", message: textPayload(payload, "AgentRun error") };
|
||||
return { ...base, type: "error", eventType: "error", status: "failed", label: `agentrun:error:${String(payload.failureKind ?? "backend")}`, errorCode: payload.failureKind ?? "agentrun_error", message: textPayload(payload, "AgentRun error") };
|
||||
}
|
||||
if (event.type === "terminal_status") {
|
||||
const terminalStatus = String(payload.terminalStatus ?? "failed");
|
||||
return { ...base, type: "result", status: terminalStatus === "cancelled" ? "canceled" : terminalStatus, label: `agentrun:terminal:${terminalStatus}`, errorCode: payload.failureKind ?? null, message: textPayload(payload, `AgentRun terminal status ${terminalStatus}`), terminal: true };
|
||||
return { ...base, type: "result", eventType: "terminal", status: terminalStatus === "cancelled" ? "canceled" : terminalStatus, label: `agentrun:terminal:${terminalStatus}`, errorCode: payload.failureKind ?? null, message: textPayload(payload, `AgentRun terminal status ${terminalStatus}`), terminal: true };
|
||||
}
|
||||
return { ...base, type: "backend", status: "running", label: `agentrun:event:${String(event.type ?? "unknown")}`, message: textPayload(payload, "AgentRun event") };
|
||||
return { ...base, type: "backend", eventType: "backend", status: "running", label: `agentrun:event:${String(event.type ?? "unknown")}`, message: textPayload(payload, "AgentRun event") };
|
||||
}
|
||||
|
||||
function isProtocolToolLifecycleMethod(value) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// Responsibility: Code Agent trace timing and durable projection tests.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
@@ -88,9 +91,13 @@ test("code agent trace store preserves upstream event timestamps", () => {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:backend:turn/completed",
|
||||
backend: "agentrun-v01/codex",
|
||||
createdAt: "2026-06-01T16:35:40.939Z"
|
||||
}, { now: "2026-06-01T16:35:56.171Z" });
|
||||
const snapshot = traceStore.snapshot(traceId);
|
||||
assert.equal(snapshot.events[0].createdAt, "2026-06-01T16:35:40.939Z");
|
||||
assert.equal(snapshot.events[0].appendedAt, "2026-06-01T16:35:56.171Z");
|
||||
assert.equal(snapshot.events[0].eventType, "backend");
|
||||
assert.equal(snapshot.events[0].backend, "agentrun-v01/codex");
|
||||
assert.equal(snapshot.elapsedMs, 0);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0
|
||||
* SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
|
||||
* 职责: Code Agent trace capture。实时内存 trace 可加速 SSE/详情面板,但 append 事件必须同步投影到 durable store。
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -351,6 +351,7 @@ function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } =
|
||||
const status = safeToken(event.status ?? "observed");
|
||||
const toolName = safeText(event.toolName ?? event.name, 120);
|
||||
const label = safeText(event.label, 180) || labelFor({ type, status, toolName });
|
||||
const appendedAt = timestampFor(now);
|
||||
const eventCreatedAt = safeTimestamp(event.createdAt) ?? timestampFor(now);
|
||||
return dropUndefined({
|
||||
seq,
|
||||
@@ -360,6 +361,9 @@ function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } =
|
||||
status,
|
||||
label,
|
||||
createdAt: eventCreatedAt,
|
||||
appendedAt,
|
||||
eventType: safeToken(event.eventType ?? type),
|
||||
backend: safeText(event.backend, 140),
|
||||
elapsedMs: typeof event.elapsedMs === "number" ? Math.max(0, Math.trunc(event.elapsedMs)) : null,
|
||||
runnerKind: safeText(event.runnerKind ?? fallbackRunnerKind, 140),
|
||||
source: safeText(event.source, 120),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// Responsibility: Cloud API AgentRun adapter and trace observability regression tests.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer as createHttpServer } from "node:http";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
@@ -680,6 +683,9 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.ok(traceBody.events.some((event) => event.label === "agentrun:assistant:message"));
|
||||
assert.equal(traceBody.events.some((event) => event.details?.initialPromptInjected === true), true);
|
||||
const commandTraceEvent = traceBody.events.find((event) => event.label === "item/commandExecution:completed");
|
||||
assert.equal(commandTraceEvent.eventType, "tool_call");
|
||||
assert.equal(commandTraceEvent.backend, "agentrun-v01/deepseek");
|
||||
assert.ok(Date.parse(commandTraceEvent.appendedAt) >= Date.parse(commandTraceEvent.createdAt));
|
||||
assert.equal(commandTraceEvent.toolName, "commandExecution");
|
||||
assert.equal(commandTraceEvent.createdAt, "2026-06-01T00:00:00.500Z");
|
||||
assert.match(commandTraceEvent.command, /hwpod inspect --dry-run/u);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"check": "bun run scripts/check.ts && bun run scripts/tsc-check.ts && bun test",
|
||||
"check": "bun run scripts/check.ts && bun run scripts/tsc-check.ts && bun test scripts",
|
||||
"check:tsc": "bun run scripts/tsc-check.ts",
|
||||
"check:tsc-strict": "bun run scripts/tsc-check.ts --strict",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
|
||||
@@ -29,10 +29,13 @@ test("Workbench performance probe records journeys and strips high-cardinality i
|
||||
acknowledgeWorkbenchVisible({ messages: [userMessage("cnv_secret")], activeConversationId: "cnv_secret", detailLoading: false });
|
||||
finishWorkbenchSessionSwitchFullLoad("cnv_secret", "ok");
|
||||
|
||||
const createdAt = new Date(Date.now() - 250).toISOString();
|
||||
const wallBase = Date.now();
|
||||
const createdAt = new Date(wallBase - 300).toISOString();
|
||||
const appendedAt = new Date(wallBase - 220).toISOString();
|
||||
const serverSentAt = new Date(wallBase - 120).toISOString();
|
||||
startWorkbenchSubmitJourney({ traceId: "trc_secret", conversationId: "cnv_secret", entry: "existing", backend: "codex", transport: "sse" });
|
||||
markWorkbenchSubmitApiAccepted("trc_secret");
|
||||
markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "sse", events: [{ type: "assistant_message", createdAt, ts: createdAt } as TraceEvent] });
|
||||
markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "sse", serverSentAt, traceSeq: 42, events: [{ type: "assistant_message", eventType: "assistant", backend: "agentrun-v01/codex", createdAt, appendedAt, ts: createdAt, sourceSeq: 42 } as TraceEvent] });
|
||||
markWorkbenchTraceProjected("trc_secret");
|
||||
acknowledgeWorkbenchVisible({ messages: [agentMessage("cnv_secret", "trc_secret")], activeConversationId: "cnv_secret", detailLoading: false });
|
||||
|
||||
@@ -41,7 +44,11 @@ test("Workbench performance probe records journeys and strips high-cardinality i
|
||||
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "workbench_open_first_visible"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "session_switch_first_visible"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.backend === "agentrun-v01/codex"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "project_to_paint"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "created_to_append" && event.eventType === "assistant"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "append_to_sse" && event.eventType === "assistant"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive" && event.eventType === "assistant"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "receive_to_project" && event.eventType === "assistant"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "project_to_paint" && event.eventType === "assistant"));
|
||||
assert.ok(events.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "assistant"));
|
||||
assert.doesNotMatch(payload, /trc_secret|cnv_secret|sessionId|conversationId|runId|commandId/u);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0.
|
||||
// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// Responsibility: Workbench SSE client. Realtime events accelerate UI projection; REST snapshots remain gap-fill authority.
|
||||
|
||||
import type { TraceEvent, WorkspaceRecord } from "@/types";
|
||||
@@ -16,6 +16,8 @@ export interface WorkbenchRealtimeTraceSnapshot {
|
||||
export interface WorkbenchRealtimeEvent {
|
||||
type?: string;
|
||||
status?: string;
|
||||
serverSentAt?: string | null;
|
||||
eventCreatedAt?: string | null;
|
||||
projectId?: string;
|
||||
workspace?: WorkspaceRecord | null;
|
||||
workspaceId?: string | null;
|
||||
@@ -26,6 +28,7 @@ export interface WorkbenchRealtimeEvent {
|
||||
turn?: Record<string, unknown> | null;
|
||||
reason?: string | null;
|
||||
error?: { code?: string; message?: string } | null;
|
||||
traceSeq?: number | null;
|
||||
cursor?: { workspaceRevision?: number | null; traceSeq?: number | null; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -764,7 +764,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return;
|
||||
}
|
||||
if (event.type === "trace.event") {
|
||||
applyRealtimeTraceEvent(event.traceId, event.event, event.snapshot);
|
||||
applyRealtimeTraceEvent(event.traceId, event.event, event.snapshot, event);
|
||||
return;
|
||||
}
|
||||
if (event.type === "turn.snapshot" && event.turn) {
|
||||
@@ -798,11 +798,11 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (isTerminalMessageStatus(snapshot.status)) void refreshTerminalTraceFromRest(id, "realtime-trace-snapshot");
|
||||
}
|
||||
|
||||
function applyRealtimeTraceEvent(traceId: string | null | undefined, event: WorkbenchRealtimeEvent["event"], snapshot: WorkbenchRealtimeEvent["snapshot"]): void {
|
||||
function applyRealtimeTraceEvent(traceId: string | null | undefined, event: WorkbenchRealtimeEvent["event"], snapshot: WorkbenchRealtimeEvent["snapshot"], realtimeEvent?: WorkbenchRealtimeEvent | null): void {
|
||||
const id = firstNonEmptyString(traceId, event?.traceId, snapshot?.traceId);
|
||||
if (!id) return;
|
||||
const events = event ? [event] : Array.isArray(snapshot?.events) ? snapshot.events : [];
|
||||
markWorkbenchTraceEventsReceived({ traceId: id, events, transport: "sse" });
|
||||
markWorkbenchTraceEventsReceived({ traceId: id, events, transport: "sse", serverSentAt: realtimeEvent?.serverSentAt, eventCreatedAt: realtimeEvent?.eventCreatedAt, traceSeq: realtimeEvent?.traceSeq ?? realtimeEvent?.cursor?.traceSeq });
|
||||
applyTraceSnapshot(id, realtimeSnapshotToTraceSnapshot(id, snapshot ?? { traceId: id, status: event?.status, events }, events));
|
||||
if (event?.terminal === true || isTerminalMessageStatus(event?.status) || isTerminalMessageStatus(snapshot?.status)) void refreshTerminalTraceFromRest(id, "realtime-trace-event");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ interface WorkbenchPerformanceEvent {
|
||||
outcome?: WorkbenchOutcome;
|
||||
}
|
||||
|
||||
interface TraceEventTimingInput {
|
||||
traceId: string | null | undefined;
|
||||
events: TraceEvent[];
|
||||
transport: "sse" | "rest_gap" | "poll";
|
||||
serverSentAt?: string | null | undefined;
|
||||
eventCreatedAt?: string | null | undefined;
|
||||
traceSeq?: number | string | null | undefined;
|
||||
}
|
||||
|
||||
interface OpenJourneyState {
|
||||
startAt: number;
|
||||
route: string;
|
||||
@@ -61,6 +70,9 @@ interface TraceEventState {
|
||||
receivedAt: number;
|
||||
projectedAt: number | null;
|
||||
eventCreatedAt: number | null;
|
||||
appendedAt: number | null;
|
||||
serverSentAt: number | null;
|
||||
traceSeq: number | null;
|
||||
eventType: string;
|
||||
backend: string;
|
||||
transport: string;
|
||||
@@ -191,19 +203,24 @@ export function failWorkbenchSubmitJourney(traceId: string | null | undefined, o
|
||||
submitJourneys.delete(state.traceId);
|
||||
}
|
||||
|
||||
export function markWorkbenchTraceEventsReceived(input: { traceId: string | null | undefined; events: TraceEvent[]; transport: "sse" | "rest_gap" | "poll" }): void {
|
||||
export function markWorkbenchTraceEventsReceived(input: TraceEventTimingInput): void {
|
||||
const traceId = safeText(input.traceId);
|
||||
if (!traceId) return;
|
||||
const event = input.events.find(isUserVisibleTraceEvent) ?? input.events[0];
|
||||
if (!event) return;
|
||||
const eventType = eventTypeFromTraceEvent(event);
|
||||
const backend = backendFromTraceEvent(event);
|
||||
const eventCreatedAt = traceEventCreatedAt(event);
|
||||
const eventCreatedAt = traceEventCreatedAt(event, input.eventCreatedAt);
|
||||
const appendedAt = timestampMs(event.appendedAt);
|
||||
const serverSentAt = timestampMs(input.serverSentAt);
|
||||
const receivedAt = monotonicNow();
|
||||
traceEvents.set(traceId, { traceId, receivedAt, projectedAt: null, eventCreatedAt, eventType, backend, transport: input.transport, visibleReported: false });
|
||||
if (eventCreatedAt !== null) {
|
||||
enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, wallNow() - eventCreatedAt) });
|
||||
}
|
||||
const receivedWallAt = wallNow();
|
||||
const traceSeq = Number(input.traceSeq ?? event.seq ?? event.sourceSeq ?? NaN);
|
||||
traceEvents.set(traceId, { traceId, receivedAt, projectedAt: null, eventCreatedAt, appendedAt, serverSentAt, traceSeq: Number.isFinite(traceSeq) ? Math.trunc(traceSeq) : null, eventType, backend, transport: input.transport, visibleReported: false });
|
||||
if (eventCreatedAt !== null && appendedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "created_to_append", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, appendedAt - eventCreatedAt) });
|
||||
if (appendedAt !== null && serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "append_to_sse", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, serverSentAt - appendedAt) });
|
||||
if (serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, receivedWallAt - serverSentAt) });
|
||||
else if (eventCreatedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, receivedWallAt - eventCreatedAt) });
|
||||
}
|
||||
|
||||
export function markWorkbenchTraceProjected(traceId: string | null | undefined): void {
|
||||
@@ -322,7 +339,7 @@ function isUserVisibleTraceEvent(event: TraceEvent): boolean {
|
||||
}
|
||||
|
||||
function eventTypeFromTraceEvent(event: TraceEvent | null | undefined): string {
|
||||
const raw = safeText(event?.type ?? event?.kind ?? event?.label ?? event?.status).toLowerCase();
|
||||
const raw = safeText(event?.eventType ?? event?.type ?? event?.kind ?? event?.label ?? event?.status).toLowerCase();
|
||||
if (/tool/u.test(raw)) return "tool_call";
|
||||
if (/assistant|message|delta|reply/u.test(raw)) return "assistant";
|
||||
if (/terminal|complete|final/u.test(raw)) return "terminal";
|
||||
@@ -357,8 +374,14 @@ function normalizeTargetState(value: unknown): string {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function traceEventCreatedAt(event: TraceEvent): number | null {
|
||||
const raw = safeText(event.createdAt ?? event.ts ?? event.timestamp);
|
||||
function traceEventCreatedAt(event: TraceEvent, fallback?: unknown): number | null {
|
||||
const raw = safeText(event.createdAt ?? fallback ?? event.ts ?? event.timestamp);
|
||||
const parsed = raw ? Date.parse(raw) : NaN;
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function timestampMs(value: unknown): number | null {
|
||||
const raw = safeText(value);
|
||||
const parsed = raw ? Date.parse(raw) : NaN;
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user