feat(workbench): integrate operational workflows and map state

This commit is contained in:
2026-09-24 04:34:46 +08:00
parent 1752b9ef84
commit 436d5500f1
76 changed files with 5081 additions and 1937 deletions
+8 -2
View File
@@ -33,7 +33,10 @@ describe("Agent API client sessions", () => {
status: undefined,
parentSessionId: undefined,
isStreaming: undefined,
runStatus: "running"
runStatus: "running",
purpose: "ordinary_chat",
taskId: undefined,
trigger: undefined
},
{
id: "old",
@@ -43,7 +46,10 @@ describe("Agent API client sessions", () => {
status: undefined,
parentSessionId: undefined,
isStreaming: undefined,
runStatus: undefined
runStatus: undefined,
purpose: "ordinary_chat",
taskId: undefined,
trigger: undefined
}
]);
});
+38 -4
View File
@@ -2,6 +2,7 @@ import { env } from "@/shared/config/env";
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
export type AgentRunStatus = "running" | "completed" | "error" | "aborted";
export type AgentSessionPurpose = "ordinary_chat" | "task_execution" | "task_collaboration";
export type AgentEventTriggerContext = {
schema_version: "agent-trigger@1";
@@ -51,9 +52,12 @@ export type AgentChatSession = {
created?: boolean;
event_duplicate?: boolean;
trigger_id?: string;
purpose?: AgentSessionPurpose;
task_id?: string;
};
export type AgentChatSessionSummary = {
trigger?: { subjectId: string; workflow: "water_quality_complaint"; occurredAt: string };
id: string;
title: string;
createdAt: number;
@@ -62,6 +66,8 @@ export type AgentChatSessionSummary = {
parentSessionId?: string;
isStreaming?: boolean;
runStatus?: AgentRunStatus;
purpose: AgentSessionPurpose;
taskId?: string;
};
export type AgentLoadedChatSession = AgentChatSessionSummary & {
@@ -71,6 +77,7 @@ export type AgentLoadedChatSession = AgentChatSessionSummary & {
};
export type AgentSessionStreamDataEventType =
| "execution_plan"
| "token"
| "progress"
| "todo_update"
@@ -90,6 +97,7 @@ export type AgentSessionStreamDataEventType =
export type AgentSessionStreamEvent =
| {
type: "state";
executionPlans?: unknown[];
sessionId: string;
messages: unknown[];
isStreaming: boolean;
@@ -103,7 +111,8 @@ export type AgentSessionStreamEvent =
export type AgentApiClient = {
createSession: (options?: { triggerContext?: AgentEventTriggerContext }) => Promise<AgentChatSession>;
listSessions: () => Promise<AgentChatSessionSummary[]>;
listSessions: (options?: { purpose?: AgentSessionPurpose; taskId?: string }) => Promise<AgentChatSessionSummary[]>;
createTaskCollaborationSession: (taskId: string) => Promise<AgentChatSession>;
loadSession: (sessionId: string) => Promise<AgentLoadedChatSession | null>;
streamSession: (
sessionId: string,
@@ -184,11 +193,23 @@ export function createAgentApiClient(
});
},
async listSessions() {
const payload = await requestJson<{ sessions?: unknown[] }>("/sessions");
async listSessions(listOptions) {
const params = new URLSearchParams();
if (listOptions?.purpose) params.set("purpose", listOptions.purpose);
if (listOptions?.taskId) params.set("task_id", listOptions.taskId);
const payload = await requestJson<{ sessions?: unknown[] }>(
`/sessions${params.size ? `?${params.toString()}` : ""}`
);
return (payload.sessions ?? []).map(toSessionSummary).filter(isPresent).sort(compareSessionSummaries);
},
async createTaskCollaborationSession(taskId) {
return requestJson<AgentChatSession>(
`/task/${encodeURIComponent(taskId)}/collaboration/session`,
{ method: "POST" }
);
},
async getFrontendActionRegistry() {
return requestJson<unknown>("/frontend-action-registry");
},
@@ -432,6 +453,10 @@ function readRunStatus(value: unknown): AgentRunStatus | undefined {
: undefined;
}
function readSessionPurpose(value: unknown): AgentSessionPurpose {
return value === "task_execution" || value === "task_collaboration" ? value : "ordinary_chat";
}
function toSessionSummary(value: unknown): AgentChatSessionSummary | null {
if (!isRecord(value)) {
return null;
@@ -450,7 +475,14 @@ function toSessionSummary(value: unknown): AgentChatSessionSummary | null {
status: typeof value.status === "string" ? value.status : undefined,
parentSessionId: typeof value.parent_session_id === "string" ? value.parent_session_id : undefined,
isStreaming: typeof value.is_streaming === "boolean" ? value.is_streaming : undefined,
runStatus: readRunStatus(value.run_status)
runStatus: readRunStatus(value.run_status),
purpose: readSessionPurpose(value.purpose),
taskId: typeof value.task_id === "string" ? value.task_id : undefined,
trigger: isRecord(value.trigger) && value.trigger.workflow === "water_quality_complaint" &&
typeof value.trigger.subject_id === "string" && typeof value.trigger.occurred_at === "string" &&
Number.isFinite(Date.parse(value.trigger.occurred_at))
? { subjectId: value.trigger.subject_id, workflow: "water_quality_complaint", occurredAt: value.trigger.occurred_at }
: undefined
};
}
@@ -530,6 +562,7 @@ function emitSessionSseEvent(
const sessionId = typeof data.session_id === "string" ? data.session_id : "";
onEvent({
type: "state",
executionPlans: Array.isArray(data.execution_plans) ? data.execution_plans : undefined,
sessionId,
messages: Array.isArray(data.messages) ? data.messages : [],
isStreaming: data.is_streaming === true,
@@ -554,6 +587,7 @@ function isSessionStreamDataEventType(value: string): value is AgentSessionStrea
value === "token" ||
value === "progress" ||
value === "todo_update" ||
value === "execution_plan" ||
value === "permission_request" ||
value === "permission_response" ||
value === "question_request" ||
+4 -2
View File
@@ -35,13 +35,15 @@ describe("Agent API SWR helpers", () => {
id: "session-1",
title: "会话",
createdAt: 1,
updatedAt: 2
updatedAt: 2,
purpose: "ordinary_chat" as const
}
];
const client = {
listSessions: vi.fn(async () => sessions)
} as Pick<AgentApiClient, "listSessions"> as AgentApiClient;
await expect(fetchAgentSessions(client)).resolves.toBe(sessions);
await expect(fetchAgentSessions(client)).resolves.toEqual(sessions);
expect(client.listSessions).toHaveBeenCalledWith({ purpose: "ordinary_chat" });
});
});
+3 -1
View File
@@ -32,5 +32,7 @@ export async function fetchAgentBootstrap(client: AgentApiClient): Promise<Agent
}
export async function fetchAgentSessions(client: AgentApiClient): Promise<AgentChatSessionSummary[]> {
return client.listSessions();
const sessions = await client.listSessions({ purpose: "ordinary_chat" });
// Keep the product boundary even while an older Agent service is rolling forward.
return sessions.filter((session) => session.purpose === "ordinary_chat");
}