fix(workbench): expose turn lifecycle ids
This commit is contained in:
@@ -299,12 +299,25 @@ test("cloud api /v1/agent/chat supports short submit and result polling", async
|
||||
assert.equal(accepted.accepted, true);
|
||||
assert.equal(accepted.shortConnection, true);
|
||||
assert.equal(accepted.traceId, traceId);
|
||||
assert.equal(accepted.turnId, traceId);
|
||||
assert.equal(accepted.userMessageId, "msg_server-test-short-submit_user");
|
||||
assert.equal(accepted.assistantMessageId, "msg_server-test-short-submit_agent");
|
||||
assert.equal(accepted.resultUrl, `/v1/agent/chat/result/${traceId}`);
|
||||
|
||||
const acceptedTurn = await fetch(`http://127.0.0.1:${port}/v1/agent/turns/${encodeURIComponent(traceId)}`, { headers: { cookie: manualSession.cookie } });
|
||||
assert.equal(acceptedTurn.status, 200);
|
||||
const acceptedTurnPayload = await acceptedTurn.json();
|
||||
assert.equal(acceptedTurnPayload.turnId, traceId);
|
||||
assert.equal(acceptedTurnPayload.userMessageId, accepted.userMessageId);
|
||||
assert.equal(acceptedTurnPayload.assistantMessageId, accepted.assistantMessageId);
|
||||
|
||||
const payload = await pollAgentResult(port, traceId);
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "completed");
|
||||
assert.equal(payload.traceId, traceId);
|
||||
assert.equal(payload.turnId, traceId);
|
||||
assert.equal(payload.userMessageId, accepted.userMessageId);
|
||||
assert.equal(payload.assistantMessageId, accepted.assistantMessageId);
|
||||
assert.equal(payload.sandbox, "danger-full-access");
|
||||
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
|
||||
} finally {
|
||||
@@ -588,10 +601,23 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(submit.status, 202);
|
||||
const accepted = await submit.json();
|
||||
assert.equal(accepted.shortConnection, true);
|
||||
assert.equal(accepted.turnId, traceId);
|
||||
assert.equal(accepted.userMessageId, "msg_server-test-agentrun-adapter_user");
|
||||
assert.equal(accepted.assistantMessageId, "msg_server-test-agentrun-adapter_agent");
|
||||
|
||||
const acceptedTurn = await fetch(`http://127.0.0.1:${port}/v1/agent/turns/${encodeURIComponent(traceId)}`, { headers: { cookie: "hwlab_session=test-stub-session" } });
|
||||
assert.equal(acceptedTurn.status, 200);
|
||||
const acceptedTurnPayload = await acceptedTurn.json();
|
||||
assert.equal(acceptedTurnPayload.turnId, traceId);
|
||||
assert.equal(acceptedTurnPayload.userMessageId, accepted.userMessageId);
|
||||
assert.equal(acceptedTurnPayload.assistantMessageId, accepted.assistantMessageId);
|
||||
|
||||
const payload = await pollAgentResult(port, traceId);
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "completed");
|
||||
assert.equal(payload.turnId, traceId);
|
||||
assert.equal(payload.userMessageId, accepted.userMessageId);
|
||||
assert.equal(payload.assistantMessageId, accepted.assistantMessageId);
|
||||
assert.equal(payload.provider, "deepseek");
|
||||
assert.equal(payload.backend, "agentrun-v01/deepseek");
|
||||
assert.equal(payload.infrastructureBackend, "agentrun-v01/deepseek");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0.
|
||||
// Responsibility: Code Agent HTTP turn admission, lifecycle compatibility wrappers, and session evidence projection.
|
||||
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
@@ -85,9 +87,11 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
||||
}
|
||||
|
||||
const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId) || `trc_${randomUUID()}`;
|
||||
const lifecycle = codeAgentTurnLifecycleFields(traceId, params);
|
||||
const chatParams = {
|
||||
...params,
|
||||
traceId,
|
||||
...lifecycle,
|
||||
ownerUserId: options.actor?.id ?? params.ownerUserId,
|
||||
ownerRole: options.actor?.role ?? params.ownerRole
|
||||
};
|
||||
@@ -113,6 +117,7 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
||||
shortConnection: true,
|
||||
controlSemantics: "submit-and-poll",
|
||||
traceId,
|
||||
...lifecycle,
|
||||
workspaceId: workspaceClaim?.workspace?.id ?? null,
|
||||
workspaceRevision: workspaceClaim?.workspace?.revision ?? null,
|
||||
conversationId: safeConversationId(nativeSessionChatParams.conversationId) || null,
|
||||
@@ -702,6 +707,7 @@ function submitCodeAgentChatTurn({ params, options, traceId }) {
|
||||
accepted: true,
|
||||
status: "running",
|
||||
traceId,
|
||||
...codeAgentTurnLifecycleFields(traceId, params),
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: safeSessionId(params.sessionId) || null,
|
||||
threadId: safeOpaqueId(params.threadId) || null,
|
||||
@@ -1276,6 +1282,7 @@ function forbiddenTurnSnapshot(traceId) {
|
||||
function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError, refreshError, options }) {
|
||||
const resultObject = result && typeof result === "object" ? result : null;
|
||||
const snapshotObject = snapshot && typeof snapshot === "object" ? snapshot : null;
|
||||
const lifecycle = codeAgentTurnLifecycleFields(traceId, resultObject ?? snapshotObject ?? {});
|
||||
const events = Array.isArray(snapshotObject?.events) ? snapshotObject.events : Array.isArray(resultObject?.runnerTrace?.events) ? resultObject.runnerTrace.events : [];
|
||||
const lastEvent = events.at(-1) ?? null;
|
||||
const status = normalizeTurnStatus(
|
||||
@@ -1300,6 +1307,7 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
|
||||
running: found ? running : false,
|
||||
terminal: found ? terminal : false,
|
||||
traceId,
|
||||
...lifecycle,
|
||||
conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null,
|
||||
sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null,
|
||||
threadId: safeOpaqueId(resultObject?.threadId ?? resultObject?.session?.threadId ?? snapshotObject?.threadId) || null,
|
||||
@@ -2219,6 +2227,7 @@ function codeAgentConversationMessagesEvidence(payload = {}, params = {}, traceI
|
||||
const userText = boundedConversationMessageText(params.message ?? params.prompt ?? payload.userMessage ?? payload.prompt);
|
||||
if (!userText) return [];
|
||||
const resolvedTraceId = safeTraceId(traceId) || null;
|
||||
const lifecycle = codeAgentTurnLifecycleFields(resolvedTraceId, payload);
|
||||
const session = payload.session && typeof payload.session === "object" ? payload.session : null;
|
||||
const sessionReuse = payload.sessionReuse && typeof payload.sessionReuse === "object" ? payload.sessionReuse : null;
|
||||
const conversationId = safeConversationId(payload.conversationId ?? session?.conversationId ?? sessionReuse?.conversationId ?? params.conversationId) || null;
|
||||
@@ -2226,14 +2235,15 @@ function codeAgentConversationMessagesEvidence(payload = {}, params = {}, traceI
|
||||
const threadId = safeOpaqueId(session?.threadId ?? sessionReuse?.threadId ?? payload.threadId ?? params.threadId) || null;
|
||||
const createdAt = payload.createdAt ?? payload.reply?.createdAt ?? new Date().toISOString();
|
||||
const updatedAt = payload.updatedAt ?? createdAt;
|
||||
const traceSuffix = codeAgentMessageTraceSuffix(resolvedTraceId);
|
||||
const userMessage = {
|
||||
id: `msg_${traceSuffix}_user`,
|
||||
id: lifecycle.userMessageId,
|
||||
messageId: lifecycle.userMessageId,
|
||||
role: "user",
|
||||
title: "用户",
|
||||
text: userText,
|
||||
status: "sent",
|
||||
traceId: resolvedTraceId,
|
||||
turnId: lifecycle.turnId,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
@@ -2256,12 +2266,14 @@ function codeAgentConversationMessagesEvidence(payload = {}, params = {}, traceI
|
||||
);
|
||||
const agentStatus = codeAgentConversationAgentMessageStatus(payload);
|
||||
const agentMessage = {
|
||||
id: `msg_${traceSuffix}_agent`,
|
||||
id: lifecycle.assistantMessageId,
|
||||
messageId: lifecycle.assistantMessageId,
|
||||
role: "agent",
|
||||
title: agentStatus === "failed" ? "Code Agent 返回阻塞" : agentStatus === "running" ? "Code Agent 处理中" : "Code Agent 回复",
|
||||
text: agentText || (agentStatus === "running" ? "" : "Code Agent 请求已结束,请查看 Trace 详情。"),
|
||||
status: agentStatus,
|
||||
traceId: resolvedTraceId,
|
||||
turnId: lifecycle.turnId,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
@@ -2306,6 +2318,31 @@ function codeAgentMessageTraceSuffix(traceId) {
|
||||
return text.replace(/^trc_/u, "").replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 48) || "trace";
|
||||
}
|
||||
|
||||
function codeAgentTurnLifecycleFields(traceId, source = {}) {
|
||||
const resolvedTraceId = safeTraceId(source?.traceId ?? traceId) || safeTraceId(traceId) || null;
|
||||
const turnId = safeTurnId(source?.turnId) || resolvedTraceId;
|
||||
const traceSuffix = codeAgentMessageTraceSuffix(resolvedTraceId);
|
||||
return {
|
||||
turnId,
|
||||
userMessageId: safeMessageId(source?.userMessageId ?? source?.userMessage?.messageId) || codeAgentLifecycleMessageId(traceSuffix, "user"),
|
||||
assistantMessageId: safeMessageId(source?.assistantMessageId ?? source?.assistantMessage?.messageId) || codeAgentLifecycleMessageId(traceSuffix, "agent")
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentLifecycleMessageId(traceSuffix, role) {
|
||||
return `msg_${traceSuffix}_${role}`;
|
||||
}
|
||||
|
||||
function safeTurnId(value) {
|
||||
const text = textValue(value);
|
||||
return /^turn_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
||||
}
|
||||
|
||||
function safeMessageId(value) {
|
||||
const text = textValue(value);
|
||||
return /^msg_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
||||
}
|
||||
|
||||
function codeAgentConversationAgentMessageStatus(payload = {}) {
|
||||
const status = textValue(payload.status ?? payload.agentRun?.terminalStatus ?? payload.commandState ?? payload.runStatus).toLowerCase();
|
||||
if (["completed", "done", "success", "active"].includes(status)) return "completed";
|
||||
@@ -2831,6 +2868,7 @@ function compactCodeAgentChatResultPayload(payload, options = {}) {
|
||||
const { userBillingReservation, ...publicPayload } = payload;
|
||||
return {
|
||||
...publicPayload,
|
||||
...codeAgentTurnLifecycleFields(publicPayload.traceId, publicPayload),
|
||||
...(terminalEvidence ? { terminalEvidence: terminalEvidencePayload(terminalEvidence) } : {}),
|
||||
...(publicPayload.runnerTrace && typeof publicPayload.runnerTrace === "object"
|
||||
? { runnerTrace: compactRunnerTraceForResult(publicPayload.runnerTrace, limit) }
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0.
|
||||
// Responsibility: Workbench client state orchestration for session selection, turn admission, and trace lifecycle rendering.
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { api } from "@/api";
|
||||
@@ -377,6 +380,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return;
|
||||
}
|
||||
void refreshSessionStatusById(sessionId);
|
||||
alignOptimisticTurnMessages(traceId, response.data);
|
||||
applyTurnStatusSnapshot(traceId, response.data);
|
||||
void refreshConversations(conversationId);
|
||||
subscribe(traceId, response.data);
|
||||
@@ -656,6 +660,19 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
messages.value = messages.value.map((message) => message.traceId === traceId && message.role === "agent" ? { ...message, ...patch, updatedAt: new Date().toISOString() } : message);
|
||||
}
|
||||
|
||||
function alignOptimisticTurnMessages(traceId: string, lifecycle: AgentChatResponse): void {
|
||||
const turnId = firstNonEmptyString(lifecycle.turnId, traceId);
|
||||
const userMessageId = firstNonEmptyString(lifecycle.userMessageId);
|
||||
const assistantMessageId = firstNonEmptyString(lifecycle.assistantMessageId);
|
||||
if (!turnId && !userMessageId && !assistantMessageId) return;
|
||||
messages.value = messages.value.map((message) => {
|
||||
if (message.traceId !== traceId) return message;
|
||||
if (message.role === "user" && userMessageId) return { ...message, id: userMessageId, messageId: userMessageId, turnId, updatedAt: new Date().toISOString() };
|
||||
if (message.role === "agent" && assistantMessageId) return { ...message, id: assistantMessageId, messageId: assistantMessageId, turnId, updatedAt: new Date().toISOString() };
|
||||
return turnId ? { ...message, turnId, updatedAt: new Date().toISOString() } : message;
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureWorkspace(): Promise<WorkspaceRecord | null> {
|
||||
if (workspace.value) return workspace.value;
|
||||
await hydrate();
|
||||
@@ -879,7 +896,8 @@ function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
const text = message.role === "agent" && isTerminalMessageStatus(status)
|
||||
? firstNonEmptyString(finalText, errorText, baseText) ?? ""
|
||||
: firstNonEmptyString(baseText, finalText, errorText) ?? "";
|
||||
return { ...message, text, id: message.id ?? nextProtocolId("msg"), title: normalizeWorkbenchMessageTitle(message.role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined };
|
||||
const messageId = firstNonEmptyString((message as Record<string, unknown>).messageId, message.id) ?? nextProtocolId("msg");
|
||||
return { ...message, text, id: messageId, messageId, title: normalizeWorkbenchMessageTitle(message.role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined };
|
||||
}
|
||||
|
||||
function activeTraceIdFromMessages(messages: ChatMessage[], turnStatusAuthority: Record<string, TurnStatusAuthority>): string | null {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0.
|
||||
// Responsibility: Cloud Web API and Workbench state type contracts shared by stores and components.
|
||||
|
||||
export type Tone = "ok" | "pending" | "blocked" | "error" | "warn" | "dev-live" | "dry-run";
|
||||
export type AuthState = "checking" | "login" | "authenticated";
|
||||
export type ProviderProfile = string;
|
||||
@@ -167,6 +170,8 @@ export interface ChatMessage {
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
traceId?: string | null;
|
||||
turnId?: string | null;
|
||||
messageId?: string | null;
|
||||
conversationId?: string | null;
|
||||
sessionId?: string | null;
|
||||
threadId?: string | null;
|
||||
@@ -200,6 +205,9 @@ export interface AgentChatResponse {
|
||||
text?: string;
|
||||
summary?: string;
|
||||
traceId?: string;
|
||||
turnId?: string | null;
|
||||
userMessageId?: string | null;
|
||||
assistantMessageId?: string | null;
|
||||
conversationId?: string;
|
||||
sessionId?: string;
|
||||
threadId?: string;
|
||||
|
||||
Reference in New Issue
Block a user