@downcity/agent 1.1.149 → 1.1.152
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/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +10 -5
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts +8 -0
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.js +21 -7
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.js.map +1 -1
- package/bin/executor/types/SessionRun.d.ts +6 -2
- package/bin/executor/types/SessionRun.d.ts.map +1 -1
- package/bin/executor/types/SessionRun.js +1 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +10 -20
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +8 -6
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +1 -1
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +6 -2
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/storage/Persistence.d.ts +5 -1
- package/bin/session/storage/Persistence.d.ts.map +1 -1
- package/bin/session/storage/Persistence.js +3 -1
- package/bin/session/storage/Persistence.js.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +2 -2
- package/package.json +3 -3
- package/scripts/session-list-title.test.mjs +141 -0
- package/scripts/session-prompt-runtime.test.mjs +44 -3
- package/src/executor/core-engine/CoreEngineRunner.ts +18 -13
- package/src/executor/core-engine/CoreEngineUiStreamCollector.ts +27 -6
- package/src/executor/types/SessionRun.ts +6 -2
- package/src/session/browse/Browse.ts +18 -22
- package/src/session/runtime/SessionPromptRuntime.ts +9 -7
- package/src/session/services/SessionStateService.ts +1 -1
- package/src/session/services/SessionTurnService.ts +7 -3
- package/src/session/storage/Persistence.ts +8 -2
- package/src/types/agent/SessionTypes.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -49,8 +49,12 @@ export interface PersistSdkAssistantResultParams extends TouchSessionMetadataPar
|
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
51
|
* 本轮执行得到的 assistant 消息。
|
|
52
|
+
*
|
|
53
|
+
* 关键点(中文)
|
|
54
|
+
* - stop/abort 且没有 assistant 内容时允许为空。
|
|
55
|
+
* - 为空时仅刷新 metadata,不写入伪造 assistant 正文。
|
|
52
56
|
*/
|
|
53
|
-
assistantMessage
|
|
57
|
+
assistantMessage?: SessionMessageV1 | null;
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* 刷新 SDK session 元数据。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Persistence.d.ts","sourceRoot":"","sources":["../../../src/session/storage/Persistence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAM9E;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,aAAa,EAAE,0BAA0B,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,+BACf,SAAQ,0BAA0B;IAClC;;OAEG;IACH,QAAQ,EAAE;QACR,sBAAsB,CAAC,MAAM,EAAE;YAC7B;;eAEG;YACH,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;YAClC;;eAEG;YACH,YAAY,CAAC,EAAE,MAAM,CAAC;SACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnB,CAAC;IACF
|
|
1
|
+
{"version":3,"file":"Persistence.d.ts","sourceRoot":"","sources":["../../../src/session/storage/Persistence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAM9E;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,aAAa,EAAE,0BAA0B,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,+BACf,SAAQ,0BAA0B;IAClC;;OAEG;IACH,QAAQ,EAAE;QACR,sBAAsB,CAAC,MAAM,EAAE;YAC7B;;eAEG;YACH,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;YAClC;;eAEG;YACH,YAAY,CAAC,EAAE,MAAM,CAAC;SACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnB,CAAC;IACF;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -42,7 +42,9 @@ export async function persistSdkAssistantResult(params) {
|
|
|
42
42
|
await persistAssistantResult({
|
|
43
43
|
writer: params.executor,
|
|
44
44
|
assistantMessage: params.assistantMessage,
|
|
45
|
-
fallbackText:
|
|
45
|
+
fallbackText: params.assistantMessage
|
|
46
|
+
? extractTextFromUiMessage(params.assistantMessage)
|
|
47
|
+
: undefined,
|
|
46
48
|
});
|
|
47
49
|
await touchSessionMetadata(params);
|
|
48
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Persistence.js","sourceRoot":"","sources":["../../../src/session/storage/Persistence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAGvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,mDAAmD,CAAC;AAE3F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"Persistence.js","sourceRoot":"","sources":["../../../src/session/storage/Persistence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAGvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,mDAAmD,CAAC;AAE3F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,+BAA+B,CAAC;AAsDvC;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAkC;IAElC,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC;QACxC,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IACH,MAAM,IAAI,GAAyB;QACjC,GAAG,OAAO;QACV,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EACP,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;QACxE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU;YACjC,CAAC,CAAC;gBACE,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,UAAU;aAC5C;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;IACF,MAAM,oBAAoB,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,IAAI,EAAE,IAAI;KACX,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAuC;IAEvC,MAAM,sBAAsB,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC,QAAQ;QACvB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,YAAY,EAAE,MAAM,CAAC,gBAAgB;YACnC,CAAC,CAAC,wBAAwB,CAAC,MAAM,CAAC,gBAAgB,CAAC;YACnD,CAAC,CAAC,SAAS;KACd,CAAC,CAAC;IACH,MAAM,oBAAoB,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC"}
|
|
@@ -193,8 +193,8 @@ export interface AgentSessionSummary {
|
|
|
193
193
|
*
|
|
194
194
|
* 说明(中文)
|
|
195
195
|
* - 标题持久化在 session `meta.json` 顶层。
|
|
196
|
-
* -
|
|
197
|
-
* -
|
|
196
|
+
* - SDK 只在模型成功生成标题时写入,不再从首条用户消息生成 fallback。
|
|
197
|
+
* - 标题允许为空,调用方需要展示占位文案时可自行回退到 `sessionId`。
|
|
198
198
|
*/
|
|
199
199
|
title?: string;
|
|
200
200
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@downcity/agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.152",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
|
|
6
6
|
"main": "./bin/index.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@ai-sdk/openai-compatible": "^2.0.48",
|
|
20
|
-
"@downcity/shell": "^0.1.
|
|
21
|
-
"@downcity/type": "0.1.
|
|
20
|
+
"@downcity/shell": "^0.1.30",
|
|
21
|
+
"@downcity/type": "0.1.64",
|
|
22
22
|
"@larksuiteoapi/node-sdk": "^1.66.0",
|
|
23
23
|
"ai": "^6.0.193",
|
|
24
24
|
"commander": "^15.0.0",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 session 列表从正确目录读取持久化 title。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 普通列表只读取 `.downcity/agents/<agentId>/sessions` 下的 meta。
|
|
6
|
+
* - 归档列表只读取 `.downcity/agents/<agentId>/archived-sessions` 下的 meta。
|
|
7
|
+
* - title 允许为空;这里仅验证已由模型生成并落盘的 title 能被列表返回。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
|
|
16
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
17
|
+
import { Agent } from "../bin/index.js";
|
|
18
|
+
|
|
19
|
+
function create_mock_title_model(title_text) {
|
|
20
|
+
return new MockLanguageModelV3({
|
|
21
|
+
modelId: "mock-session-list-title-model",
|
|
22
|
+
doGenerate: async () => ({
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: title_text,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
finishReason: "stop",
|
|
30
|
+
usage: {
|
|
31
|
+
inputTokens: {
|
|
32
|
+
total: 0,
|
|
33
|
+
noCache: 0,
|
|
34
|
+
cacheRead: 0,
|
|
35
|
+
cacheWrite: 0,
|
|
36
|
+
},
|
|
37
|
+
outputTokens: {
|
|
38
|
+
total: 0,
|
|
39
|
+
text: 0,
|
|
40
|
+
reasoning: 0,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
warnings: [],
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function create_agent_with_titled_session(input) {
|
|
49
|
+
const agent_path = await fs.mkdtemp(
|
|
50
|
+
path.join(os.tmpdir(), input.tmp_prefix),
|
|
51
|
+
);
|
|
52
|
+
const agent = new Agent({
|
|
53
|
+
id: input.agent_id,
|
|
54
|
+
path: agent_path,
|
|
55
|
+
model: create_mock_title_model(input.title),
|
|
56
|
+
});
|
|
57
|
+
const collection = agent.session_collection();
|
|
58
|
+
const session = await collection.create_session({
|
|
59
|
+
sessionId: input.session_id,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await session.appendUserMessage({
|
|
63
|
+
text: input.first_user_text,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
agent,
|
|
68
|
+
collection,
|
|
69
|
+
session,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test("list_sessions returns persisted title from active session metadata", async () => {
|
|
74
|
+
const { agent, collection, session } = await create_agent_with_titled_session({
|
|
75
|
+
tmp_prefix: "downcity-agent-session-list-title-",
|
|
76
|
+
agent_id: "list_title_agent",
|
|
77
|
+
session_id: "active_session",
|
|
78
|
+
title: "列表标题",
|
|
79
|
+
first_user_text: "Need the session list to show the generated title",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const page = await collection.list_sessions();
|
|
84
|
+
|
|
85
|
+
assert.equal(page.total, 1);
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
page.items.map((item) => ({
|
|
88
|
+
sessionId: item.sessionId,
|
|
89
|
+
title: item.title,
|
|
90
|
+
messageCount: item.messageCount,
|
|
91
|
+
})),
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
sessionId: session.id,
|
|
95
|
+
title: "列表标题",
|
|
96
|
+
messageCount: 1,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
await agent.dispose();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("archive_sessions returns title from archived session metadata", async () => {
|
|
106
|
+
const { agent, collection, session } = await create_agent_with_titled_session({
|
|
107
|
+
tmp_prefix: "downcity-agent-archive-list-title-",
|
|
108
|
+
agent_id: "archive_list_title_agent",
|
|
109
|
+
session_id: "archived_session",
|
|
110
|
+
title: "归档标题",
|
|
111
|
+
first_user_text: "Archive this titled session",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await collection.archive_session({
|
|
116
|
+
id: session.id,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const active_page = await collection.list_sessions();
|
|
120
|
+
const archived_page = await collection.archive_sessions();
|
|
121
|
+
|
|
122
|
+
assert.equal(active_page.total, 0);
|
|
123
|
+
assert.equal(archived_page.total, 1);
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
archived_page.items.map((item) => ({
|
|
126
|
+
sessionId: item.sessionId,
|
|
127
|
+
title: item.title,
|
|
128
|
+
messageCount: item.messageCount,
|
|
129
|
+
})),
|
|
130
|
+
[
|
|
131
|
+
{
|
|
132
|
+
sessionId: session.id,
|
|
133
|
+
title: "归档标题",
|
|
134
|
+
messageCount: 1,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
);
|
|
138
|
+
} finally {
|
|
139
|
+
await agent.dispose();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -207,9 +207,10 @@ test("SessionPromptRuntime stops current turn and cancels unmerged queued prompt
|
|
|
207
207
|
});
|
|
208
208
|
await executionFinished.promise;
|
|
209
209
|
return {
|
|
210
|
-
text: "
|
|
211
|
-
success:
|
|
212
|
-
assistantMessage: createAssistantMessage("
|
|
210
|
+
text: "partial answer",
|
|
211
|
+
success: false,
|
|
212
|
+
assistantMessage: createAssistantMessage("partial answer", 1),
|
|
213
|
+
error: "Turn stopped",
|
|
213
214
|
};
|
|
214
215
|
},
|
|
215
216
|
stopTurn: () => {
|
|
@@ -234,6 +235,8 @@ test("SessionPromptRuntime stops current turn and cancels unmerged queued prompt
|
|
|
234
235
|
assert.equal(stopResult.cancelledQueuedPrompts, 1);
|
|
235
236
|
assert.equal(firstResult.success, false);
|
|
236
237
|
assert.equal(firstResult.error, "Turn stopped");
|
|
238
|
+
assert.equal(firstResult.text, "partial answer");
|
|
239
|
+
assert.equal(firstResult.assistantMessage.parts[0]?.text, "partial answer");
|
|
237
240
|
assert.equal(secondResult.success, false);
|
|
238
241
|
assert.equal(
|
|
239
242
|
secondResult.error,
|
|
@@ -244,3 +247,41 @@ test("SessionPromptRuntime stops current turn and cancels unmerged queued prompt
|
|
|
244
247
|
["turn-start", "turn-start", "turn-finish", "turn-finish"],
|
|
245
248
|
);
|
|
246
249
|
});
|
|
250
|
+
|
|
251
|
+
test("SessionPromptRuntime does not synthesize assistant text when stopped before output", async () => {
|
|
252
|
+
const executionFinished = createDeferred();
|
|
253
|
+
|
|
254
|
+
const runtime = new SessionPromptRuntime({
|
|
255
|
+
sessionId: "test",
|
|
256
|
+
publish: () => {},
|
|
257
|
+
createAndPersistUserMessage: async (input) => {
|
|
258
|
+
return createUserMessage(input.query, 1);
|
|
259
|
+
},
|
|
260
|
+
executeTurn: async (input) => {
|
|
261
|
+
await new Promise((resolve) => {
|
|
262
|
+
input.abortSignal.addEventListener("abort", resolve, { once: true });
|
|
263
|
+
});
|
|
264
|
+
await executionFinished.promise;
|
|
265
|
+
return {
|
|
266
|
+
text: "",
|
|
267
|
+
success: false,
|
|
268
|
+
error: "Turn stopped",
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
stopTurn: () => {
|
|
272
|
+
executionFinished.resolve();
|
|
273
|
+
return true;
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const turn = await runtime.prompt({ query: "first" });
|
|
278
|
+
await waitUntil(() => runtime.isActive());
|
|
279
|
+
|
|
280
|
+
runtime.stop();
|
|
281
|
+
const result = await turn.finished;
|
|
282
|
+
|
|
283
|
+
assert.equal(result.success, false);
|
|
284
|
+
assert.equal(result.error, "Turn stopped");
|
|
285
|
+
assert.equal(result.text, "");
|
|
286
|
+
assert.equal(result.assistantMessage, undefined);
|
|
287
|
+
});
|
|
@@ -151,6 +151,7 @@ export class CoreEngineRunner {
|
|
|
151
151
|
: [];
|
|
152
152
|
const tools = input.execute_input.tools;
|
|
153
153
|
let last_observed_stream_error: unknown = undefined;
|
|
154
|
+
let final_assistant_ui_message: SessionMessageV1 | null = null;
|
|
154
155
|
|
|
155
156
|
try {
|
|
156
157
|
const message_state = await CoreEngineMessageState.create({
|
|
@@ -190,7 +191,6 @@ export class CoreEngineRunner {
|
|
|
190
191
|
runContext: input.run_context,
|
|
191
192
|
});
|
|
192
193
|
|
|
193
|
-
let final_assistant_ui_message: SessionMessageV1 | null = null;
|
|
194
194
|
let text_only_continuation_count = 0;
|
|
195
195
|
let incomplete_response_recovery_count = 0;
|
|
196
196
|
|
|
@@ -224,8 +224,18 @@ export class CoreEngineRunner {
|
|
|
224
224
|
input.run_context,
|
|
225
225
|
),
|
|
226
226
|
onUiMessageChunkCallback: input.run_context.onUiMessageChunkCallback,
|
|
227
|
+
abortSignal: input.run_context.abortSignal,
|
|
227
228
|
});
|
|
228
229
|
|
|
230
|
+
final_assistant_ui_message = mergeAssistantUiMessages(
|
|
231
|
+
final_assistant_ui_message,
|
|
232
|
+
step_assistant_ui_message,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// 关键点(中文):先保存本 step 已收敛的 assistant 消息,再等待 steps。
|
|
236
|
+
// stop/abort 时 `result.steps` 可能抛错,但当前已经生成的文本仍应沉淀。
|
|
237
|
+
message_state.appendRuntimeSessionMessage(step_assistant_ui_message);
|
|
238
|
+
|
|
229
239
|
const executed_steps = await result.steps;
|
|
230
240
|
const last_step = executed_steps[executed_steps.length - 1];
|
|
231
241
|
if (!last_step) break;
|
|
@@ -312,14 +322,6 @@ export class CoreEngineRunner {
|
|
|
312
322
|
: [];
|
|
313
323
|
message_state.appendModelMessages(response_messages);
|
|
314
324
|
|
|
315
|
-
final_assistant_ui_message = mergeAssistantUiMessages(
|
|
316
|
-
final_assistant_ui_message,
|
|
317
|
-
step_assistant_ui_message,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// 关键点(中文):把本 step 的 assistant UI 消息并入运行时上下文,保证后续全量重算不丢历史。
|
|
321
|
-
message_state.appendRuntimeSessionMessage(step_assistant_ui_message);
|
|
322
|
-
|
|
323
325
|
if (loop_decision.continueForToolCalls) {
|
|
324
326
|
text_only_continuation_count = 0;
|
|
325
327
|
incomplete_response_recovery_count = 0;
|
|
@@ -413,13 +415,16 @@ export class CoreEngineRunner {
|
|
|
413
415
|
await this.logger.log("info", "[agent] stopped", {
|
|
414
416
|
sessionId: session_id,
|
|
415
417
|
});
|
|
418
|
+
const stopped_message = final_assistant_ui_message
|
|
419
|
+
? mergePendingAssistantFileParts(
|
|
420
|
+
final_assistant_ui_message,
|
|
421
|
+
input.run_context.pendingAssistantFileParts,
|
|
422
|
+
)
|
|
423
|
+
: null;
|
|
416
424
|
return {
|
|
417
425
|
success: false,
|
|
418
426
|
error: error_text,
|
|
419
|
-
assistantMessage:
|
|
420
|
-
error_text,
|
|
421
|
-
input.run_context,
|
|
422
|
-
),
|
|
427
|
+
...(stopped_message ? { assistantMessage: stopped_message } : {}),
|
|
423
428
|
deferredPersistedUserMessages: [
|
|
424
429
|
...input.run_context.deferredPersistedUserMessages,
|
|
425
430
|
],
|
|
@@ -42,9 +42,18 @@ export async function collectFinalAssistantMessageFromUiStream(params: {
|
|
|
42
42
|
* UI stream chunk 回调。
|
|
43
43
|
*/
|
|
44
44
|
onUiMessageChunkCallback?: SessionUiMessageChunkCallback;
|
|
45
|
+
/**
|
|
46
|
+
* 当前 turn 的取消信号。
|
|
47
|
+
*
|
|
48
|
+
* 关键点(中文)
|
|
49
|
+
* - stop 触发后,UI stream 可能在 onFinish 前中断。
|
|
50
|
+
* - 此时仍应尽量用已经收到的 text delta 构造可持久化 assistant 消息。
|
|
51
|
+
*/
|
|
52
|
+
abortSignal?: AbortSignal;
|
|
45
53
|
}): Promise<SessionMessageV1> {
|
|
46
54
|
let streamedAssistantMessage: SessionMessageV1 | null = null;
|
|
47
55
|
let uiFinishSummary: JsonObject | null = null;
|
|
56
|
+
let streamed_text = "";
|
|
48
57
|
|
|
49
58
|
const uiStream = params.result.toUIMessageStream<SessionMessageV1>({
|
|
50
59
|
// 关键点(中文):SDK stream 需要 reasoning 旁路事件时可直接消费;最终落盘仍由 responseMessage 收敛。
|
|
@@ -74,12 +83,21 @@ export async function collectFinalAssistantMessageFromUiStream(params: {
|
|
|
74
83
|
},
|
|
75
84
|
});
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
try {
|
|
87
|
+
for await (const chunk of uiStream) {
|
|
88
|
+
if (chunk.type === "text-delta") {
|
|
89
|
+
streamed_text += String(chunk.delta || "");
|
|
90
|
+
}
|
|
91
|
+
if (typeof params.onUiMessageChunkCallback !== "function") continue;
|
|
92
|
+
try {
|
|
93
|
+
await params.onUiMessageChunkCallback(chunk);
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore UI stream callback failures
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!params.abortSignal?.aborted) {
|
|
100
|
+
throw error;
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
|
|
@@ -98,6 +116,9 @@ export async function collectFinalAssistantMessageFromUiStream(params: {
|
|
|
98
116
|
} catch {
|
|
99
117
|
assistantText = "";
|
|
100
118
|
}
|
|
119
|
+
if (!assistantText) {
|
|
120
|
+
assistantText = streamed_text.trim();
|
|
121
|
+
}
|
|
101
122
|
|
|
102
123
|
await params.logger.log("warn", "[agent] final.message.fallback", {
|
|
103
124
|
sessionId: params.sessionId,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - `SessionRunInput` 表示上层会话入口输入(例如 context query)。
|
|
6
6
|
* - `SessionExecuteInput` 表示 Executor 通过 Composer 装配后的中间运行态。
|
|
7
|
-
* -
|
|
7
|
+
* - 输出暴露可选 assistantMessage(UIMessage)。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Tool, UIMessageChunk } from "ai";
|
|
@@ -94,8 +94,12 @@ export interface SessionRunResult {
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* 最终 assistant 消息。
|
|
97
|
+
*
|
|
98
|
+
* 关键点(中文)
|
|
99
|
+
* - stop/abort 且没有任何 assistant 内容时可以为空。
|
|
100
|
+
* - turn 状态通过 `success` / `error` 表达,不应伪造成 assistant 正文。
|
|
97
101
|
*/
|
|
98
|
-
assistantMessage
|
|
102
|
+
assistantMessage?: SessionMessageV1 | null;
|
|
99
103
|
|
|
100
104
|
/**
|
|
101
105
|
* 本轮执行结束后待写入长期历史的 user 消息。
|
|
@@ -32,15 +32,12 @@ import type {
|
|
|
32
32
|
import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
|
|
33
33
|
import { pickLastSuccessfulChatSendText } from "@/executor/messages/UserVisibleText.js";
|
|
34
34
|
import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
|
|
35
|
+
import { getSdkAgentSessionMetaPath } from "@/session/storage/Paths.js";
|
|
35
36
|
import { getSdkAgentSessionsRootDirPath } from "@/session/storage/Paths.js";
|
|
36
37
|
import { getSdkAgentArchivedSessionsDirPath } from "@/session/storage/Paths.js";
|
|
37
38
|
import { getSdkAgentArchivedSessionMessagesPath } from "@/session/storage/Paths.js";
|
|
38
39
|
import { getSdkAgentArchivedSessionMetaPath } from "@/session/storage/Paths.js";
|
|
39
|
-
import { readSessionMetadata } from "@/session/storage/Metadata.js";
|
|
40
40
|
import { readSessionMetadataFromPath } from "@/session/storage/Metadata.js";
|
|
41
|
-
import {
|
|
42
|
-
ensureSessionTitle,
|
|
43
|
-
} from "@/session/SessionTitle.js";
|
|
44
41
|
|
|
45
42
|
type AnyUiPart = UIMessagePart<Record<string, never>, Record<string, never>>;
|
|
46
43
|
|
|
@@ -479,7 +476,7 @@ export async function listAgentSessionSummaryPage(params: {
|
|
|
479
476
|
const sessionId = decodeMaybe(entry.name);
|
|
480
477
|
if (!sessionId) continue;
|
|
481
478
|
const metadata = await readSessionMetadataFromPath({
|
|
482
|
-
filePath:
|
|
479
|
+
filePath: getSdkAgentSessionMetaPath(
|
|
483
480
|
params.projectRoot,
|
|
484
481
|
params.agentId,
|
|
485
482
|
sessionId,
|
|
@@ -488,19 +485,17 @@ export async function listAgentSessionSummaryPage(params: {
|
|
|
488
485
|
agentId: params.agentId,
|
|
489
486
|
});
|
|
490
487
|
const messages = await loadSessionMessagesFromPath(
|
|
491
|
-
|
|
488
|
+
getSdkAgentSessionMessagesPath(
|
|
492
489
|
params.projectRoot,
|
|
493
490
|
params.agentId,
|
|
494
491
|
sessionId,
|
|
495
492
|
),
|
|
496
493
|
);
|
|
497
|
-
// 关键点(中文):归档 session 不再生成新 title,仅读取已有 meta。
|
|
498
|
-
const metadataWithTitle = metadata;
|
|
499
494
|
const info = buildSessionInfo({
|
|
500
495
|
projectRoot: params.projectRoot,
|
|
501
496
|
agentId: params.agentId,
|
|
502
497
|
sessionId,
|
|
503
|
-
metadata
|
|
498
|
+
metadata,
|
|
504
499
|
messages,
|
|
505
500
|
executing: params.executingSessionIds?.has(sessionId),
|
|
506
501
|
});
|
|
@@ -575,27 +570,28 @@ export async function listArchivedAgentSessionSummaryPage(params: {
|
|
|
575
570
|
if (!entry.isDirectory()) continue;
|
|
576
571
|
const sessionId = decodeMaybe(entry.name);
|
|
577
572
|
if (!sessionId) continue;
|
|
578
|
-
const metadata = await
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
const metadata = await readSessionMetadataFromPath({
|
|
574
|
+
filePath: getSdkAgentArchivedSessionMetaPath(
|
|
575
|
+
params.projectRoot,
|
|
576
|
+
params.agentId,
|
|
577
|
+
sessionId,
|
|
578
|
+
),
|
|
581
579
|
sessionId,
|
|
580
|
+
agentId: params.agentId,
|
|
582
581
|
});
|
|
583
582
|
const messages = await loadSessionMessagesFromPath(
|
|
584
|
-
|
|
583
|
+
getSdkAgentArchivedSessionMessagesPath(
|
|
584
|
+
params.projectRoot,
|
|
585
|
+
params.agentId,
|
|
586
|
+
sessionId,
|
|
587
|
+
),
|
|
585
588
|
);
|
|
586
|
-
|
|
587
|
-
? metadata
|
|
588
|
-
: await ensureSessionTitle({
|
|
589
|
-
projectRoot: params.projectRoot,
|
|
590
|
-
agentId: params.agentId,
|
|
591
|
-
sessionId,
|
|
592
|
-
messages,
|
|
593
|
-
});
|
|
589
|
+
// 关键点(中文):归档 session 不再生成新 title,仅读取归档目录内已有 meta。
|
|
594
590
|
const info = buildSessionInfo({
|
|
595
591
|
projectRoot: params.projectRoot,
|
|
596
592
|
agentId: params.agentId,
|
|
597
593
|
sessionId,
|
|
598
|
-
metadata
|
|
594
|
+
metadata,
|
|
599
595
|
messages,
|
|
600
596
|
executing: false,
|
|
601
597
|
});
|
|
@@ -106,7 +106,7 @@ export interface SessionPromptRuntimeOptions {
|
|
|
106
106
|
}) => Promise<{
|
|
107
107
|
text: string;
|
|
108
108
|
success: boolean;
|
|
109
|
-
assistantMessage
|
|
109
|
+
assistantMessage?: SessionMessageV1 | null;
|
|
110
110
|
error?: string;
|
|
111
111
|
}>;
|
|
112
112
|
|
|
@@ -228,15 +228,17 @@ export class SessionPromptRuntime {
|
|
|
228
228
|
},
|
|
229
229
|
abortSignal: activeTurn.abortController.signal,
|
|
230
230
|
});
|
|
231
|
-
|
|
232
|
-
throw new Error(TURN_STOPPED_MESSAGE);
|
|
233
|
-
}
|
|
231
|
+
const stopped = activeTurn.abortController.signal.aborted;
|
|
234
232
|
const finalResult: AgentSessionTurnResult = {
|
|
235
233
|
turnId,
|
|
236
234
|
text: result.text,
|
|
237
|
-
success: result.success,
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
success: stopped ? false : result.success,
|
|
236
|
+
...(result.assistantMessage
|
|
237
|
+
? { assistantMessage: result.assistantMessage }
|
|
238
|
+
: {}),
|
|
239
|
+
...(stopped
|
|
240
|
+
? { error: TURN_STOPPED_MESSAGE }
|
|
241
|
+
: result.error ? { error: result.error } : {}),
|
|
240
242
|
};
|
|
241
243
|
activeTurn.result = finalResult;
|
|
242
244
|
this.publish({
|
|
@@ -289,7 +289,7 @@ export class SessionStateService {
|
|
|
289
289
|
* 持久化最终 assistant 结果。
|
|
290
290
|
*/
|
|
291
291
|
async persist_assistant_result(
|
|
292
|
-
assistant_message
|
|
292
|
+
assistant_message?: SessionMessageV1 | null,
|
|
293
293
|
): Promise<void> {
|
|
294
294
|
await persistSdkAssistantResult({
|
|
295
295
|
projectRoot: this.project_root,
|
|
@@ -146,7 +146,7 @@ export class SessionTurnService {
|
|
|
146
146
|
}): Promise<{
|
|
147
147
|
text: string;
|
|
148
148
|
success: boolean;
|
|
149
|
-
assistantMessage
|
|
149
|
+
assistantMessage?: SessionMessageV1 | null;
|
|
150
150
|
error?: string;
|
|
151
151
|
}> {
|
|
152
152
|
const tool_name_by_call_id = new Map<string, string>();
|
|
@@ -214,9 +214,13 @@ export class SessionTurnService {
|
|
|
214
214
|
result.deferredPersistedUserMessages,
|
|
215
215
|
);
|
|
216
216
|
return {
|
|
217
|
-
text:
|
|
217
|
+
text: result.assistantMessage
|
|
218
|
+
? extractTextFromUiMessage(result.assistantMessage)
|
|
219
|
+
: "",
|
|
218
220
|
success: result.success,
|
|
219
|
-
|
|
221
|
+
...(result.assistantMessage
|
|
222
|
+
? { assistantMessage: result.assistantMessage }
|
|
223
|
+
: {}),
|
|
220
224
|
...(result.error ? { error: result.error } : {}),
|
|
221
225
|
};
|
|
222
226
|
}
|
|
@@ -60,8 +60,12 @@ export interface PersistSdkAssistantResultParams
|
|
|
60
60
|
};
|
|
61
61
|
/**
|
|
62
62
|
* 本轮执行得到的 assistant 消息。
|
|
63
|
+
*
|
|
64
|
+
* 关键点(中文)
|
|
65
|
+
* - stop/abort 且没有 assistant 内容时允许为空。
|
|
66
|
+
* - 为空时仅刷新 metadata,不写入伪造 assistant 正文。
|
|
63
67
|
*/
|
|
64
|
-
assistantMessage
|
|
68
|
+
assistantMessage?: SessionMessageV1 | null;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -104,7 +108,9 @@ export async function persistSdkAssistantResult(
|
|
|
104
108
|
await persistAssistantResult({
|
|
105
109
|
writer: params.executor,
|
|
106
110
|
assistantMessage: params.assistantMessage,
|
|
107
|
-
fallbackText:
|
|
111
|
+
fallbackText: params.assistantMessage
|
|
112
|
+
? extractTextFromUiMessage(params.assistantMessage)
|
|
113
|
+
: undefined,
|
|
108
114
|
});
|
|
109
115
|
await touchSessionMetadata(params);
|
|
110
116
|
}
|
|
@@ -212,8 +212,8 @@ export interface AgentSessionSummary {
|
|
|
212
212
|
*
|
|
213
213
|
* 说明(中文)
|
|
214
214
|
* - 标题持久化在 session `meta.json` 顶层。
|
|
215
|
-
* -
|
|
216
|
-
* -
|
|
215
|
+
* - SDK 只在模型成功生成标题时写入,不再从首条用户消息生成 fallback。
|
|
216
|
+
* - 标题允许为空,调用方需要展示占位文案时可自行回退到 `sessionId`。
|
|
217
217
|
*/
|
|
218
218
|
title?: string;
|
|
219
219
|
/**
|