feat: 停止失败与重试
This commit is contained in:
parent
3a2c51ed67
commit
4c3b2e48e7
@ -38,10 +38,10 @@
|
|||||||
状态: "已完成"
|
状态: "已完成"
|
||||||
- 编号: "F-005"
|
- 编号: "F-005"
|
||||||
名称: "停止、失败与重试"
|
名称: "停止、失败与重试"
|
||||||
状态: "进行中"
|
状态: "已完成"
|
||||||
- 编号: "F-006"
|
- 编号: "F-006"
|
||||||
名称: "附件、文件列表、读取与搜索"
|
名称: "附件、文件列表、读取与搜索"
|
||||||
状态: "待开始"
|
状态: "进行中"
|
||||||
- 编号: "F-007"
|
- 编号: "F-007"
|
||||||
名称: "文件创建与安全修改"
|
名称: "文件创建与安全修改"
|
||||||
状态: "待开始"
|
状态: "待开始"
|
||||||
|
|||||||
@ -38,6 +38,26 @@ export class RunRegistry {
|
|||||||
return this.paused.has(runId);
|
return this.paused.has(runId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForTerminal(runId: string, timeoutMs = 2_000): Promise<void> {
|
||||||
|
if (this.isFinished(runId)) return;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const finish = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
unsubscribe();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const unsubscribe = this.subscribe(runId, async (event) => {
|
||||||
|
if (
|
||||||
|
event.type === "run.completed" ||
|
||||||
|
event.type === "run.failed" ||
|
||||||
|
event.type === "run.cancelled"
|
||||||
|
)
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
const timer = setTimeout(finish, timeoutMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(runId: string, listener: Listener): () => void {
|
subscribe(runId: string, listener: Listener): () => void {
|
||||||
const values = this.listeners.get(runId) ?? new Set<Listener>();
|
const values = this.listeners.get(runId) ?? new Set<Listener>();
|
||||||
values.add(listener);
|
values.add(listener);
|
||||||
|
|||||||
@ -26,7 +26,8 @@ export function createErrorHandler(logger: Logger): ErrorHandler {
|
|||||||
error.code === "INTERACTION_NOT_FOUND"
|
error.code === "INTERACTION_NOT_FOUND"
|
||||||
? 404
|
? 404
|
||||||
: error.code === "PROJECT_RUN_ACTIVE" ||
|
: error.code === "PROJECT_RUN_ACTIVE" ||
|
||||||
error.code === "INTERACTION_ALREADY_RESOLVED"
|
error.code === "INTERACTION_ALREADY_RESOLVED" ||
|
||||||
|
error.code === "RUN_ALREADY_ACTIVE"
|
||||||
? 409
|
? 409
|
||||||
: 400;
|
: 400;
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|||||||
@ -56,6 +56,7 @@ const model: ModelPort = environment.deepSeekApiKey
|
|||||||
apiKey: environment.deepSeekApiKey,
|
apiKey: environment.deepSeekApiKey,
|
||||||
baseURL: environment.deepSeekBaseUrl,
|
baseURL: environment.deepSeekBaseUrl,
|
||||||
model: environment.deepSeekModel,
|
model: environment.deepSeekModel,
|
||||||
|
timeoutMs: environment.modelTimeoutMs,
|
||||||
})
|
})
|
||||||
: {
|
: {
|
||||||
async *stream() {
|
async *stream() {
|
||||||
@ -97,6 +98,10 @@ logger.info(
|
|||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
hostname: environment.host,
|
hostname: environment.host,
|
||||||
port: environment.port,
|
port: environment.port,
|
||||||
|
idleTimeout: Math.min(
|
||||||
|
255,
|
||||||
|
Math.ceil(environment.modelTimeoutMs / 1_000) + 15,
|
||||||
|
),
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,12 @@ class Runs implements RunRepository {
|
|||||||
async getById(id: string) {
|
async getById(id: string) {
|
||||||
return this.value?.id === id ? this.value : null;
|
return this.value?.id === id ? this.value : null;
|
||||||
}
|
}
|
||||||
|
async findActive() {
|
||||||
|
return this.value?.status === "running" ||
|
||||||
|
this.value?.status === "waiting_user"
|
||||||
|
? this.value
|
||||||
|
: null;
|
||||||
|
}
|
||||||
async appendEvent(event: RunEvent) {
|
async appendEvent(event: RunEvent) {
|
||||||
this.events.push(event);
|
this.events.push(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,9 @@ export function createRunRoutes(
|
|||||||
registry: RunRegistry,
|
registry: RunRegistry,
|
||||||
): Hono {
|
): Hono {
|
||||||
const routes = new Hono();
|
const routes = new Hono();
|
||||||
|
routes.get("/runs/:runId", async (context) =>
|
||||||
|
context.json(await service.getRun(context.req.param("runId"))),
|
||||||
|
);
|
||||||
routes.post("/runs", async (context) => {
|
routes.post("/runs", async (context) => {
|
||||||
const input = startRunRequestSchema.parse(await context.req.json());
|
const input = startRunRequestSchema.parse(await context.req.json());
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -24,13 +27,31 @@ export function createRunRoutes(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
routes.post("/runs/:runId/cancel", async (context) => {
|
routes.post("/runs/:runId/cancel", async (context) => {
|
||||||
const events = await service.cancelWaiting(context.req.param("runId"));
|
const events = await service.cancel(context.req.param("runId"));
|
||||||
for (const event of events) await registry.publish(event);
|
for (const event of events) await registry.publish(event);
|
||||||
|
if (events.length === 0)
|
||||||
|
await registry.waitForTerminal(context.req.param("runId"));
|
||||||
|
const run = await service.getRun(context.req.param("runId"));
|
||||||
return context.json({
|
return context.json({
|
||||||
runId: context.req.param("runId"),
|
runId: context.req.param("runId"),
|
||||||
status: "cancelled" as const,
|
status: run.status,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
routes.post("/runs/:runId/retry", async (context) => {
|
||||||
|
const started = await service.retry(
|
||||||
|
context.req.param("runId"),
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
void consume(started.events, registry);
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
conversationId: started.run.conversationId,
|
||||||
|
runId: started.run.id,
|
||||||
|
status: "running" as const,
|
||||||
|
},
|
||||||
|
202,
|
||||||
|
);
|
||||||
|
});
|
||||||
routes.get("/runs/:runId/events", (context) =>
|
routes.get("/runs/:runId/events", (context) =>
|
||||||
streamSSE(context, async (stream) => {
|
streamSSE(context, async (stream) => {
|
||||||
const runId = context.req.param("runId");
|
const runId = context.req.param("runId");
|
||||||
|
|||||||
@ -30,9 +30,3 @@ export async function answerInteraction(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelWaitingRun(runId: string): Promise<void> {
|
|
||||||
await request(`/api/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import {
|
|||||||
startedRunSchema,
|
startedRunSchema,
|
||||||
type RunEventResponse,
|
type RunEventResponse,
|
||||||
type StartedRunResponse,
|
type StartedRunResponse,
|
||||||
|
agentRunSchema,
|
||||||
|
type AgentRunResponse,
|
||||||
} from "@great-agent/web-contracts";
|
} from "@great-agent/web-contracts";
|
||||||
|
import { request } from "./request";
|
||||||
|
|
||||||
export type StartRunInput =
|
export type StartRunInput =
|
||||||
| { kind: "ordinary"; message: string }
|
| { kind: "ordinary"; message: string }
|
||||||
@ -23,6 +26,26 @@ export async function startRun(
|
|||||||
return startedRunSchema.parse(value);
|
return startedRunSchema.parse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRun(runId: string): Promise<AgentRunResponse> {
|
||||||
|
return agentRunSchema.parse(
|
||||||
|
await request(`/api/runs/${encodeURIComponent(runId)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retryRun(runId: string): Promise<StartedRunResponse> {
|
||||||
|
return startedRunSchema.parse(
|
||||||
|
await request(`/api/runs/${encodeURIComponent(runId)}/retry`, {
|
||||||
|
method: "POST",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelRun(runId: string): Promise<void> {
|
||||||
|
await request(`/api/runs/${encodeURIComponent(runId)}/cancel`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function streamRun(
|
export async function streamRun(
|
||||||
runId: string,
|
runId: string,
|
||||||
onEvent: (event: RunEventResponse) => void,
|
onEvent: (event: RunEventResponse) => void,
|
||||||
|
|||||||
52
apps/web/src/app/app-controller-types.ts
Normal file
52
apps/web/src/app/app-controller-types.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { State } from "vanjs-core";
|
||||||
|
import type {
|
||||||
|
AgentRunResponse,
|
||||||
|
ConversationResponse,
|
||||||
|
ConversationSummaryResponse,
|
||||||
|
InteractionAnswerRequest,
|
||||||
|
InteractionResponse,
|
||||||
|
ProjectResponse,
|
||||||
|
} from "@great-agent/web-contracts";
|
||||||
|
|
||||||
|
export type Selection =
|
||||||
|
| { kind: "none" }
|
||||||
|
| { kind: "ordinary-draft" }
|
||||||
|
| { kind: "project-create" }
|
||||||
|
| { kind: "project-draft"; projectId: string }
|
||||||
|
| { kind: "project-rename"; projectId: string }
|
||||||
|
| { kind: "project-delete"; projectId: string }
|
||||||
|
| { kind: "conversation"; id: string };
|
||||||
|
|
||||||
|
export type AppController = Readonly<{
|
||||||
|
selection: State<Selection>;
|
||||||
|
recent: State<ConversationSummaryResponse[]>;
|
||||||
|
projects: State<ProjectResponse[]>;
|
||||||
|
projectConversations: State<Record<string, ConversationSummaryResponse[]>>;
|
||||||
|
active: State<ConversationResponse | null>;
|
||||||
|
interactions: State<InteractionResponse[]>;
|
||||||
|
currentRun: State<AgentRunResponse | null>;
|
||||||
|
activeRunId: State<string | null>;
|
||||||
|
loading: State<boolean>;
|
||||||
|
sending: State<boolean>;
|
||||||
|
assistantDraft: State<string>;
|
||||||
|
error: State<string>;
|
||||||
|
initialize(): Promise<void>;
|
||||||
|
startNewTask(): void;
|
||||||
|
startProjectCreation(): void;
|
||||||
|
createProject(name: string, workspaceRoot: string): Promise<void>;
|
||||||
|
openProject(id: string): Promise<void>;
|
||||||
|
startProjectTask(id: string): void;
|
||||||
|
startProjectRename(id: string): void;
|
||||||
|
renameProject(id: string, name: string): Promise<void>;
|
||||||
|
startProjectDelete(id: string): void;
|
||||||
|
deleteProject(id: string): Promise<void>;
|
||||||
|
openConversation(id: string): Promise<void>;
|
||||||
|
send(content: string): Promise<boolean>;
|
||||||
|
answerInteraction(
|
||||||
|
id: string,
|
||||||
|
answer: InteractionAnswerRequest,
|
||||||
|
): Promise<void>;
|
||||||
|
cancelInteraction(runId: string): Promise<void>;
|
||||||
|
stopGenerating(): Promise<void>;
|
||||||
|
retryRun(runId: string): Promise<void>;
|
||||||
|
}>;
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import van, { type State } from "vanjs-core";
|
import van from "vanjs-core";
|
||||||
import type {
|
import type {
|
||||||
ConversationResponse,
|
ConversationResponse,
|
||||||
ConversationSummaryResponse,
|
ConversationSummaryResponse,
|
||||||
ProjectResponse,
|
|
||||||
InteractionAnswerRequest,
|
InteractionAnswerRequest,
|
||||||
InteractionResponse,
|
InteractionResponse,
|
||||||
|
AgentRunResponse,
|
||||||
|
ProjectResponse,
|
||||||
} from "@great-agent/web-contracts";
|
} from "@great-agent/web-contracts";
|
||||||
import {
|
import {
|
||||||
getConversation,
|
getConversation,
|
||||||
@ -17,51 +18,19 @@ import {
|
|||||||
listProjects,
|
listProjects,
|
||||||
renameProject,
|
renameProject,
|
||||||
} from "../api/projects";
|
} from "../api/projects";
|
||||||
import { startRun, streamRun } from "../api/runs";
|
import {
|
||||||
|
cancelRun,
|
||||||
|
getRun,
|
||||||
|
retryRun as requestRetryRun,
|
||||||
|
startRun,
|
||||||
|
streamRun,
|
||||||
|
} from "../api/runs";
|
||||||
import {
|
import {
|
||||||
answerInteraction as submitInteractionAnswer,
|
answerInteraction as submitInteractionAnswer,
|
||||||
cancelWaitingRun,
|
|
||||||
listInteractions,
|
listInteractions,
|
||||||
} from "../api/interactions";
|
} from "../api/interactions";
|
||||||
|
import type { AppController, Selection } from "./app-controller-types";
|
||||||
export type Selection =
|
export type { AppController, Selection } from "./app-controller-types";
|
||||||
| { kind: "none" }
|
|
||||||
| { kind: "ordinary-draft" }
|
|
||||||
| { kind: "project-create" }
|
|
||||||
| { kind: "project-draft"; projectId: string }
|
|
||||||
| { kind: "project-rename"; projectId: string }
|
|
||||||
| { kind: "project-delete"; projectId: string }
|
|
||||||
| { kind: "conversation"; id: string };
|
|
||||||
|
|
||||||
export type AppController = Readonly<{
|
|
||||||
selection: State<Selection>;
|
|
||||||
recent: State<ConversationSummaryResponse[]>;
|
|
||||||
projects: State<ProjectResponse[]>;
|
|
||||||
projectConversations: State<Record<string, ConversationSummaryResponse[]>>;
|
|
||||||
active: State<ConversationResponse | null>;
|
|
||||||
interactions: State<InteractionResponse[]>;
|
|
||||||
loading: State<boolean>;
|
|
||||||
sending: State<boolean>;
|
|
||||||
assistantDraft: State<string>;
|
|
||||||
error: State<string>;
|
|
||||||
initialize(): Promise<void>;
|
|
||||||
startNewTask(): void;
|
|
||||||
startProjectCreation(): void;
|
|
||||||
createProject(name: string, workspaceRoot: string): Promise<void>;
|
|
||||||
openProject(id: string): Promise<void>;
|
|
||||||
startProjectTask(id: string): void;
|
|
||||||
startProjectRename(id: string): void;
|
|
||||||
renameProject(id: string, name: string): Promise<void>;
|
|
||||||
startProjectDelete(id: string): void;
|
|
||||||
deleteProject(id: string): Promise<void>;
|
|
||||||
openConversation(id: string): Promise<void>;
|
|
||||||
send(content: string): Promise<void>;
|
|
||||||
answerInteraction(
|
|
||||||
id: string,
|
|
||||||
answer: InteractionAnswerRequest,
|
|
||||||
): Promise<void>;
|
|
||||||
cancelInteraction(runId: string): Promise<void>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export function createAppController(): AppController {
|
export function createAppController(): AppController {
|
||||||
const selection = van.state<Selection>({ kind: "none" });
|
const selection = van.state<Selection>({ kind: "none" });
|
||||||
@ -72,6 +41,8 @@ export function createAppController(): AppController {
|
|||||||
>({});
|
>({});
|
||||||
const active = van.state<ConversationResponse | null>(null);
|
const active = van.state<ConversationResponse | null>(null);
|
||||||
const interactions = van.state<InteractionResponse[]>([]);
|
const interactions = van.state<InteractionResponse[]>([]);
|
||||||
|
const currentRun = van.state<AgentRunResponse | null>(null);
|
||||||
|
const activeRunId = van.state<string | null>(null);
|
||||||
const loading = van.state(false);
|
const loading = van.state(false);
|
||||||
const sending = van.state(false);
|
const sending = van.state(false);
|
||||||
const assistantDraft = van.state("");
|
const assistantDraft = van.state("");
|
||||||
@ -96,6 +67,7 @@ export function createAppController(): AppController {
|
|||||||
selection.val = next;
|
selection.val = next;
|
||||||
active.val = null;
|
active.val = null;
|
||||||
interactions.val = [];
|
interactions.val = [];
|
||||||
|
currentRun.val = null;
|
||||||
error.val = "";
|
error.val = "";
|
||||||
}
|
}
|
||||||
function startNewTask() {
|
function startNewTask() {
|
||||||
@ -163,12 +135,14 @@ export function createAppController(): AppController {
|
|||||||
await perform(async () => {
|
await perform(async () => {
|
||||||
active.val = await getConversation(id);
|
active.val = await getConversation(id);
|
||||||
interactions.val = await listInteractions(id);
|
interactions.val = await listInteractions(id);
|
||||||
|
currentRun.val = await loadLatestRun(active.val);
|
||||||
selection.val = { kind: "conversation", id };
|
selection.val = { kind: "conversation", id };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function send(content: string) {
|
async function send(content: string) {
|
||||||
if (!content.trim() || sending.val) return;
|
if (!content.trim() || sending.val) return false;
|
||||||
|
let accepted = false;
|
||||||
sending.val = true;
|
sending.val = true;
|
||||||
assistantDraft.val = "";
|
assistantDraft.val = "";
|
||||||
error.val = "";
|
error.val = "";
|
||||||
@ -189,16 +163,23 @@ export function createAppController(): AppController {
|
|||||||
}
|
}
|
||||||
: { kind: "ordinary" as const, message: content };
|
: { kind: "ordinary" as const, message: content };
|
||||||
const started = await startRun(input);
|
const started = await startRun(input);
|
||||||
|
accepted = true;
|
||||||
|
activeRunId.val = started.runId;
|
||||||
|
currentRun.val = await getRun(started.runId);
|
||||||
active.val = await getConversation(started.conversationId);
|
active.val = await getConversation(started.conversationId);
|
||||||
selection.val = { kind: "conversation", id: started.conversationId };
|
selection.val = { kind: "conversation", id: started.conversationId };
|
||||||
await refreshLists(active.val.projectId);
|
await refreshLists(active.val.projectId);
|
||||||
await streamRun(started.runId, collectDelta);
|
await streamRun(started.runId, collectEvent);
|
||||||
await refreshActive(started.conversationId);
|
await refreshActive(started.conversationId);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
error.val = readMessage(cause);
|
const message = readMessage(cause);
|
||||||
|
await settleActiveRun();
|
||||||
|
error.val = message;
|
||||||
} finally {
|
} finally {
|
||||||
|
activeRunId.val = null;
|
||||||
sending.val = false;
|
sending.val = false;
|
||||||
}
|
}
|
||||||
|
return accepted;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function answerInteraction(
|
async function answerInteraction(
|
||||||
@ -215,16 +196,21 @@ export function createAppController(): AppController {
|
|||||||
item.id === id ? resolved.interaction : item,
|
item.id === id ? resolved.interaction : item,
|
||||||
);
|
);
|
||||||
if (resolved.resumed) {
|
if (resolved.resumed) {
|
||||||
|
activeRunId.val = resolved.interaction.runId;
|
||||||
|
currentRun.val = await getRun(resolved.interaction.runId);
|
||||||
await streamRun(
|
await streamRun(
|
||||||
resolved.interaction.runId,
|
resolved.interaction.runId,
|
||||||
collectDelta,
|
collectEvent,
|
||||||
resolved.resumeFromSequence,
|
resolved.resumeFromSequence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await refreshActive(resolved.interaction.conversationId);
|
await refreshActive(resolved.interaction.conversationId);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
error.val = readMessage(cause);
|
const message = readMessage(cause);
|
||||||
|
await settleActiveRun();
|
||||||
|
error.val = message;
|
||||||
} finally {
|
} finally {
|
||||||
|
activeRunId.val = null;
|
||||||
sending.val = false;
|
sending.val = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,16 +220,62 @@ export function createAppController(): AppController {
|
|||||||
sending.val = true;
|
sending.val = true;
|
||||||
error.val = "";
|
error.val = "";
|
||||||
try {
|
try {
|
||||||
await cancelWaitingRun(runId);
|
await cancelRun(runId);
|
||||||
if (active.val) interactions.val = await listInteractions(active.val.id);
|
if (active.val) interactions.val = await listInteractions(active.val.id);
|
||||||
|
currentRun.val = await getRun(runId);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
error.val = readMessage(cause);
|
const message = readMessage(cause);
|
||||||
|
await settleActiveRun();
|
||||||
|
error.val = message;
|
||||||
} finally {
|
} finally {
|
||||||
sending.val = false;
|
sending.val = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectDelta(event: {
|
async function stopGenerating() {
|
||||||
|
const runId = activeRunId.val;
|
||||||
|
if (!runId) return;
|
||||||
|
try {
|
||||||
|
await cancelRun(runId);
|
||||||
|
} catch (cause) {
|
||||||
|
error.val = readMessage(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settleActiveRun() {
|
||||||
|
const runId = activeRunId.val;
|
||||||
|
if (!runId) return;
|
||||||
|
try {
|
||||||
|
await cancelRun(runId);
|
||||||
|
currentRun.val = await getRun(runId);
|
||||||
|
if (active.val) await refreshActive(active.val.id);
|
||||||
|
} catch {
|
||||||
|
// Preserve the original stream or model error shown to the user.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryFailedRun(runId: string) {
|
||||||
|
if (sending.val) return;
|
||||||
|
sending.val = true;
|
||||||
|
assistantDraft.val = "";
|
||||||
|
error.val = "";
|
||||||
|
try {
|
||||||
|
const started = await requestRetryRun(runId);
|
||||||
|
activeRunId.val = started.runId;
|
||||||
|
currentRun.val = await getRun(started.runId);
|
||||||
|
await streamRun(started.runId, collectEvent);
|
||||||
|
await refreshActive(started.conversationId);
|
||||||
|
} catch (cause) {
|
||||||
|
const message = readMessage(cause);
|
||||||
|
await settleActiveRun();
|
||||||
|
error.val = message;
|
||||||
|
} finally {
|
||||||
|
activeRunId.val = null;
|
||||||
|
sending.val = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectEvent(event: {
|
||||||
type: string;
|
type: string;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
@ -252,6 +284,19 @@ export function createAppController(): AppController {
|
|||||||
typeof event.payload.delta === "string"
|
typeof event.payload.delta === "string"
|
||||||
)
|
)
|
||||||
assistantDraft.val += event.payload.delta;
|
assistantDraft.val += event.payload.delta;
|
||||||
|
if (currentRun.val && event.type === "run.failed")
|
||||||
|
currentRun.val = {
|
||||||
|
...currentRun.val,
|
||||||
|
status: "failed",
|
||||||
|
errorCode:
|
||||||
|
typeof event.payload.code === "string"
|
||||||
|
? event.payload.code
|
||||||
|
: "MODEL_UNAVAILABLE",
|
||||||
|
};
|
||||||
|
if (currentRun.val && event.type === "run.cancelled")
|
||||||
|
currentRun.val = { ...currentRun.val, status: "cancelled" };
|
||||||
|
if (currentRun.val && event.type === "run.completed")
|
||||||
|
currentRun.val = { ...currentRun.val, status: "completed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshActive(conversationId: string) {
|
async function refreshActive(conversationId: string) {
|
||||||
@ -259,6 +304,7 @@ export function createAppController(): AppController {
|
|||||||
getConversation(conversationId),
|
getConversation(conversationId),
|
||||||
listInteractions(conversationId),
|
listInteractions(conversationId),
|
||||||
]);
|
]);
|
||||||
|
currentRun.val = await loadLatestRun(active.val);
|
||||||
assistantDraft.val = "";
|
assistantDraft.val = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +339,8 @@ export function createAppController(): AppController {
|
|||||||
projectConversations,
|
projectConversations,
|
||||||
active,
|
active,
|
||||||
interactions,
|
interactions,
|
||||||
|
currentRun,
|
||||||
|
activeRunId,
|
||||||
loading,
|
loading,
|
||||||
sending,
|
sending,
|
||||||
assistantDraft,
|
assistantDraft,
|
||||||
@ -311,9 +359,23 @@ export function createAppController(): AppController {
|
|||||||
send,
|
send,
|
||||||
answerInteraction,
|
answerInteraction,
|
||||||
cancelInteraction,
|
cancelInteraction,
|
||||||
|
stopGenerating,
|
||||||
|
retryRun: retryFailedRun,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMessage(cause: unknown): string {
|
function readMessage(cause: unknown): string {
|
||||||
return cause instanceof Error ? cause.message : "操作失败,请重试";
|
return cause instanceof Error ? cause.message : "操作失败,请重试";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLatestRun(
|
||||||
|
conversation: ConversationResponse | null,
|
||||||
|
): Promise<AgentRunResponse | null> {
|
||||||
|
const runId = conversation?.messages.at(-1)?.runId;
|
||||||
|
if (!runId) return null;
|
||||||
|
try {
|
||||||
|
return await getRun(runId);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { AppController } from "../../app/app-controller";
|
|||||||
import "./conversation-pane.css";
|
import "./conversation-pane.css";
|
||||||
import { ProjectPanel } from "../projects/project-panel";
|
import { ProjectPanel } from "../projects/project-panel";
|
||||||
import { InteractionCard } from "../interactions/interaction-card";
|
import { InteractionCard } from "../interactions/interaction-card";
|
||||||
|
import { RunStatus } from "../runs/run-status";
|
||||||
|
|
||||||
const { article, button, div, h1, header, main, p, span, textarea } = van.tags;
|
const { article, button, div, h1, header, main, p, span, textarea } = van.tags;
|
||||||
|
|
||||||
@ -12,8 +13,7 @@ export function ConversationPane(controller: AppController): HTMLElement {
|
|||||||
async function submit() {
|
async function submit() {
|
||||||
const content = draft.val;
|
const content = draft.val;
|
||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
await controller.send(content);
|
if (await controller.send(content)) draft.val = "";
|
||||||
draft.val = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return main(
|
return main(
|
||||||
@ -58,6 +58,7 @@ export function ConversationPane(controller: AppController): HTMLElement {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
() => RunStatus(controller),
|
||||||
)
|
)
|
||||||
: div(
|
: div(
|
||||||
{ class: "onboarding" },
|
{ class: "onboarding" },
|
||||||
@ -113,11 +114,15 @@ function Composer(
|
|||||||
button(
|
button(
|
||||||
{
|
{
|
||||||
class: "send",
|
class: "send",
|
||||||
disabled: () => controller.sending.val || !draft.val.trim(),
|
disabled: () => !controller.sending.val && !draft.val.trim(),
|
||||||
onclick: () => void submit(),
|
onclick: () =>
|
||||||
"aria-label": "发送消息",
|
controller.sending.val
|
||||||
|
? void controller.stopGenerating()
|
||||||
|
: void submit(),
|
||||||
|
"aria-label": () =>
|
||||||
|
controller.sending.val ? "停止生成" : "发送消息",
|
||||||
},
|
},
|
||||||
() => (controller.sending.val ? "…" : "↑"),
|
() => (controller.sending.val ? "■" : "↑"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
51
apps/web/src/features/runs/run-status.css
Normal file
51
apps/web/src/features/runs/run-status.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.run-status {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 26px 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
margin: 22px 0 0 37px;
|
||||||
|
padding: 12px 13px;
|
||||||
|
border: 1px solid #67443d;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #302522;
|
||||||
|
}
|
||||||
|
.run-status.cancelled {
|
||||||
|
border-color: #494641;
|
||||||
|
background: #282725;
|
||||||
|
}
|
||||||
|
.run-status-mark {
|
||||||
|
display: grid;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #e6a092;
|
||||||
|
background: #5a302a;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.run-status.cancelled .run-status-mark {
|
||||||
|
color: #aaa59d;
|
||||||
|
background: #403e3a;
|
||||||
|
}
|
||||||
|
.run-status p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.run-status-title {
|
||||||
|
color: #e0dcd5;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.run-status .run-status-detail {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #918c85;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.run-status button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #ddd8d0;
|
||||||
|
background: #45423e;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
42
apps/web/src/features/runs/run-status.ts
Normal file
42
apps/web/src/features/runs/run-status.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import van from "vanjs-core";
|
||||||
|
import type { AppController } from "../../app/app-controller";
|
||||||
|
import "./run-status.css";
|
||||||
|
|
||||||
|
const { button, div, p, span } = van.tags;
|
||||||
|
|
||||||
|
export function RunStatus(controller: AppController): HTMLElement | null {
|
||||||
|
const run = controller.currentRun.val;
|
||||||
|
if (!run || (run.status !== "failed" && run.status !== "cancelled"))
|
||||||
|
return null;
|
||||||
|
const failed = run.status === "failed";
|
||||||
|
return div(
|
||||||
|
{ class: `run-status ${run.status}` },
|
||||||
|
span({ class: "run-status-mark" }, failed ? "!" : "■"),
|
||||||
|
div(
|
||||||
|
p(
|
||||||
|
{ class: "run-status-title" },
|
||||||
|
failed ? errorTitle(run.errorCode) : "已停止生成",
|
||||||
|
),
|
||||||
|
p(
|
||||||
|
{ class: "run-status-detail" },
|
||||||
|
failed
|
||||||
|
? "本次任务没有成功完成,可以使用同一条用户消息重试。"
|
||||||
|
: "已保留停止前成功生成的内容。",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
button(
|
||||||
|
{
|
||||||
|
disabled: controller.sending,
|
||||||
|
onclick: () => void controller.retryRun(run.id),
|
||||||
|
},
|
||||||
|
"重试",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorTitle(code?: string): string {
|
||||||
|
if (code === "MODEL_TIMEOUT") return "模型响应超时";
|
||||||
|
if (code === "MODEL_CONFIG_MISSING") return "模型尚未配置";
|
||||||
|
if (code === "MODEL_RESPONSE_INVALID") return "模型没有返回有效内容";
|
||||||
|
return "任务运行失败";
|
||||||
|
}
|
||||||
@ -67,13 +67,18 @@
|
|||||||
|
|
||||||
### F-005——停止、失败与重试
|
### F-005——停止、失败与重试
|
||||||
|
|
||||||
- 状态:进行中
|
- 状态:已完成(Agent 于 2026-08-14 验证通过,等待最终统一人工审核)
|
||||||
- 用户可见结果:停止当前生成并从失败状态重试。
|
- 用户可见结果:停止当前生成并从失败状态重试。
|
||||||
- 主要验收:AC-005、AC-010、AC-011。
|
- 主要验收:AC-005、AC-010、AC-011。
|
||||||
|
- 实现结果:Agent Core 持有每个活动 Run 的取消控制器,停止操作真正中断 ModelPort;停止前已生成内容持久化,Run 和事件日志进入 `cancelled` 终态。失败或取消后创建带 `retryOfRunId` 的新 Run,复用原触发消息且不重复写入用户消息。Repository 与串行启动锁共同执行全局单活动 Run 检查,第二个任务返回 `RUN_ALREADY_ACTIVE`。
|
||||||
|
- 失败处理:DeepSeek 请求增加与环境配置一致的模型超时并映射 `MODEL_TIMEOUT`;Bun 服务空闲超时与模型超时对齐;流式连接提前中断时前端结束加载并主动取消仍在运行的后台任务。
|
||||||
|
- 页面结果:生成时发送按钮切换为“停止生成”;失败和取消显示明确状态及“重试”按钮;API 启动失败时保留输入草稿。
|
||||||
|
- 自动化验证结果:2026-08-14 通过全仓类型检查、24 项测试和 84 个断言、生产构建、代码检查、格式检查、文件规模检查与架构依赖检查。
|
||||||
|
- 人工操作结果:2026-08-14 使用隔离慢速 Fake Model 验证停止按钮、部分回复保留、取消终态和重试成功;并通过两个连续 HTTP 请求确认活动任务期间第二个请求稳定返回 `409 RUN_ALREADY_ACTIVE`;页面控制台无错误。
|
||||||
|
|
||||||
### F-006——附件、文件列表、读取与搜索
|
### F-006——附件、文件列表、读取与搜索
|
||||||
|
|
||||||
- 状态:待开始
|
- 状态:进行中
|
||||||
- 用户可见结果:附加工作区文件并让 Agent 安全读取和搜索。
|
- 用户可见结果:附加工作区文件并让 Agent 安全读取和搜索。
|
||||||
- 主要验收:AC-007 至 AC-009、AC-021、AC-022、AC-028、AC-030。
|
- 主要验收:AC-007 至 AC-009、AC-021、AC-022、AC-028、AC-030。
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type AgentRun = Readonly<{
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
|
retryOfRunId?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type RunEvent = Readonly<{
|
export type RunEvent = Readonly<{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export interface RunRepository {
|
|||||||
create(run: AgentRun): Promise<void>;
|
create(run: AgentRun): Promise<void>;
|
||||||
update(run: AgentRun): Promise<void>;
|
update(run: AgentRun): Promise<void>;
|
||||||
getById(id: string): Promise<AgentRun | null>;
|
getById(id: string): Promise<AgentRun | null>;
|
||||||
|
findActive(): Promise<AgentRun | null>;
|
||||||
appendEvent(event: RunEvent): Promise<void>;
|
appendEvent(event: RunEvent): Promise<void>;
|
||||||
listEvents(runId: string): Promise<readonly RunEvent[]>;
|
listEvents(runId: string): Promise<readonly RunEvent[]>;
|
||||||
hasActiveForConversations(
|
hasActiveForConversations(
|
||||||
|
|||||||
@ -56,15 +56,25 @@ class MemoryConversationRepository implements ConversationRepository {
|
|||||||
|
|
||||||
class MemoryRunRepository implements RunRepository {
|
class MemoryRunRepository implements RunRepository {
|
||||||
value: AgentRun | null = null;
|
value: AgentRun | null = null;
|
||||||
|
readonly values = new Map<string, AgentRun>();
|
||||||
readonly events: RunEvent[] = [];
|
readonly events: RunEvent[] = [];
|
||||||
async create(run: AgentRun) {
|
async create(run: AgentRun) {
|
||||||
this.value = run;
|
this.value = run;
|
||||||
|
this.values.set(run.id, run);
|
||||||
}
|
}
|
||||||
async update(run: AgentRun) {
|
async update(run: AgentRun) {
|
||||||
this.value = run;
|
this.value = run;
|
||||||
|
this.values.set(run.id, run);
|
||||||
}
|
}
|
||||||
async getById(id: string) {
|
async getById(id: string) {
|
||||||
return this.value?.id === id ? this.value : null;
|
return this.values.get(id) ?? null;
|
||||||
|
}
|
||||||
|
async findActive() {
|
||||||
|
return (
|
||||||
|
[...this.values.values()].find(
|
||||||
|
(run) => run.status === "running" || run.status === "waiting_user",
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async appendEvent(event: RunEvent) {
|
async appendEvent(event: RunEvent) {
|
||||||
this.events.push(event);
|
this.events.push(event);
|
||||||
@ -170,4 +180,117 @@ describe("AgentRunService", () => {
|
|||||||
.messages[0]?.id,
|
.messages[0]?.id,
|
||||||
).toBe(started.run.triggerMessageId);
|
).toBe(started.run.triggerMessageId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("停止生成会中断模型并保存部分内容和取消终态", async () => {
|
||||||
|
const fixture = createFixture({
|
||||||
|
async *stream(_request, signal) {
|
||||||
|
yield { type: "text.delta", delta: "已经生成的部分" };
|
||||||
|
if (!signal.aborted)
|
||||||
|
await new Promise<void>((resolve) =>
|
||||||
|
signal.addEventListener("abort", () => resolve(), { once: true }),
|
||||||
|
);
|
||||||
|
throw signal.reason;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const started = await fixture.service.start(
|
||||||
|
{ kind: "ordinary", message: "开始长任务" },
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
const events: RunEvent[] = [];
|
||||||
|
const consuming = (async () => {
|
||||||
|
for await (const event of started.events) {
|
||||||
|
events.push(event);
|
||||||
|
if (event.type === "message.delta")
|
||||||
|
await fixture.service.cancel(started.run.id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
await consuming;
|
||||||
|
expect(events.at(-1)?.type).toBe("run.cancelled");
|
||||||
|
expect(fixture.runs.value?.status).toBe("cancelled");
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await fixture.conversationService.getConversation(
|
||||||
|
started.run.conversationId,
|
||||||
|
)
|
||||||
|
).messages.at(-1)?.content,
|
||||||
|
).toBe("已经生成的部分");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("全局已有活动任务时拒绝创建第二个 Run", async () => {
|
||||||
|
const fixture = createFixture({
|
||||||
|
async *stream() {
|
||||||
|
yield { type: "text.delta", delta: "完成" };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const first = await fixture.service.start(
|
||||||
|
{ kind: "ordinary", message: "第一个任务" },
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
fixture.service.start(
|
||||||
|
{ kind: "ordinary", message: "第二个任务" },
|
||||||
|
new AbortController().signal,
|
||||||
|
),
|
||||||
|
).rejects.toMatchObject({ code: "RUN_ALREADY_ACTIVE" });
|
||||||
|
expect(fixture.conversations.values.size).toBe(1);
|
||||||
|
await fixture.service.cancel(first.run.id);
|
||||||
|
for await (const _event of first.events) {
|
||||||
|
// Drain the cancelled execution so it reaches a terminal state.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("失败后重试关联原 Run 且不重复用户消息", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const fixture = createFixture({
|
||||||
|
async *stream() {
|
||||||
|
calls += 1;
|
||||||
|
if (calls === 1) throw new Error("temporary");
|
||||||
|
yield { type: "text.delta", delta: "重试成功" };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const first = await fixture.service.start(
|
||||||
|
{ kind: "ordinary", message: "只保存一次" },
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
for await (const _event of first.events) {
|
||||||
|
// Complete the failed attempt.
|
||||||
|
}
|
||||||
|
const retried = await fixture.service.retry(
|
||||||
|
first.run.id,
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
for await (const _event of retried.events) {
|
||||||
|
// Complete the retry.
|
||||||
|
}
|
||||||
|
const conversation = await fixture.conversationService.getConversation(
|
||||||
|
first.run.conversationId,
|
||||||
|
);
|
||||||
|
expect(retried.run.retryOfRunId).toBe(first.run.id);
|
||||||
|
expect(retried.run.triggerMessageId).toBe(first.run.triggerMessageId);
|
||||||
|
expect(
|
||||||
|
conversation.messages.filter((message) => message.role === "user"),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(conversation.messages.at(-1)?.content).toBe("重试成功");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createFixture(model: ModelPort) {
|
||||||
|
let id = 0;
|
||||||
|
const conversations = new MemoryConversationRepository();
|
||||||
|
const runs = new MemoryRunRepository();
|
||||||
|
const conversationService = new ConversationService({
|
||||||
|
conversations,
|
||||||
|
clock: { now: () => new Date("2026-08-14T00:00:00Z") },
|
||||||
|
ids: { create: () => `fixture_${++id}` },
|
||||||
|
});
|
||||||
|
const service = new AgentRunService(
|
||||||
|
conversationService,
|
||||||
|
model,
|
||||||
|
runs,
|
||||||
|
{ now: () => new Date("2026-08-14T00:00:00Z") },
|
||||||
|
{ create: () => `fixture_${++id}` },
|
||||||
|
projects,
|
||||||
|
interactions,
|
||||||
|
);
|
||||||
|
return { service, runs, conversations, conversationService };
|
||||||
|
}
|
||||||
|
|||||||
@ -2,54 +2,25 @@ import type { AgentRun, RunEvent } from "../domain/agent-run";
|
|||||||
import type { Conversation } from "../domain/conversation";
|
import type { Conversation } from "../domain/conversation";
|
||||||
import type { UserInteraction } from "../domain/user-interaction";
|
import type { UserInteraction } from "../domain/user-interaction";
|
||||||
import { CoreError } from "../errors/core-error";
|
import { CoreError } from "../errors/core-error";
|
||||||
import type { ModelPort, ModelRequest, ModelTool } from "../ports/model-port";
|
import type { ModelPort, ModelRequest } from "../ports/model-port";
|
||||||
import type { RunRepository } from "../ports/run-repository";
|
import type { RunRepository } from "../ports/run-repository";
|
||||||
import type { ClockPort, IdPort } from "../ports/system-ports";
|
import type { ClockPort, IdPort } from "../ports/system-ports";
|
||||||
import type { ConversationService } from "./conversation-service";
|
import type { ConversationService } from "./conversation-service";
|
||||||
import type { InteractionService } from "./interaction-service";
|
import type { InteractionService } from "./interaction-service";
|
||||||
import type { ProjectService } from "./project-service";
|
import type { ProjectService } from "./project-service";
|
||||||
|
import { interactionTool } from "./interaction-tool";
|
||||||
export type StartRunInput =
|
import {
|
||||||
| Readonly<{ kind: "ordinary"; message: string }>
|
lastSequence,
|
||||||
| Readonly<{ kind: "project"; projectId: string; message: string }>
|
messagesThroughTrigger,
|
||||||
| Readonly<{ kind: "existing"; conversationId: string; message: string }>;
|
safeErrorMessage,
|
||||||
|
} from "./run-service-helpers";
|
||||||
export type StartedRun = Readonly<{
|
import type { StartedRun, StartRunInput } from "./run-types";
|
||||||
run: AgentRun;
|
export type { StartedRun, StartRunInput } from "./run-types";
|
||||||
events: AsyncIterable<RunEvent>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const interactionTool: ModelTool = {
|
|
||||||
name: "request_user_interaction",
|
|
||||||
description: "需要用户选择、确认或补充意见时调用。调用后等待用户回答。",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
required: ["kind", "question"],
|
|
||||||
properties: {
|
|
||||||
kind: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["single_choice", "multiple_choice", "confirmation", "free_text"],
|
|
||||||
},
|
|
||||||
question: { type: "string" },
|
|
||||||
description: { type: "string" },
|
|
||||||
required: { type: "boolean" },
|
|
||||||
options: {
|
|
||||||
type: "array",
|
|
||||||
minItems: 2,
|
|
||||||
maxItems: 10,
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
required: ["id", "label"],
|
|
||||||
properties: { id: { type: "string" }, label: { type: "string" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
minSelections: { type: "integer", minimum: 0 },
|
|
||||||
maxSelections: { type: "integer", minimum: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AgentRunService {
|
export class AgentRunService {
|
||||||
|
private readonly controllers = new Map<string, AbortController>();
|
||||||
|
private startQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly conversations: ConversationService,
|
private readonly conversations: ConversationService,
|
||||||
private readonly model: ModelPort,
|
private readonly model: ModelPort,
|
||||||
@ -61,22 +32,28 @@ export class AgentRunService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async start(input: StartRunInput, signal: AbortSignal): Promise<StartedRun> {
|
async start(input: StartRunInput, signal: AbortSignal): Promise<StartedRun> {
|
||||||
const runId = this.ids.create();
|
return this.withStartLock(async () => {
|
||||||
const conversation = await this.prepareConversation(input, runId);
|
await this.ensureNoActiveRun();
|
||||||
const timestamp = this.clock.now().toISOString();
|
const runId = this.ids.create();
|
||||||
const triggerMessage = conversation.messages.at(-1);
|
const conversation = await this.prepareConversation(input, runId);
|
||||||
if (triggerMessage?.role !== "user")
|
const timestamp = this.clock.now().toISOString();
|
||||||
throw new CoreError("RUN_TRIGGER_INVALID", "无法确定本次运行的用户消息");
|
const triggerMessage = conversation.messages.at(-1);
|
||||||
const run: AgentRun = {
|
if (triggerMessage?.role !== "user")
|
||||||
id: runId,
|
throw new CoreError(
|
||||||
conversationId: conversation.id,
|
"RUN_TRIGGER_INVALID",
|
||||||
triggerMessageId: triggerMessage.id,
|
"无法确定本次运行的用户消息",
|
||||||
status: "running",
|
);
|
||||||
createdAt: timestamp,
|
const run: AgentRun = {
|
||||||
updatedAt: timestamp,
|
id: runId,
|
||||||
};
|
conversationId: conversation.id,
|
||||||
await this.runs.create(run);
|
triggerMessageId: triggerMessage.id,
|
||||||
return { run, events: this.execute(run, signal, 0) };
|
status: "running",
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
await this.runs.create(run);
|
||||||
|
return this.started(run, signal, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resume(
|
async resume(
|
||||||
@ -101,17 +78,27 @@ export class AgentRunService {
|
|||||||
toolArguments: interaction.toolArguments,
|
toolArguments: interaction.toolArguments,
|
||||||
result: JSON.stringify(interaction.answer),
|
result: JSON.stringify(interaction.answer),
|
||||||
};
|
};
|
||||||
return {
|
return this.started(run, signal, sequence, continuation, interaction);
|
||||||
run,
|
|
||||||
events: this.execute(run, signal, sequence, continuation, interaction),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelWaiting(runId: string): Promise<readonly RunEvent[]> {
|
async cancel(runId: string): Promise<readonly RunEvent[]> {
|
||||||
const run = await this.requireRun(runId);
|
const run = await this.requireRun(runId);
|
||||||
if (run.status === "cancelled") return [];
|
if (
|
||||||
if (run.status !== "waiting_user")
|
run.status === "cancelled" ||
|
||||||
throw new CoreError("RUN_NOT_WAITING", "该任务当前不在等待用户回答");
|
run.status === "completed" ||
|
||||||
|
run.status === "failed"
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
if (run.status === "running") {
|
||||||
|
const controller = this.controllers.get(runId);
|
||||||
|
if (!controller)
|
||||||
|
throw new CoreError(
|
||||||
|
"RUN_NOT_CANCELLABLE",
|
||||||
|
"任务正在恢复中,请稍后重试",
|
||||||
|
);
|
||||||
|
controller.abort();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const interaction = await this.interactions.cancelPending(runId);
|
const interaction = await this.interactions.cancelPending(runId);
|
||||||
if (!interaction)
|
if (!interaction)
|
||||||
throw new CoreError("INTERACTION_NOT_FOUND", "等待中的交互请求不存在");
|
throw new CoreError("INTERACTION_NOT_FOUND", "等待中的交互请求不存在");
|
||||||
@ -136,6 +123,38 @@ export class AgentRunService {
|
|||||||
return [cancelled, finished];
|
return [cancelled, finished];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async retry(runId: string, signal: AbortSignal): Promise<StartedRun> {
|
||||||
|
return this.withStartLock(async () => {
|
||||||
|
await this.ensureNoActiveRun();
|
||||||
|
const original = await this.requireRun(runId);
|
||||||
|
if (original.status !== "failed" && original.status !== "cancelled")
|
||||||
|
throw new CoreError(
|
||||||
|
"RUN_NOT_RETRYABLE",
|
||||||
|
"只有失败或已取消的任务可以重试",
|
||||||
|
);
|
||||||
|
const conversation = await this.conversations.getConversation(
|
||||||
|
original.conversationId,
|
||||||
|
);
|
||||||
|
const trigger = conversation.messages.find(
|
||||||
|
(message) => message.id === original.triggerMessageId,
|
||||||
|
);
|
||||||
|
if (trigger?.role !== "user")
|
||||||
|
throw new CoreError("RUN_TRIGGER_INVALID", "原任务的用户消息不存在");
|
||||||
|
const timestamp = this.clock.now().toISOString();
|
||||||
|
const run: AgentRun = {
|
||||||
|
id: this.ids.create(),
|
||||||
|
conversationId: original.conversationId,
|
||||||
|
triggerMessageId: original.triggerMessageId,
|
||||||
|
retryOfRunId: original.id,
|
||||||
|
status: "running",
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
await this.runs.create(run);
|
||||||
|
return this.started(run, signal, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async lastSequence(runId: string): Promise<number> {
|
async lastSequence(runId: string): Promise<number> {
|
||||||
await this.requireRun(runId);
|
await this.requireRun(runId);
|
||||||
return lastSequence(await this.runs.listEvents(runId));
|
return lastSequence(await this.runs.listEvents(runId));
|
||||||
@ -145,6 +164,45 @@ export class AgentRunService {
|
|||||||
return this.requireRun(runId);
|
return this.requireRun(runId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private started(
|
||||||
|
run: AgentRun,
|
||||||
|
externalSignal: AbortSignal,
|
||||||
|
sequence: number,
|
||||||
|
continuation?: NonNullable<ModelRequest["continuation"]>,
|
||||||
|
interaction?: UserInteraction,
|
||||||
|
): StartedRun {
|
||||||
|
const controller = new AbortController();
|
||||||
|
this.controllers.set(run.id, controller);
|
||||||
|
const signal = AbortSignal.any([externalSignal, controller.signal]);
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
events: this.execute(run, signal, sequence, continuation, interaction),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureNoActiveRun(): Promise<void> {
|
||||||
|
const active = await this.runs.findActive();
|
||||||
|
if (active)
|
||||||
|
throw new CoreError(
|
||||||
|
"RUN_ALREADY_ACTIVE",
|
||||||
|
"已有任务正在运行,请先停止或完成当前任务",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async withStartLock<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
const previous = this.startQueue;
|
||||||
|
let release = () => {};
|
||||||
|
this.startQueue = new Promise<void>((resolve) => {
|
||||||
|
release = resolve;
|
||||||
|
});
|
||||||
|
await previous;
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async prepareConversation(
|
private async prepareConversation(
|
||||||
input: StartRunInput,
|
input: StartRunInput,
|
||||||
runId: string,
|
runId: string,
|
||||||
@ -179,6 +237,9 @@ export class AgentRunService {
|
|||||||
resolvedInteraction?: UserInteraction,
|
resolvedInteraction?: UserInteraction,
|
||||||
): AsyncIterable<RunEvent> {
|
): AsyncIterable<RunEvent> {
|
||||||
let sequence = initialSequence;
|
let sequence = initialSequence;
|
||||||
|
let messageId: string | undefined;
|
||||||
|
let content = "";
|
||||||
|
let messagePersisted = false;
|
||||||
const event = async (
|
const event = async (
|
||||||
type: RunEvent["type"],
|
type: RunEvent["type"],
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
@ -193,21 +254,25 @@ export class AgentRunService {
|
|||||||
interactionId: resolvedInteraction.id,
|
interactionId: resolvedInteraction.id,
|
||||||
answer: resolvedInteraction.answer,
|
answer: resolvedInteraction.answer,
|
||||||
});
|
});
|
||||||
const messageId = this.ids.create();
|
messageId = this.ids.create();
|
||||||
yield await event("message.started", { messageId });
|
yield await event("message.started", { messageId });
|
||||||
const conversation = await this.conversations.getConversation(
|
const conversation = await this.conversations.getConversation(
|
||||||
run.conversationId,
|
run.conversationId,
|
||||||
);
|
);
|
||||||
let content = "";
|
const modelMessages = run.retryOfRunId
|
||||||
|
? messagesThroughTrigger(conversation, run.triggerMessageId)
|
||||||
|
: conversation.messages;
|
||||||
|
signal.throwIfAborted();
|
||||||
for await (const modelEvent of this.model.stream(
|
for await (const modelEvent of this.model.stream(
|
||||||
{
|
{
|
||||||
model: "default",
|
model: "default",
|
||||||
messages: conversation.messages,
|
messages: modelMessages,
|
||||||
tools: [interactionTool],
|
tools: [interactionTool],
|
||||||
...(continuation ? { continuation } : {}),
|
...(continuation ? { continuation } : {}),
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
)) {
|
)) {
|
||||||
|
signal.throwIfAborted();
|
||||||
if (modelEvent.type === "text.delta") {
|
if (modelEvent.type === "text.delta") {
|
||||||
content += modelEvent.delta;
|
content += modelEvent.delta;
|
||||||
yield await event("message.delta", {
|
yield await event("message.delta", {
|
||||||
@ -228,6 +293,7 @@ export class AgentRunService {
|
|||||||
run.id,
|
run.id,
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
messagePersisted = true;
|
||||||
yield await event("message.completed", { messageId, content });
|
yield await event("message.completed", { messageId, content });
|
||||||
const interaction = await this.interactions.create({
|
const interaction = await this.interactions.create({
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
@ -245,6 +311,7 @@ export class AgentRunService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
signal.throwIfAborted();
|
||||||
if (!content)
|
if (!content)
|
||||||
throw new CoreError("MODEL_RESPONSE_INVALID", "模型没有返回有效内容");
|
throw new CoreError("MODEL_RESPONSE_INVALID", "模型没有返回有效内容");
|
||||||
await this.conversations.appendAssistantMessage(
|
await this.conversations.appendAssistantMessage(
|
||||||
@ -253,6 +320,7 @@ export class AgentRunService {
|
|||||||
run.id,
|
run.id,
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
messagePersisted = true;
|
||||||
yield await event("message.completed", { messageId, content });
|
yield await event("message.completed", { messageId, content });
|
||||||
await this.runs.update({
|
await this.runs.update({
|
||||||
...run,
|
...run,
|
||||||
@ -261,6 +329,28 @@ export class AgentRunService {
|
|||||||
});
|
});
|
||||||
yield await event("run.completed", {});
|
yield await event("run.completed", {});
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
if (content && messageId && !messagePersisted) {
|
||||||
|
await this.conversations.appendAssistantMessage(
|
||||||
|
run.conversationId,
|
||||||
|
messageId,
|
||||||
|
run.id,
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
yield await event("message.completed", {
|
||||||
|
messageId,
|
||||||
|
content,
|
||||||
|
cancelled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.runs.update({
|
||||||
|
...run,
|
||||||
|
status: "cancelled",
|
||||||
|
updatedAt: this.clock.now().toISOString(),
|
||||||
|
});
|
||||||
|
yield await event("run.cancelled", {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const code =
|
const code =
|
||||||
cause instanceof CoreError ? cause.code : "MODEL_UNAVAILABLE";
|
cause instanceof CoreError ? cause.code : "MODEL_UNAVAILABLE";
|
||||||
await this.runs.update({
|
await this.runs.update({
|
||||||
@ -273,6 +363,8 @@ export class AgentRunService {
|
|||||||
code,
|
code,
|
||||||
message: safeErrorMessage(cause),
|
message: safeErrorMessage(cause),
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
this.controllers.delete(run.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,10 +391,3 @@ export class AgentRunService {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastSequence(events: readonly RunEvent[]): number {
|
|
||||||
return events.at(-1)?.sequence ?? 0;
|
|
||||||
}
|
|
||||||
function safeErrorMessage(cause: unknown): string {
|
|
||||||
return cause instanceof CoreError ? cause.message : "模型服务暂时不可用";
|
|
||||||
}
|
|
||||||
|
|||||||
@ -53,6 +53,13 @@ class Runs implements RunRepository {
|
|||||||
async getById(id: string) {
|
async getById(id: string) {
|
||||||
return this.values.get(id) ?? null;
|
return this.values.get(id) ?? null;
|
||||||
}
|
}
|
||||||
|
async findActive() {
|
||||||
|
return (
|
||||||
|
[...this.values.values()].find(
|
||||||
|
(run) => run.status === "running" || run.status === "waiting_user",
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
async appendEvent(event: RunEvent) {
|
async appendEvent(event: RunEvent) {
|
||||||
this.events.push(event);
|
this.events.push(event);
|
||||||
}
|
}
|
||||||
@ -135,7 +142,7 @@ describe("等待用户的 Run", () => {
|
|||||||
new AbortController().signal,
|
new AbortController().signal,
|
||||||
);
|
);
|
||||||
await collect(started.events);
|
await collect(started.events);
|
||||||
const events = await fixture.service.cancelWaiting(started.run.id);
|
const events = await fixture.service.cancel(started.run.id);
|
||||||
expect(events.map((event) => event.type)).toEqual([
|
expect(events.map((event) => event.type)).toEqual([
|
||||||
"interaction.cancelled",
|
"interaction.cancelled",
|
||||||
"run.cancelled",
|
"run.cancelled",
|
||||||
|
|||||||
31
packages/agent-core/src/use-cases/interaction-tool.ts
Normal file
31
packages/agent-core/src/use-cases/interaction-tool.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { ModelTool } from "../ports/model-port";
|
||||||
|
|
||||||
|
export const interactionTool: ModelTool = {
|
||||||
|
name: "request_user_interaction",
|
||||||
|
description: "需要用户选择、确认或补充意见时调用。调用后等待用户回答。",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
required: ["kind", "question"],
|
||||||
|
properties: {
|
||||||
|
kind: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["single_choice", "multiple_choice", "confirmation", "free_text"],
|
||||||
|
},
|
||||||
|
question: { type: "string" },
|
||||||
|
description: { type: "string" },
|
||||||
|
required: { type: "boolean" },
|
||||||
|
options: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 2,
|
||||||
|
maxItems: 10,
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id", "label"],
|
||||||
|
properties: { id: { type: "string" }, label: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minSelections: { type: "integer", minimum: 0 },
|
||||||
|
maxSelections: { type: "integer", minimum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -62,6 +62,9 @@ class Runs implements RunRepository {
|
|||||||
async getById(_id: string) {
|
async getById(_id: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
async findActive() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
async appendEvent(_event: RunEvent) {}
|
async appendEvent(_event: RunEvent) {}
|
||||||
async listEvents(_id: string) {
|
async listEvents(_id: string) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
23
packages/agent-core/src/use-cases/run-service-helpers.ts
Normal file
23
packages/agent-core/src/use-cases/run-service-helpers.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { RunEvent } from "../domain/agent-run";
|
||||||
|
import type { Conversation } from "../domain/conversation";
|
||||||
|
import { CoreError } from "../errors/core-error";
|
||||||
|
|
||||||
|
export function lastSequence(events: readonly RunEvent[]): number {
|
||||||
|
return events.at(-1)?.sequence ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeErrorMessage(cause: unknown): string {
|
||||||
|
return cause instanceof CoreError ? cause.message : "模型服务暂时不可用";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagesThroughTrigger(
|
||||||
|
conversation: Conversation,
|
||||||
|
triggerMessageId: string,
|
||||||
|
) {
|
||||||
|
const index = conversation.messages.findIndex(
|
||||||
|
(message) => message.id === triggerMessageId,
|
||||||
|
);
|
||||||
|
return index < 0
|
||||||
|
? conversation.messages
|
||||||
|
: conversation.messages.slice(0, index + 1);
|
||||||
|
}
|
||||||
11
packages/agent-core/src/use-cases/run-types.ts
Normal file
11
packages/agent-core/src/use-cases/run-types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { AgentRun, RunEvent } from "../domain/agent-run";
|
||||||
|
|
||||||
|
export type StartRunInput =
|
||||||
|
| Readonly<{ kind: "ordinary"; message: string }>
|
||||||
|
| Readonly<{ kind: "project"; projectId: string; message: string }>
|
||||||
|
| Readonly<{ kind: "existing"; conversationId: string; message: string }>;
|
||||||
|
|
||||||
|
export type StartedRun = Readonly<{
|
||||||
|
run: AgentRun;
|
||||||
|
events: AsyncIterable<RunEvent>;
|
||||||
|
}>;
|
||||||
@ -33,6 +33,14 @@ export class FileRunRepository implements RunRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findActive(): Promise<AgentRun | null> {
|
||||||
|
return (
|
||||||
|
(await this.readAll()).find(
|
||||||
|
(run) => run.status === "running" || run.status === "waiting_user",
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async appendEvent(event: RunEvent): Promise<void> {
|
async appendEvent(event: RunEvent): Promise<void> {
|
||||||
await appendNdjson(this.eventsPath(event.runId), event);
|
await appendNdjson(this.eventsPath(event.runId), event);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
|
import { CoreError } from "@great-agent/agent-core";
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
ModelEvent,
|
ModelEvent,
|
||||||
@ -10,6 +11,7 @@ export type DeepSeekModelOptions = Readonly<{
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
timeoutMs: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class DeepSeekModelAdapter implements ModelPort {
|
export class DeepSeekModelAdapter implements ModelPort {
|
||||||
@ -26,56 +28,65 @@ export class DeepSeekModelAdapter implements ModelPort {
|
|||||||
request: ModelRequest,
|
request: ModelRequest,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): AsyncIterable<ModelEvent> {
|
): AsyncIterable<ModelEvent> {
|
||||||
const response = await this.client.chat.completions.create(
|
const timeoutSignal = AbortSignal.timeout(this.options.timeoutMs);
|
||||||
{
|
const requestSignal = AbortSignal.any([signal, timeoutSignal]);
|
||||||
model: request.model === "default" ? this.options.model : request.model,
|
try {
|
||||||
messages: toModelMessages(request),
|
const response = await this.client.chat.completions.create(
|
||||||
...(request.tools
|
{
|
||||||
? {
|
model:
|
||||||
tools: request.tools.map((tool) => ({
|
request.model === "default" ? this.options.model : request.model,
|
||||||
type: "function" as const,
|
messages: toModelMessages(request),
|
||||||
function: {
|
...(request.tools
|
||||||
name: tool.name,
|
? {
|
||||||
description: tool.description,
|
tools: request.tools.map((tool) => ({
|
||||||
parameters: tool.parameters,
|
type: "function" as const,
|
||||||
},
|
function: {
|
||||||
})),
|
name: tool.name,
|
||||||
}
|
description: tool.description,
|
||||||
: {}),
|
parameters: tool.parameters,
|
||||||
stream: true,
|
},
|
||||||
},
|
})),
|
||||||
{ signal },
|
}
|
||||||
);
|
: {}),
|
||||||
const toolCalls = new Map<
|
stream: true,
|
||||||
number,
|
},
|
||||||
{ id: string; name: string; arguments: string }
|
{ signal: requestSignal },
|
||||||
>();
|
);
|
||||||
for await (const chunk of response) {
|
const toolCalls = new Map<
|
||||||
const choice = chunk.choices[0]?.delta;
|
number,
|
||||||
const delta = choice?.content;
|
{ id: string; name: string; arguments: string }
|
||||||
if (delta) yield { type: "text.delta", delta };
|
>();
|
||||||
for (const call of choice?.tool_calls ?? []) {
|
for await (const chunk of response) {
|
||||||
const current = toolCalls.get(call.index) ?? {
|
const choice = chunk.choices[0]?.delta;
|
||||||
id: call.id ?? "",
|
const delta = choice?.content;
|
||||||
name: call.function?.name ?? "",
|
if (delta) yield { type: "text.delta", delta };
|
||||||
arguments: "",
|
for (const call of choice?.tool_calls ?? []) {
|
||||||
};
|
const current = toolCalls.get(call.index) ?? {
|
||||||
toolCalls.set(call.index, {
|
id: call.id ?? "",
|
||||||
id: call.id ?? current.id,
|
name: call.function?.name ?? "",
|
||||||
name: call.function?.name ?? current.name,
|
arguments: "",
|
||||||
arguments: current.arguments + (call.function?.arguments ?? ""),
|
};
|
||||||
});
|
toolCalls.set(call.index, {
|
||||||
|
id: call.id ?? current.id,
|
||||||
|
name: call.function?.name ?? current.name,
|
||||||
|
arguments: current.arguments + (call.function?.arguments ?? ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
for (const call of toolCalls.values()) {
|
||||||
|
yield {
|
||||||
|
type: "tool.requested",
|
||||||
|
toolCallId: call.id,
|
||||||
|
name: call.name,
|
||||||
|
arguments: call.arguments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
yield { type: "response.completed" };
|
||||||
|
} catch (cause) {
|
||||||
|
if (timeoutSignal.aborted && !signal.aborted)
|
||||||
|
throw new CoreError("MODEL_TIMEOUT", "模型响应超时,请重试");
|
||||||
|
throw cause;
|
||||||
}
|
}
|
||||||
for (const call of toolCalls.values()) {
|
|
||||||
yield {
|
|
||||||
type: "tool.requested",
|
|
||||||
toolCallId: call.id,
|
|
||||||
name: call.name,
|
|
||||||
arguments: call.arguments,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
yield { type: "response.completed" };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,12 @@ export {
|
|||||||
type RenameProjectRequest,
|
type RenameProjectRequest,
|
||||||
} from "./requests/project";
|
} from "./requests/project";
|
||||||
export { runEventSchema, type RunEventResponse } from "./events/run-event";
|
export { runEventSchema, type RunEventResponse } from "./events/run-event";
|
||||||
export { startedRunSchema, type StartedRunResponse } from "./responses/run";
|
export {
|
||||||
|
agentRunSchema,
|
||||||
|
startedRunSchema,
|
||||||
|
type AgentRunResponse,
|
||||||
|
type StartedRunResponse,
|
||||||
|
} from "./responses/run";
|
||||||
export {
|
export {
|
||||||
conversationSchema,
|
conversationSchema,
|
||||||
conversationSummarySchema,
|
conversationSummarySchema,
|
||||||
|
|||||||
@ -6,4 +6,22 @@ export const startedRunSchema = z.object({
|
|||||||
status: z.literal("running"),
|
status: z.literal("running"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const agentRunSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
conversationId: z.string(),
|
||||||
|
triggerMessageId: z.string(),
|
||||||
|
status: z.enum([
|
||||||
|
"running",
|
||||||
|
"waiting_user",
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"cancelled",
|
||||||
|
]),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
errorCode: z.string().optional(),
|
||||||
|
retryOfRunId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export type StartedRunResponse = z.infer<typeof startedRunSchema>;
|
export type StartedRunResponse = z.infer<typeof startedRunSchema>;
|
||||||
|
export type AgentRunResponse = z.infer<typeof agentRunSchema>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user