feat: expose task execution history for debugging
This commit is contained in:
@@ -54,6 +54,16 @@ const taskExecutionPayloadSchema = z.object({
|
||||
workflow: z.string().trim().min(1).max(120).optional(),
|
||||
});
|
||||
|
||||
const taskExecutionHistoryQuerySchema = z.object({
|
||||
run_id: z.string().uuid(),
|
||||
session_id: z.string().trim().min(1).max(128),
|
||||
});
|
||||
|
||||
const taskExecutionDebugPayloadSchema = z.object({
|
||||
run_id: z.string().uuid(),
|
||||
session_id: z.string().trim().min(1).max(128),
|
||||
}).strict();
|
||||
|
||||
const sessionListQuerySchema = z.object({
|
||||
purpose: z.enum(["ordinary_chat", "task_execution", "task_collaboration"]).optional(),
|
||||
task_id: z.string().uuid().optional(),
|
||||
@@ -240,6 +250,35 @@ export const registerChatSessionRoutes = (
|
||||
}
|
||||
};
|
||||
|
||||
const resolveTaskExecutionHistory = async (
|
||||
projectId: string,
|
||||
credential: ProductCredential,
|
||||
taskId: string,
|
||||
runId: string,
|
||||
sessionId: string,
|
||||
) => {
|
||||
const validationSessionId = `task-history-access-${crypto.randomUUID()}`;
|
||||
let taskTitle: string;
|
||||
bindOperationalTaskCredential(validationSessionId, credential);
|
||||
try {
|
||||
taskTitle = (await operationalTaskClient.getAgentContext(validationSessionId, taskId)).task.title;
|
||||
} finally {
|
||||
releaseOperationalTaskCredential(validationSessionId);
|
||||
clearOperationalTaskLease(validationSessionId);
|
||||
}
|
||||
|
||||
const record = await sessionMetadataStore.getTaskExecutionBinding(
|
||||
projectId,
|
||||
taskId,
|
||||
runId,
|
||||
sessionId,
|
||||
);
|
||||
if (!record) return null;
|
||||
const state = await sessionUiStateStore.read(toSessionUiStateContext(record));
|
||||
if (!state) return null;
|
||||
return { record, state, taskTitle };
|
||||
};
|
||||
|
||||
chatRouter.post("/session", async (req, res) => {
|
||||
const parsed = createSessionPayloadSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
@@ -413,6 +452,152 @@ export const registerChatSessionRoutes = (
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.get("/task/:task_id/execution-session", async (req, res) => {
|
||||
const taskId = z.string().uuid().safeParse(req.params.task_id);
|
||||
const query = taskExecutionHistoryQuerySchema.safeParse(req.query);
|
||||
if (!taskId.success || !query.success) {
|
||||
res.status(400).json({ message: "valid task_id, run_id and session_id are required" });
|
||||
return;
|
||||
}
|
||||
const authContext = getLocalAgentContext(req);
|
||||
const credential = getTrustedProductCredential(req);
|
||||
if (!authContext.projectId) {
|
||||
res.status(403).json({ message: "task execution history requires project context" });
|
||||
return;
|
||||
}
|
||||
if (!credential) {
|
||||
res.status(401).json({ message: "task execution history requires product credentials" });
|
||||
return;
|
||||
}
|
||||
|
||||
let history: Awaited<ReturnType<typeof resolveTaskExecutionHistory>>;
|
||||
try {
|
||||
history = await resolveTaskExecutionHistory(
|
||||
authContext.projectId,
|
||||
credential,
|
||||
taskId.data,
|
||||
query.data.run_id,
|
||||
query.data.session_id,
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : 502).json({
|
||||
message: error instanceof Error ? error.message : "task access validation failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!history) {
|
||||
res.status(404).json({ message: "task execution session not found" });
|
||||
return;
|
||||
}
|
||||
const { record, state, taskTitle } = history;
|
||||
const runStatus = getSessionRunStatus(record.sessionId);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.json({
|
||||
id: record.sessionId,
|
||||
session_id: record.sessionId,
|
||||
title: `${taskTitle} · 执行记录`,
|
||||
created_at: record.createdAt,
|
||||
updated_at: record.updatedAt,
|
||||
status: record.status,
|
||||
messages: await withInteractionReplies(record, state.messages ?? []),
|
||||
execution_plans: state.executionPlans ?? [],
|
||||
is_streaming: runStatus === "running",
|
||||
run_status: runStatus,
|
||||
purpose: "task_execution",
|
||||
task_id: record.taskId,
|
||||
read_only: true,
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.post("/task/:task_id/execution-session/debug", async (req, res) => {
|
||||
const taskId = z.string().uuid().safeParse(req.params.task_id);
|
||||
const payload = taskExecutionDebugPayloadSchema.safeParse(req.body);
|
||||
if (!taskId.success || !payload.success) {
|
||||
res.status(400).json({ message: "valid task_id, run_id and session_id are required" });
|
||||
return;
|
||||
}
|
||||
const authContext = getLocalAgentContext(req);
|
||||
const credential = getTrustedProductCredential(req);
|
||||
if (!authContext.projectId) {
|
||||
res.status(403).json({ message: "task execution debugging requires project context" });
|
||||
return;
|
||||
}
|
||||
if (!credential) {
|
||||
res.status(401).json({ message: "task execution debugging requires product credentials" });
|
||||
return;
|
||||
}
|
||||
|
||||
let history: Awaited<ReturnType<typeof resolveTaskExecutionHistory>>;
|
||||
try {
|
||||
history = await resolveTaskExecutionHistory(
|
||||
authContext.projectId,
|
||||
credential,
|
||||
taskId.data,
|
||||
payload.data.run_id,
|
||||
payload.data.session_id,
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : 502).json({
|
||||
message: error instanceof Error ? error.message : "task access validation failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!history) {
|
||||
res.status(404).json({ message: "task execution session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataContext = {
|
||||
actorKey: authContext.actorKey,
|
||||
projectId: authContext.projectId,
|
||||
projectKey: authContext.projectKey,
|
||||
userId: authContext.userId,
|
||||
};
|
||||
const existing = (await sessionMetadataStore.list(metadataContext, {
|
||||
purpose: "task_collaboration",
|
||||
taskId: taskId.data,
|
||||
})).find((record) => record.parentSessionId === history.record.sessionId);
|
||||
if (existing) {
|
||||
res.json({
|
||||
session_id: existing.sessionId,
|
||||
title: existing.title,
|
||||
purpose: readPurpose(existing),
|
||||
task_id: existing.taskId,
|
||||
parent_session_id: existing.parentSessionId,
|
||||
created: false,
|
||||
status: existing.status,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeSession = await runtime.createSession(undefined, metadataContext);
|
||||
const { record } = await sessionMetadataStore.ensure({
|
||||
...metadataContext,
|
||||
sessionId: runtimeSession.id,
|
||||
parentSessionId: history.record.sessionId,
|
||||
purpose: "task_collaboration",
|
||||
taskId: taskId.data,
|
||||
});
|
||||
const titled = await sessionMetadataStore.touch(record, { title: `${history.taskTitle} · 调试` });
|
||||
const messages = await withInteractionReplies(history.record, history.state.messages ?? []);
|
||||
await sessionUiStateStore.write(
|
||||
toSessionUiStateContext(titled),
|
||||
buildForkedSessionUiState({ messages }, {
|
||||
keepMessageCount: messages.length,
|
||||
targetSessionId: titled.sessionId,
|
||||
}),
|
||||
);
|
||||
res.status(201).json({
|
||||
session_id: titled.sessionId,
|
||||
title: titled.title,
|
||||
purpose: readPurpose(titled),
|
||||
task_id: titled.taskId,
|
||||
parent_session_id: titled.parentSessionId,
|
||||
created: true,
|
||||
status: titled.status,
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.post("/task-executions", async (req, res) => {
|
||||
const parsed = taskExecutionPayloadSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
|
||||
Reference in New Issue
Block a user