@downcity/agent 1.1.400 → 1.1.408
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/README.md +4 -3
- package/bin/agent/WorkspaceEntry.js +1 -1
- package/bin/agent/WorkspaceEntry.js.map +1 -1
- package/bin/city/types/PluginHostContext.d.ts +3 -0
- package/bin/city/types/PluginHostContext.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineError.js +8 -2
- package/bin/executor/core-engine/CoreEngineError.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +36 -0
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/model/ModelGenerate.d.ts.map +1 -1
- package/bin/executor/model/ModelGenerate.js +2 -1
- package/bin/executor/model/ModelGenerate.js.map +1 -1
- package/bin/executor/model/ModelStepRunner.d.ts +2 -0
- package/bin/executor/model/ModelStepRunner.d.ts.map +1 -1
- package/bin/executor/model/ModelStepRunner.js +33 -7
- package/bin/executor/model/ModelStepRunner.js.map +1 -1
- package/bin/executor/model/ModelStreamFailure.d.ts +22 -0
- package/bin/executor/model/ModelStreamFailure.d.ts.map +1 -0
- package/bin/executor/model/ModelStreamFailure.js +35 -0
- package/bin/executor/model/ModelStreamFailure.js.map +1 -0
- package/bin/group/GroupDispatchSession.d.ts +36 -0
- package/bin/group/GroupDispatchSession.d.ts.map +1 -0
- package/bin/group/GroupDispatchSession.js +154 -0
- package/bin/group/GroupDispatchSession.js.map +1 -0
- package/bin/group/GroupSession.d.ts +3 -1
- package/bin/group/GroupSession.d.ts.map +1 -1
- package/bin/group/GroupSession.js +65 -28
- package/bin/group/GroupSession.js.map +1 -1
- package/bin/index.d.ts +2 -0
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.js +10 -6
- package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
- package/bin/types/group/DispatchStrategy.d.ts +3 -0
- package/bin/types/group/DispatchStrategy.d.ts.map +1 -1
- package/bin/types/group/DispatchStrategy.js +66 -18
- package/bin/types/group/DispatchStrategy.js.map +1 -1
- package/bin/types/group/GroupDispatchSession.d.ts +65 -0
- package/bin/types/group/GroupDispatchSession.d.ts.map +1 -0
- package/bin/types/group/GroupDispatchSession.js +8 -0
- package/bin/types/group/GroupDispatchSession.js.map +1 -0
- package/bin/types/group/GroupSession.d.ts +2 -0
- package/bin/types/group/GroupSession.d.ts.map +1 -1
- package/bin/types/group/GroupSessionStore.d.ts +6 -1
- package/bin/types/group/GroupSessionStore.d.ts.map +1 -1
- package/bin/types/plugin/PluginNotification.d.ts +31 -0
- package/bin/types/plugin/PluginNotification.d.ts.map +1 -0
- package/bin/types/plugin/PluginNotification.js +8 -0
- package/bin/types/plugin/PluginNotification.js.map +1 -0
- package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts +28 -0
- package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts.map +1 -0
- package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js +102 -0
- package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js.map +1 -0
- package/bin/workspace/store/LocalGroupSessionDataStore.d.ts +2 -0
- package/bin/workspace/store/LocalGroupSessionDataStore.d.ts.map +1 -1
- package/bin/workspace/store/LocalGroupSessionDataStore.js +25 -8
- package/bin/workspace/store/LocalGroupSessionDataStore.js.map +1 -1
- package/city-test/group.test.mjs +252 -1
- package/package.json +15 -17
- package/scripts/agent-multi-workspace.test.mjs +67 -0
- package/scripts/core-engine-context-compaction.test.mjs +49 -0
- package/scripts/core-engine-model-stream.test.mjs +69 -0
- package/src/agent/WorkspaceEntry.ts +1 -1
- package/src/city/types/PluginHostContext.ts +4 -0
- package/src/executor/core-engine/CoreEngineError.ts +8 -2
- package/src/executor/core-engine/CoreEngineRunner.ts +44 -0
- package/src/executor/model/ModelGenerate.ts +3 -1
- package/src/executor/model/ModelStepRunner.ts +36 -5
- package/src/executor/model/ModelStreamFailure.ts +44 -0
- package/src/group/GroupDispatchSession.ts +175 -0
- package/src/group/GroupSession.ts +83 -28
- package/src/index.ts +10 -0
- package/src/session/messages/SessionAssistantMessageWriter.ts +9 -5
- package/src/types/group/DispatchStrategy.ts +79 -19
- package/src/types/group/GroupDispatchSession.ts +80 -0
- package/src/types/group/GroupSession.ts +2 -0
- package/src/types/group/GroupSessionStore.ts +6 -1
- package/src/types/plugin/PluginNotification.ts +38 -0
- package/src/workspace/store/LocalGroupDispatchSessionDataStore.ts +118 -0
- package/src/workspace/store/LocalGroupSessionDataStore.ts +35 -9
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type ModelStepResult,
|
|
35
35
|
type ModelStepToolCall,
|
|
36
36
|
} from "@executor/model/ModelStepRunner.js";
|
|
37
|
+
import { is_retryable_empty_model_stream_failure } from "@executor/model/ModelStreamFailure.js";
|
|
37
38
|
import { CoreEngineMessageState } from "@executor/core-engine/CoreEngineMessageState.js";
|
|
38
39
|
import {
|
|
39
40
|
deep_compact_model_messages,
|
|
@@ -58,6 +59,10 @@ const TURN_STOPPED_MESSAGE = "Turn stopped";
|
|
|
58
59
|
|
|
59
60
|
/** Provider context-length error 在当前 step 内最多压缩重试三次。 */
|
|
60
61
|
const MAX_CONTEXT_ERROR_COMPACTION_RETRIES = 3;
|
|
62
|
+
/** 无任何可见输出时,临时 Provider 流错误的最大自动重试次数。 */
|
|
63
|
+
const MAX_EMPTY_STREAM_RETRIES = 2;
|
|
64
|
+
/** 无输出流错误的重试退避,避免立即重复冲击同一 Provider。 */
|
|
65
|
+
const EMPTY_STREAM_RETRY_DELAYS_MS = [300, 1_000] as const;
|
|
61
66
|
|
|
62
67
|
interface CoreEngineRunnerOptions {
|
|
63
68
|
/** 当前 Session 稳定标识。 */
|
|
@@ -181,6 +186,7 @@ export class CoreEngineRunner {
|
|
|
181
186
|
|
|
182
187
|
let incomplete_response_recovery_count = 0;
|
|
183
188
|
let context_error_compaction_retries = 0;
|
|
189
|
+
let empty_stream_retry_count = 0;
|
|
184
190
|
let compact_pending = false;
|
|
185
191
|
let compact_validation_pending = Boolean(
|
|
186
192
|
persisted_compaction_summary_id &&
|
|
@@ -293,6 +299,24 @@ export class CoreEngineRunner {
|
|
|
293
299
|
) {
|
|
294
300
|
await input.turn_context.output.assistant.abort_step();
|
|
295
301
|
}
|
|
302
|
+
if (
|
|
303
|
+
is_retryable_empty_model_stream_failure(error) &&
|
|
304
|
+
empty_stream_retry_count < MAX_EMPTY_STREAM_RETRIES
|
|
305
|
+
) {
|
|
306
|
+
empty_stream_retry_count += 1;
|
|
307
|
+
await this.logger.log("warn", "[agent] model_stream.retry", {
|
|
308
|
+
session_id,
|
|
309
|
+
retry_count: empty_stream_retry_count,
|
|
310
|
+
error_code: error.code,
|
|
311
|
+
error: error.message,
|
|
312
|
+
provider_request_id: error.provider_request_id ?? null,
|
|
313
|
+
});
|
|
314
|
+
await wait_for_stream_retry(
|
|
315
|
+
EMPTY_STREAM_RETRY_DELAYS_MS[empty_stream_retry_count - 1] ?? 1_000,
|
|
316
|
+
input.turn_context.lifecycle.abort_signal,
|
|
317
|
+
);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
296
320
|
const compact_error = this.should_compact_on_error(error)
|
|
297
321
|
? error
|
|
298
322
|
: last_observed_stream_error;
|
|
@@ -328,6 +352,7 @@ export class CoreEngineRunner {
|
|
|
328
352
|
}
|
|
329
353
|
|
|
330
354
|
context_error_compaction_retries = 0;
|
|
355
|
+
empty_stream_retry_count = 0;
|
|
331
356
|
const last_step = executed_steps[executed_steps.length - 1];
|
|
332
357
|
if (!last_step) break;
|
|
333
358
|
|
|
@@ -533,6 +558,25 @@ export class CoreEngineRunner {
|
|
|
533
558
|
}
|
|
534
559
|
}
|
|
535
560
|
|
|
561
|
+
/** 等待下一次模型流重试,并在 Turn 停止时立即结束等待。 */
|
|
562
|
+
async function wait_for_stream_retry(
|
|
563
|
+
delay_ms: number,
|
|
564
|
+
abort_signal: AbortSignal,
|
|
565
|
+
): Promise<void> {
|
|
566
|
+
if (abort_signal.aborted) throw abort_signal.reason;
|
|
567
|
+
await new Promise<void>((resolve, reject) => {
|
|
568
|
+
const timer = setTimeout(() => {
|
|
569
|
+
abort_signal.removeEventListener("abort", on_abort);
|
|
570
|
+
resolve();
|
|
571
|
+
}, delay_ms);
|
|
572
|
+
const on_abort = (): void => {
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
reject(abort_signal.reason);
|
|
575
|
+
};
|
|
576
|
+
abort_signal.addEventListener("abort", on_abort, { once: true });
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
536
580
|
/** 在工具执行前接入 Session canonical Interaction 生命周期。 */
|
|
537
581
|
async function resolve_tool_approval(input: {
|
|
538
582
|
call: ModelStepToolCall;
|
|
@@ -51,7 +51,9 @@ export async function generate_model(
|
|
|
51
51
|
tools.set(event.content_id, { tool_call_id: event.tool_call_id, tool_name: event.tool_name });
|
|
52
52
|
} else if (event.type === "tool_call_finish") {
|
|
53
53
|
const tool = tools.get(event.content_id);
|
|
54
|
-
if (tool
|
|
54
|
+
if (tool && !event.input_error) {
|
|
55
|
+
tool_calls.push({ type: "tool_call", ...tool, input: event.input });
|
|
56
|
+
}
|
|
55
57
|
} else if (event.type === "model_usage") usage = event.usage;
|
|
56
58
|
else if (event.type === "model_finish") finish_reason = event.finish_reason;
|
|
57
59
|
}
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
SessionAssistantMessagePart,
|
|
27
27
|
SessionAssistantToolPart,
|
|
28
28
|
} from "@/types/session/SessionMessage.js";
|
|
29
|
+
import { ModelStreamFailure } from "@/executor/model/ModelStreamFailure.js";
|
|
29
30
|
|
|
30
31
|
/** 单个工具调用的执行事实。 */
|
|
31
32
|
export interface ModelStepToolCall {
|
|
@@ -35,6 +36,8 @@ export interface ModelStepToolCall {
|
|
|
35
36
|
tool_name: string;
|
|
36
37
|
/** 已解析工具输入。 */
|
|
37
38
|
input: ModelJsonValue;
|
|
39
|
+
/** 模型生成的工具输入无法解析时记录的错误。 */
|
|
40
|
+
input_error?: string;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/** 单个工具结果的执行事实。 */
|
|
@@ -162,7 +165,14 @@ class StepEventCollector {
|
|
|
162
165
|
|
|
163
166
|
/** 消费单个标准模型事件。 */
|
|
164
167
|
accept(event: ModelStreamEvent): void {
|
|
165
|
-
if (event.type === "model_error")
|
|
168
|
+
if (event.type === "model_error") {
|
|
169
|
+
throw new ModelStreamFailure(
|
|
170
|
+
event.error,
|
|
171
|
+
this.text.length > 0 ||
|
|
172
|
+
this.reasoning_by_id.size > 0 ||
|
|
173
|
+
this.tool_by_content_id.size > 0,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
166
176
|
if (event.type === "text_start") {
|
|
167
177
|
this.text_by_id.set(event.content_id, "");
|
|
168
178
|
} else if (event.type === "text_delta") {
|
|
@@ -213,6 +223,7 @@ class StepEventCollector {
|
|
|
213
223
|
const tool = this.tool_by_content_id.get(event.content_id);
|
|
214
224
|
if (tool) {
|
|
215
225
|
tool.input = event.input;
|
|
226
|
+
if (event.input_error) tool.input_error = event.input_error;
|
|
216
227
|
this.model_content.push({
|
|
217
228
|
type: "tool_call",
|
|
218
229
|
tool_call_id: tool.tool_call_id,
|
|
@@ -265,6 +276,14 @@ async function execute_tools(
|
|
|
265
276
|
): Promise<ModelStepToolResult[]> {
|
|
266
277
|
const results: ModelStepToolResult[] = [];
|
|
267
278
|
for (const call of calls) {
|
|
279
|
+
if (call.input_error) {
|
|
280
|
+
results.push({
|
|
281
|
+
...call,
|
|
282
|
+
success: false,
|
|
283
|
+
output: { error: call.input_error },
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
268
287
|
const tool = input.tools[call.tool_name];
|
|
269
288
|
if (!tool || typeof tool.execute !== "function") {
|
|
270
289
|
results.push({
|
|
@@ -292,7 +311,11 @@ async function execute_tools(
|
|
|
292
311
|
abort_signal: input.abort_signal,
|
|
293
312
|
};
|
|
294
313
|
const output = await tool.execute(call.input, options);
|
|
295
|
-
results.push({
|
|
314
|
+
results.push({
|
|
315
|
+
...call,
|
|
316
|
+
success: !is_structured_tool_failure(output),
|
|
317
|
+
output,
|
|
318
|
+
});
|
|
296
319
|
} catch (error) {
|
|
297
320
|
results.push({
|
|
298
321
|
...call,
|
|
@@ -304,6 +327,12 @@ async function execute_tools(
|
|
|
304
327
|
return results;
|
|
305
328
|
}
|
|
306
329
|
|
|
330
|
+
/** 识别 Tool 正常返回的结构化失败,避免把业务失败标记成执行成功。 */
|
|
331
|
+
function is_structured_tool_failure(output: unknown): boolean {
|
|
332
|
+
if (!output || typeof output !== "object" || Array.isArray(output)) return false;
|
|
333
|
+
return (output as { success?: unknown }).success === false;
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
/** 把 System 快照转换成标准 ModelMessage。 */
|
|
308
337
|
function convert_system_messages(messages: SessionSystemMessage[]): ModelMessage[] {
|
|
309
338
|
return messages.map((message) => ({
|
|
@@ -373,9 +402,11 @@ function append_tool_results(
|
|
|
373
402
|
|
|
374
403
|
/** 从结构化工具失败结果中提取可展示错误文本。 */
|
|
375
404
|
function read_tool_error(value: unknown): string {
|
|
376
|
-
if (value && typeof value === "object"
|
|
377
|
-
const
|
|
378
|
-
|
|
405
|
+
if (value && typeof value === "object") {
|
|
406
|
+
const result = value as { error?: unknown; message?: unknown; output?: unknown };
|
|
407
|
+
for (const candidate of [result.error, result.message, result.output]) {
|
|
408
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate;
|
|
409
|
+
}
|
|
379
410
|
}
|
|
380
411
|
return "Tool execution failed";
|
|
381
412
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型流失败对象。
|
|
3
|
+
*
|
|
4
|
+
* 该对象完整保留 Model Protocol 的错误分类,避免 Executor 只能依赖错误文本
|
|
5
|
+
* 判断是否可以重试。它只描述一次模型流失败,不负责决定恢复策略。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ModelError } from "@downcity/type";
|
|
9
|
+
|
|
10
|
+
/** 一次具有稳定协议语义的模型流失败。 */
|
|
11
|
+
export class ModelStreamFailure extends Error {
|
|
12
|
+
/** Downcity 稳定模型错误码。 */
|
|
13
|
+
readonly code: ModelError["code"];
|
|
14
|
+
|
|
15
|
+
/** 当前模型调用是否可以安全重试。 */
|
|
16
|
+
readonly retryable: boolean;
|
|
17
|
+
|
|
18
|
+
/** 失败前是否已经向用户发布正文或工具输入。 */
|
|
19
|
+
readonly has_partial_output: boolean;
|
|
20
|
+
|
|
21
|
+
/** Provider 返回的可观测请求标识。 */
|
|
22
|
+
readonly provider_request_id?: string;
|
|
23
|
+
|
|
24
|
+
constructor(error: ModelError, has_partial_output: boolean) {
|
|
25
|
+
super(error.message);
|
|
26
|
+
this.name = "ModelStreamFailure";
|
|
27
|
+
this.code = error.code;
|
|
28
|
+
this.retryable = error.retryable;
|
|
29
|
+
this.has_partial_output = has_partial_output;
|
|
30
|
+
if (error.provider_request_id) {
|
|
31
|
+
this.provider_request_id = error.provider_request_id;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 判断未知错误是否为可安全自动重试的无输出模型流失败。 */
|
|
37
|
+
export function is_retryable_empty_model_stream_failure(
|
|
38
|
+
error: unknown,
|
|
39
|
+
): error is ModelStreamFailure {
|
|
40
|
+
return error instanceof ModelStreamFailure &&
|
|
41
|
+
error.retryable &&
|
|
42
|
+
!error.has_partial_output &&
|
|
43
|
+
error.code !== "cancelled";
|
|
44
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GroupDispatchSession:单个 GroupSession 独享的持久化调度执行边界。
|
|
3
|
+
*
|
|
4
|
+
* 本对象统一拥有调度队列、当前模型取消信号和调度 Turn 状态。调度策略只负责决策,
|
|
5
|
+
* 不再自行拥有跨 GroupSession 的运行状态或停止语义。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { nanoid } from "nanoid";
|
|
9
|
+
import type {
|
|
10
|
+
GroupDispatchSessionDataStore,
|
|
11
|
+
GroupDispatchSessionInput,
|
|
12
|
+
GroupDispatchSessionOptions,
|
|
13
|
+
GroupDispatchSessionResult,
|
|
14
|
+
GroupDispatchTurnRecord,
|
|
15
|
+
} from "@/types/group/GroupDispatchSession.js";
|
|
16
|
+
|
|
17
|
+
/** 主动停止 Dispatch Session 时使用的内部错误。 */
|
|
18
|
+
export class GroupDispatchStoppedError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("Group dispatch stopped");
|
|
21
|
+
this.name = "GroupDispatchStoppedError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** GroupSession 私有的调度 Session。 */
|
|
26
|
+
export class GroupDispatchSession {
|
|
27
|
+
private readonly dispatch_strategy: GroupDispatchSessionOptions["dispatch_strategy"];
|
|
28
|
+
private store?: GroupDispatchSessionDataStore;
|
|
29
|
+
private queue_tail: Promise<void> = Promise.resolve();
|
|
30
|
+
private active_abort_controller?: AbortController;
|
|
31
|
+
private stopping = false;
|
|
32
|
+
private disposed = false;
|
|
33
|
+
|
|
34
|
+
constructor(options: GroupDispatchSessionOptions) {
|
|
35
|
+
this.dispatch_strategy = options.dispatch_strategy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 绑定持久化视图,并把进程中断留下的运行态明确收口为 stopped。 */
|
|
39
|
+
async initialize(store: GroupDispatchSessionDataStore): Promise<this> {
|
|
40
|
+
if (this.store && this.store !== store) {
|
|
41
|
+
throw new Error("GroupDispatchSession is already initialized with another Store");
|
|
42
|
+
}
|
|
43
|
+
if (this.store === store) return this;
|
|
44
|
+
this.store = store;
|
|
45
|
+
const interrupted_turns = (await store.list_turns()).filter((turn) => (
|
|
46
|
+
turn.status === "queued" || turn.status === "running"
|
|
47
|
+
));
|
|
48
|
+
for (const turn of interrupted_turns) {
|
|
49
|
+
await store.append_turn({
|
|
50
|
+
...turn,
|
|
51
|
+
status: "stopped",
|
|
52
|
+
error: "Group dispatch interrupted before recovery",
|
|
53
|
+
updated_at: Date.now(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 按提交顺序执行一次调度,并在返回前持久化最终决定。 */
|
|
60
|
+
decide(input: GroupDispatchSessionInput): Promise<GroupDispatchSessionResult> {
|
|
61
|
+
this.assert_available();
|
|
62
|
+
const store = this.require_store();
|
|
63
|
+
const created_at = Date.now();
|
|
64
|
+
const queued_turn: GroupDispatchTurnRecord = {
|
|
65
|
+
dispatch_id: `group-dispatch-${nanoid(12)}`,
|
|
66
|
+
trigger: input.trigger,
|
|
67
|
+
message_id: input.message.id,
|
|
68
|
+
pending_message_ids: input.pending_messages.map((message) => message.id),
|
|
69
|
+
status: "queued",
|
|
70
|
+
created_at,
|
|
71
|
+
updated_at: created_at,
|
|
72
|
+
};
|
|
73
|
+
const persist_queued = store.append_turn(queued_turn);
|
|
74
|
+
const operation = this.queue_tail.then(async () => {
|
|
75
|
+
await persist_queued;
|
|
76
|
+
return await this.execute_turn(input, queued_turn);
|
|
77
|
+
});
|
|
78
|
+
this.queue_tail = operation.then(() => undefined, () => undefined);
|
|
79
|
+
return operation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 中断当前模型调用,并让尚未开始的调度 Turn 按队列顺序收口。 */
|
|
83
|
+
async stop(): Promise<void> {
|
|
84
|
+
if (this.disposed || this.stopping) return;
|
|
85
|
+
this.stopping = true;
|
|
86
|
+
this.active_abort_controller?.abort(new GroupDispatchStoppedError());
|
|
87
|
+
await this.queue_tail;
|
|
88
|
+
this.active_abort_controller = undefined;
|
|
89
|
+
this.stopping = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 永久释放当前 Dispatch Session。 */
|
|
93
|
+
async dispose(): Promise<void> {
|
|
94
|
+
if (this.disposed) return;
|
|
95
|
+
await this.stop();
|
|
96
|
+
this.disposed = true;
|
|
97
|
+
this.store = undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 执行队首调度 Turn,确保成功、失败和停止都形成持久化终态。 */
|
|
101
|
+
private async execute_turn(
|
|
102
|
+
input: GroupDispatchSessionInput,
|
|
103
|
+
queued_turn: GroupDispatchTurnRecord,
|
|
104
|
+
): Promise<GroupDispatchSessionResult> {
|
|
105
|
+
const store = this.require_store();
|
|
106
|
+
if (this.stopping || this.disposed) {
|
|
107
|
+
await this.persist_terminal_turn(queued_turn, "stopped", "Group dispatch stopped");
|
|
108
|
+
throw new GroupDispatchStoppedError();
|
|
109
|
+
}
|
|
110
|
+
const abort_controller = new AbortController();
|
|
111
|
+
this.active_abort_controller = abort_controller;
|
|
112
|
+
await store.append_turn({
|
|
113
|
+
...queued_turn,
|
|
114
|
+
status: "running",
|
|
115
|
+
updated_at: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
const decision = await this.dispatch_strategy.decide_dispatch({
|
|
119
|
+
...input,
|
|
120
|
+
abort_signal: abort_controller.signal,
|
|
121
|
+
});
|
|
122
|
+
if (this.stopping || this.disposed || abort_controller.signal.aborted) {
|
|
123
|
+
throw new GroupDispatchStoppedError();
|
|
124
|
+
}
|
|
125
|
+
await store.append_turn({
|
|
126
|
+
...queued_turn,
|
|
127
|
+
status: "completed",
|
|
128
|
+
decision,
|
|
129
|
+
updated_at: Date.now(),
|
|
130
|
+
});
|
|
131
|
+
return { dispatch_id: queued_turn.dispatch_id, decision };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (
|
|
134
|
+
error instanceof GroupDispatchStoppedError ||
|
|
135
|
+
this.stopping ||
|
|
136
|
+
this.disposed ||
|
|
137
|
+
abort_controller.signal.aborted
|
|
138
|
+
) {
|
|
139
|
+
await this.persist_terminal_turn(queued_turn, "stopped", "Group dispatch stopped");
|
|
140
|
+
throw new GroupDispatchStoppedError();
|
|
141
|
+
}
|
|
142
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
143
|
+
await this.persist_terminal_turn(queued_turn, "failed", detail);
|
|
144
|
+
throw error;
|
|
145
|
+
} finally {
|
|
146
|
+
if (this.active_abort_controller === abort_controller) {
|
|
147
|
+
this.active_abort_controller = undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 提交没有 decision 的失败或停止终态。 */
|
|
153
|
+
private async persist_terminal_turn(
|
|
154
|
+
turn: GroupDispatchTurnRecord,
|
|
155
|
+
status: "failed" | "stopped",
|
|
156
|
+
error: string,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
await this.require_store().append_turn({
|
|
159
|
+
...turn,
|
|
160
|
+
status,
|
|
161
|
+
error,
|
|
162
|
+
updated_at: Date.now(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private require_store(): GroupDispatchSessionDataStore {
|
|
167
|
+
if (!this.store) throw new Error("GroupDispatchSession is not initialized");
|
|
168
|
+
return this.store;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private assert_available(): void {
|
|
172
|
+
if (this.disposed) throw new Error("GroupDispatchSession is disposed");
|
|
173
|
+
if (this.stopping) throw new Error("GroupDispatchSession is stopping");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -17,6 +17,11 @@ import type { DispatchDecision, DispatchNode, DispatchStrategy } from "@/types/g
|
|
|
17
17
|
import type { WorkspaceBase } from "@downcity/workspace";
|
|
18
18
|
import type { GroupSessionDataStore } from "@/types/group/GroupSessionStore.js";
|
|
19
19
|
import type { RespondSessionInteractionInput } from "@/types/session/SessionInteraction.js";
|
|
20
|
+
import {
|
|
21
|
+
GroupDispatchSession,
|
|
22
|
+
GroupDispatchStoppedError,
|
|
23
|
+
} from "@/group/GroupDispatchSession.js";
|
|
24
|
+
import type { GroupDispatchSessionResult } from "@/types/group/GroupDispatchSession.js";
|
|
20
25
|
|
|
21
26
|
const max_auto_dispatch_count = 32;
|
|
22
27
|
|
|
@@ -47,7 +52,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
47
52
|
private readonly group_name: string;
|
|
48
53
|
private readonly instruction?: string;
|
|
49
54
|
private readonly members: readonly Agent[];
|
|
50
|
-
private readonly
|
|
55
|
+
private readonly dispatch_session: GroupDispatchSession;
|
|
51
56
|
private readonly workspace?: WorkspaceBase;
|
|
52
57
|
private readonly messages_by_id: GroupMessage[] = [];
|
|
53
58
|
private readonly subscribers = new Set<GroupEventSubscriber>();
|
|
@@ -77,7 +82,9 @@ export class GroupSession implements GroupSessionContract {
|
|
|
77
82
|
this.group_name = options.group_name;
|
|
78
83
|
this.instruction = options.instruction;
|
|
79
84
|
this.members = options.members;
|
|
80
|
-
this.
|
|
85
|
+
this.dispatch_session = new GroupDispatchSession({
|
|
86
|
+
dispatch_strategy: options.dispatch_strategy,
|
|
87
|
+
});
|
|
81
88
|
this.workspace = options.workspace;
|
|
82
89
|
this.workspace_id = options.workspace?.id;
|
|
83
90
|
}
|
|
@@ -90,6 +97,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
90
97
|
if (this.store === store) return this;
|
|
91
98
|
this.store = store;
|
|
92
99
|
await store.initialize();
|
|
100
|
+
await this.dispatch_session.initialize(store.dispatch_session);
|
|
93
101
|
const metadata = await store.read_metadata();
|
|
94
102
|
if (metadata.group_id !== this.group_id) {
|
|
95
103
|
throw new Error(`GroupSession "${this.id}" belongs to another Group`);
|
|
@@ -112,7 +120,8 @@ export class GroupSession implements GroupSessionContract {
|
|
|
112
120
|
turn_id: checkpoint.turn_id,
|
|
113
121
|
root_message_id: checkpoint.root_message_id,
|
|
114
122
|
context_message_ids: checkpoint.context_message_ids,
|
|
115
|
-
|
|
123
|
+
dispatch_stage: checkpoint.dispatch_stage,
|
|
124
|
+
auto_pending: checkpoint.dispatch_stage === "auto",
|
|
116
125
|
stopped: false,
|
|
117
126
|
recovered: true,
|
|
118
127
|
});
|
|
@@ -151,7 +160,12 @@ export class GroupSession implements GroupSessionContract {
|
|
|
151
160
|
if (Object.keys(valid_member_session_ids).length !== Object.keys(member_session_ids).length) {
|
|
152
161
|
await store.update_metadata({ member_session_ids: valid_member_session_ids });
|
|
153
162
|
}
|
|
154
|
-
|
|
163
|
+
for (const group_turn of this.group_turns_by_id.values()) {
|
|
164
|
+
if (group_turn.dispatch_stage !== "user") continue;
|
|
165
|
+
const message = this.messages_by_id.find((item) => item.id === group_turn.root_message_id);
|
|
166
|
+
if (message) this.start_user_dispatch(message, group_turn);
|
|
167
|
+
}
|
|
168
|
+
if ([...this.group_turns_by_id.values()].some((turn) => turn.auto_pending) || this.auto_frontier_messages.length > 0) {
|
|
155
169
|
this.request_auto_dispatch();
|
|
156
170
|
}
|
|
157
171
|
return this;
|
|
@@ -169,19 +183,14 @@ export class GroupSession implements GroupSessionContract {
|
|
|
169
183
|
turn_id,
|
|
170
184
|
root_message_id: message.id,
|
|
171
185
|
context_message_ids: [...context_message_ids, message.id],
|
|
186
|
+
dispatch_stage: "user",
|
|
172
187
|
auto_pending: false,
|
|
173
188
|
stopped: false,
|
|
174
189
|
};
|
|
175
190
|
this.group_turns_by_id.set(turn_id, group_turn);
|
|
176
191
|
await this.persist_dispatch_checkpoint();
|
|
177
192
|
this.publish_status(turn_id, "dispatching", { message_id: message.id });
|
|
178
|
-
|
|
179
|
-
group_turn.user_dispatch = dispatch;
|
|
180
|
-
this.pending_deliveries.add(dispatch);
|
|
181
|
-
void dispatch.finally(() => {
|
|
182
|
-
this.pending_deliveries.delete(dispatch);
|
|
183
|
-
if (group_turn.auto_pending) this.request_auto_dispatch();
|
|
184
|
-
});
|
|
193
|
+
this.start_user_dispatch(message, group_turn);
|
|
185
194
|
return { turn_id, success: true, message_count: this.messages_by_id.length };
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -217,7 +226,10 @@ export class GroupSession implements GroupSessionContract {
|
|
|
217
226
|
if (this.disposed) return;
|
|
218
227
|
this.stop_requested = true;
|
|
219
228
|
this.publish_status(undefined, "stopped");
|
|
220
|
-
await Promise.allSettled([
|
|
229
|
+
await Promise.allSettled([
|
|
230
|
+
this.dispatch_session.stop(),
|
|
231
|
+
...[...this.member_sessions.values()].map((session) => session.stop()),
|
|
232
|
+
]);
|
|
221
233
|
await Promise.allSettled([...this.pending_deliveries]);
|
|
222
234
|
if (this.auto_dispatch_promise) await this.auto_dispatch_promise;
|
|
223
235
|
this.auto_dispatch_requested = false;
|
|
@@ -232,6 +244,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
232
244
|
async dispose(): Promise<void> {
|
|
233
245
|
if (this.disposed) return;
|
|
234
246
|
await this.stop();
|
|
247
|
+
await this.dispatch_session.dispose();
|
|
235
248
|
this.disposed = true;
|
|
236
249
|
this.subscribers.clear();
|
|
237
250
|
for (const unsubscribe of this.member_session_unsubscribes.values()) unsubscribe();
|
|
@@ -250,28 +263,53 @@ export class GroupSession implements GroupSessionContract {
|
|
|
250
263
|
const group_turn = this.group_turns_by_id.get(turn_id);
|
|
251
264
|
if (!group_turn) return;
|
|
252
265
|
try {
|
|
253
|
-
const
|
|
266
|
+
const dispatch = await this.decide_dispatch("user", message, [message]);
|
|
254
267
|
const context_messages = group_turn.context_message_ids
|
|
255
268
|
.map((message_id) => this.messages_by_id.find((item) => item.id === message_id))
|
|
256
269
|
.filter((item): item is GroupMessage => Boolean(item));
|
|
257
|
-
await this.run_dispatch_plan(
|
|
270
|
+
await this.run_dispatch_plan(
|
|
271
|
+
dispatch.decision,
|
|
272
|
+
message,
|
|
273
|
+
context_messages,
|
|
274
|
+
turn_id,
|
|
275
|
+
dispatch.dispatch_id,
|
|
276
|
+
);
|
|
258
277
|
if (this.stop_requested || this.disposed) {
|
|
259
278
|
group_turn.stopped = true;
|
|
260
279
|
this.group_turns_by_id.delete(turn_id);
|
|
261
280
|
await this.persist_dispatch_checkpoint();
|
|
262
281
|
return;
|
|
263
282
|
}
|
|
264
|
-
|
|
283
|
+
if (dispatch.decision.terminal) {
|
|
284
|
+
this.group_turns_by_id.delete(turn_id);
|
|
285
|
+
} else {
|
|
286
|
+
group_turn.dispatch_stage = "auto";
|
|
287
|
+
group_turn.auto_pending = true;
|
|
288
|
+
}
|
|
265
289
|
await this.persist_dispatch_checkpoint();
|
|
290
|
+
if (dispatch.decision.terminal) this.publish_status(turn_id, "idle");
|
|
266
291
|
} catch (error) {
|
|
267
292
|
group_turn.stopped = this.stop_requested;
|
|
268
|
-
|
|
293
|
+
if (!(error instanceof GroupDispatchStoppedError)) {
|
|
294
|
+
await this.append_dispatch_failure(message, error);
|
|
295
|
+
}
|
|
269
296
|
this.group_turns_by_id.delete(turn_id);
|
|
270
297
|
await this.persist_dispatch_checkpoint();
|
|
271
298
|
this.publish_status(turn_id, this.stop_requested ? "stopped" : "failed");
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
/** 登记一次用户调度运行;新建和恢复必须经过同一个生命周期入口。 */
|
|
303
|
+
private start_user_dispatch(message: GroupMessage, group_turn: GroupTurnRuntime): void {
|
|
304
|
+
const dispatch = this.run_user_dispatch(message, group_turn.turn_id);
|
|
305
|
+
group_turn.user_dispatch = dispatch;
|
|
306
|
+
this.pending_deliveries.add(dispatch);
|
|
307
|
+
void dispatch.finally(() => {
|
|
308
|
+
this.pending_deliveries.delete(dispatch);
|
|
309
|
+
if (group_turn.auto_pending) this.request_auto_dispatch();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
275
313
|
/** GroupSession 唯一的自动调度循环,统一消费已完成成员产生的新消息。 */
|
|
276
314
|
private request_auto_dispatch(): void {
|
|
277
315
|
if (this.stop_requested || this.disposed) return;
|
|
@@ -299,8 +337,8 @@ export class GroupSession implements GroupSessionContract {
|
|
|
299
337
|
const message = frontier_messages[frontier_messages.length - 1];
|
|
300
338
|
this.publish_status(undefined, "dispatching", { message_id: message.id });
|
|
301
339
|
try {
|
|
302
|
-
const
|
|
303
|
-
const dispatch_key = JSON.stringify(decision);
|
|
340
|
+
const dispatch = await this.decide_dispatch("auto", message, frontier_messages);
|
|
341
|
+
const dispatch_key = JSON.stringify(dispatch.decision);
|
|
304
342
|
if (this.auto_dispatch_path_keys.has(dispatch_key)) {
|
|
305
343
|
await this.append_message({ sender_type: "system", sender_id: "system", text: "Group auto dispatch detected a repeated path.", reply_to: message.id });
|
|
306
344
|
frontier_messages = [];
|
|
@@ -311,8 +349,14 @@ export class GroupSession implements GroupSessionContract {
|
|
|
311
349
|
continue;
|
|
312
350
|
}
|
|
313
351
|
this.auto_dispatch_path_keys.add(dispatch_key);
|
|
314
|
-
frontier_messages = await this.run_dispatch_plan(
|
|
315
|
-
|
|
352
|
+
frontier_messages = await this.run_dispatch_plan(
|
|
353
|
+
dispatch.decision,
|
|
354
|
+
message,
|
|
355
|
+
this.messages_by_id,
|
|
356
|
+
undefined,
|
|
357
|
+
dispatch.dispatch_id,
|
|
358
|
+
);
|
|
359
|
+
if (dispatch.decision.terminal || frontier_messages.length === 0) {
|
|
316
360
|
frontier_messages = [];
|
|
317
361
|
this.auto_frontier_messages = [];
|
|
318
362
|
this.auto_dispatch_path_keys.clear();
|
|
@@ -324,6 +368,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
324
368
|
this.auto_dispatch_requested = true;
|
|
325
369
|
}
|
|
326
370
|
} catch (error) {
|
|
371
|
+
if (error instanceof GroupDispatchStoppedError) return;
|
|
327
372
|
await this.append_dispatch_failure(message, error);
|
|
328
373
|
this.publish_status(undefined, "failed");
|
|
329
374
|
frontier_messages = [];
|
|
@@ -361,16 +406,21 @@ export class GroupSession implements GroupSessionContract {
|
|
|
361
406
|
batch_turns.forEach((group_turn) => { group_turn.auto_pending = false; });
|
|
362
407
|
for (const message of candidate_messages) this.consumed_auto_message_ids.add(message.id);
|
|
363
408
|
await this.persist_dispatch_checkpoint();
|
|
364
|
-
const dispatch_id = `group-dispatch-${nanoid(12)}`;
|
|
365
409
|
dispatch_count = 0;
|
|
366
410
|
const message = candidate_messages[candidate_messages.length - 1];
|
|
367
411
|
this.publish_status(undefined, "dispatching", { message_id: message.id });
|
|
368
412
|
try {
|
|
369
|
-
const
|
|
413
|
+
const dispatch = await this.decide_dispatch("auto", message, pending_messages);
|
|
370
414
|
const context_messages = this.messages_by_id.filter((item) => item.turn_id && batch_turn_ids.has(item.turn_id));
|
|
371
|
-
const outputs = await this.run_dispatch_plan(
|
|
415
|
+
const outputs = await this.run_dispatch_plan(
|
|
416
|
+
dispatch.decision,
|
|
417
|
+
message,
|
|
418
|
+
context_messages,
|
|
419
|
+
undefined,
|
|
420
|
+
dispatch.dispatch_id,
|
|
421
|
+
);
|
|
372
422
|
for (const turn_id of batch_turn_ids) this.group_turns_by_id.delete(turn_id);
|
|
373
|
-
if (decision.terminal || outputs.length === 0) {
|
|
423
|
+
if (dispatch.decision.terminal || outputs.length === 0) {
|
|
374
424
|
this.auto_frontier_messages = [];
|
|
375
425
|
await this.persist_dispatch_checkpoint();
|
|
376
426
|
this.publish_status(undefined, "idle");
|
|
@@ -381,6 +431,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
381
431
|
this.auto_dispatch_requested = true;
|
|
382
432
|
}
|
|
383
433
|
} catch (error) {
|
|
434
|
+
if (error instanceof GroupDispatchStoppedError) return;
|
|
384
435
|
await this.append_dispatch_failure(message, error);
|
|
385
436
|
this.publish_status(undefined, "failed");
|
|
386
437
|
for (const turn_id of batch_turn_ids) this.group_turns_by_id.delete(turn_id);
|
|
@@ -394,8 +445,8 @@ export class GroupSession implements GroupSessionContract {
|
|
|
394
445
|
trigger: "user" | "auto",
|
|
395
446
|
message: GroupMessage,
|
|
396
447
|
pending_messages: readonly GroupMessage[],
|
|
397
|
-
): Promise<
|
|
398
|
-
return await this.
|
|
448
|
+
): Promise<GroupDispatchSessionResult> {
|
|
449
|
+
return await this.dispatch_session.decide({
|
|
399
450
|
trigger,
|
|
400
451
|
message,
|
|
401
452
|
pending_messages,
|
|
@@ -537,7 +588,10 @@ export class GroupSession implements GroupSessionContract {
|
|
|
537
588
|
await this.append_member_failure(agent, message, result.error || "未知错误");
|
|
538
589
|
return null;
|
|
539
590
|
}
|
|
540
|
-
if (!result.text?.trim())
|
|
591
|
+
if (!result.text?.trim()) {
|
|
592
|
+
await this.append_member_failure(agent, message, "未生成有效回复");
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
541
595
|
const reply = await this.append_message({
|
|
542
596
|
sender_type: "agent",
|
|
543
597
|
sender_id: agent.id,
|
|
@@ -653,7 +707,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
653
707
|
`Current message from ${message.sender_type === "agent" ? message.sender_id : "user"}: ${message.text}`,
|
|
654
708
|
dispatch_instruction,
|
|
655
709
|
`你只能代表成员 ${agent.id} 自己发言。不得代替、指挥、裁定、总结或转述其他成员;不得假装自己是其他成员;不得创建队长、裁判或协调者。`,
|
|
656
|
-
"
|
|
710
|
+
"你已被 Group 调度选中,必须直接回复当前消息。",
|
|
657
711
|
].filter(Boolean).join("\n");
|
|
658
712
|
}
|
|
659
713
|
|
|
@@ -695,6 +749,7 @@ export class GroupSession implements GroupSessionContract {
|
|
|
695
749
|
turn_id: group_turn.turn_id,
|
|
696
750
|
root_message_id: group_turn.root_message_id,
|
|
697
751
|
context_message_ids: [...group_turn.context_message_ids],
|
|
752
|
+
dispatch_stage: group_turn.dispatch_stage,
|
|
698
753
|
})),
|
|
699
754
|
auto_frontier_message_ids: this.auto_frontier_messages.map((message) => message.id),
|
|
700
755
|
});
|