@downcity/agent 1.1.199 → 1.1.200
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/Executor.d.ts +2 -2
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +23 -24
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +5 -5
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +2 -2
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +23 -30
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.d.ts.map +1 -1
- package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js +4 -3
- package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts +1 -1
- package/bin/executor/messages/SessionMessageCodec.js +3 -3
- package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
- package/bin/executor/messages/SessionStepEventMapper.d.ts +2 -2
- package/bin/executor/messages/SessionStepEventMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionStepEventMapper.js.map +1 -1
- package/bin/executor/services/ExecutorInflightService.d.ts +2 -2
- package/bin/executor/services/ExecutorInflightService.d.ts.map +1 -1
- package/bin/executor/services/ExecutorInflightService.js +9 -5
- package/bin/executor/services/ExecutorInflightService.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +11 -11
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +9 -7
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +97 -52
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionMessages.d.ts +41 -22
- package/bin/executor/types/SessionMessages.d.ts.map +1 -1
- package/bin/executor/types/SessionMessages.js +25 -7
- package/bin/executor/types/SessionMessages.js.map +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/session/SessionTitle.d.ts.map +1 -1
- package/bin/session/SessionTitle.js +5 -0
- package/bin/session/SessionTitle.js.map +1 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +35 -32
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +8 -8
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +44 -54
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.js +3 -3
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +17 -27
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +10 -10
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +6 -6
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/sdk/AgentSessionAction.d.ts +67 -0
- package/bin/types/sdk/AgentSessionAction.d.ts.map +1 -0
- package/bin/types/sdk/AgentSessionAction.js +10 -0
- package/bin/types/sdk/AgentSessionAction.js.map +1 -0
- package/bin/types/sdk/AgentSessionEvent.d.ts +2 -2
- package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/{session-operation-message.test.mjs → session-action-message.test.mjs} +57 -55
- package/src/executor/Executor.ts +28 -29
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -5
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +1 -1
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +32 -37
- package/src/executor/composer/history/jsonl/JsonlSessionHistoryComposer.ts +7 -4
- package/src/executor/messages/SessionAttachmentMapper.ts +2 -2
- package/src/executor/messages/SessionMessageCodec.ts +3 -3
- package/src/executor/messages/SessionStepEventMapper.ts +5 -5
- package/src/executor/services/ExecutorInflightService.ts +11 -7
- package/src/executor/store/history/SessionHistoryStore.ts +11 -10
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +118 -63
- package/src/executor/types/SessionMessages.ts +69 -31
- package/src/index.ts +5 -6
- package/src/session/SessionTitle.ts +3 -0
- package/src/session/browse/Browse.ts +42 -34
- package/src/session/services/SessionStateService.ts +54 -64
- package/src/session/services/SessionTurnService.ts +3 -3
- package/src/session/services/SessionViewService.ts +17 -27
- package/src/types/agent/SessionTypes.ts +10 -10
- package/src/types/executor/SessionRunContext.ts +11 -11
- package/src/types/sdk/AgentSessionAction.ts +79 -0
- package/src/types/sdk/AgentSessionEvent.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/types/sdk/AgentSessionOperation.d.ts +0 -99
- package/bin/types/sdk/AgentSessionOperation.d.ts.map +0 -1
- package/bin/types/sdk/AgentSessionOperation.js +0 -10
- package/bin/types/sdk/AgentSessionOperation.js.map +0 -1
- package/src/types/sdk/AgentSessionOperation.ts +0 -125
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证
|
|
2
|
+
* @file 验证 action message 的持久化、模型输入过滤与 timeline 投影。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* - action message 会作为 `type=action` item 写入 JSONL。
|
|
6
|
+
* - 同一个 action id 会被更新为一条历史记录,不拆成 running/completed 多条。
|
|
7
|
+
* - history composer 组装 LLM 输入时必须过滤 action message。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
@@ -18,14 +18,14 @@ import { JsonlSessionHistoryComposer } from "../bin/executor/composer/history/js
|
|
|
18
18
|
import { toSessionTimelineEvents } from "../bin/session/browse/Browse.js";
|
|
19
19
|
import { Agent } from "../bin/index.js";
|
|
20
20
|
|
|
21
|
-
test("
|
|
21
|
+
test("action message is upserted but filtered from LLM history", async () => {
|
|
22
22
|
const root_path = await fs.mkdtemp(
|
|
23
|
-
path.join(os.tmpdir(), "downcity-agent-
|
|
23
|
+
path.join(os.tmpdir(), "downcity-agent-action-message-"),
|
|
24
24
|
);
|
|
25
|
-
const session_id = "
|
|
25
|
+
const session_id = "action_session";
|
|
26
26
|
const store = new JsonlSessionHistoryStore({
|
|
27
27
|
rootPath: root_path,
|
|
28
|
-
agentId: "
|
|
28
|
+
agentId: "action_agent",
|
|
29
29
|
sessionId: session_id,
|
|
30
30
|
});
|
|
31
31
|
const composer = new JsonlSessionHistoryComposer({ store });
|
|
@@ -38,16 +38,26 @@ test("operation message is persisted but filtered from LLM history", async () =>
|
|
|
38
38
|
}),
|
|
39
39
|
);
|
|
40
40
|
await store.append(
|
|
41
|
-
store.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
store.action({
|
|
42
|
+
action: {
|
|
43
|
+
id: "action:1",
|
|
44
|
+
title: "Compacting session history",
|
|
45
|
+
state: "running",
|
|
46
|
+
turnId: "turn-1",
|
|
47
|
+
},
|
|
48
|
+
metadata: { sessionId: session_id },
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
await store.append(
|
|
52
|
+
store.action({
|
|
53
|
+
action: {
|
|
54
|
+
id: "action:1",
|
|
55
|
+
title: "Session history compacted",
|
|
56
|
+
description: "Compacted earlier messages.",
|
|
57
|
+
state: "completed",
|
|
47
58
|
turnId: "turn-1",
|
|
48
59
|
},
|
|
49
60
|
metadata: { sessionId: session_id },
|
|
50
|
-
id: "op:1",
|
|
51
61
|
}),
|
|
52
62
|
);
|
|
53
63
|
await store.append(
|
|
@@ -60,9 +70,10 @@ test("operation message is persisted but filtered from LLM history", async () =>
|
|
|
60
70
|
|
|
61
71
|
const persisted_messages = await store.list();
|
|
62
72
|
assert.equal(persisted_messages.length, 3);
|
|
63
|
-
assert.equal(persisted_messages[1].
|
|
64
|
-
assert.equal(persisted_messages[1].
|
|
65
|
-
assert.equal(persisted_messages[1].
|
|
73
|
+
assert.equal(persisted_messages[1].type, "action");
|
|
74
|
+
assert.equal(persisted_messages[1].title, "Session history compacted");
|
|
75
|
+
assert.equal(persisted_messages[1].description, "Compacted earlier messages.");
|
|
76
|
+
assert.equal(persisted_messages[1].state, "completed");
|
|
66
77
|
|
|
67
78
|
const model_messages = await composer.prepare({
|
|
68
79
|
query: "",
|
|
@@ -78,28 +89,17 @@ test("operation message is persisted but filtered from LLM history", async () =>
|
|
|
78
89
|
|
|
79
90
|
const timeline_events = toSessionTimelineEvents(persisted_messages[1]);
|
|
80
91
|
assert.equal(timeline_events.length, 1);
|
|
81
|
-
assert.equal(timeline_events[0].role, "
|
|
82
|
-
assert.equal(timeline_events[0].
|
|
83
|
-
assert.equal(timeline_events[0].
|
|
84
|
-
assert.equal(timeline_events[0].
|
|
85
|
-
assert.equal(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
store.operation({
|
|
89
|
-
operation: {
|
|
90
|
-
operationId: "op-started",
|
|
91
|
-
name: "compacting",
|
|
92
|
-
status: "started",
|
|
93
|
-
label: "Compacting session history",
|
|
94
|
-
},
|
|
95
|
-
metadata: { sessionId: session_id },
|
|
96
|
-
id: "op:started",
|
|
97
|
-
}),
|
|
92
|
+
assert.equal(timeline_events[0].role, "action");
|
|
93
|
+
assert.equal(timeline_events[0].actionTitle, "Session history compacted");
|
|
94
|
+
assert.equal(timeline_events[0].actionDescription, "Compacted earlier messages.");
|
|
95
|
+
assert.equal(timeline_events[0].actionState, "completed");
|
|
96
|
+
assert.equal(
|
|
97
|
+
timeline_events[0].text,
|
|
98
|
+
"Session history compacted\nCompacted earlier messages.",
|
|
98
99
|
);
|
|
99
|
-
assert.deepEqual(hidden_started_events, []);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test("session.set persists and publishes model-switching
|
|
102
|
+
test("session.set persists and publishes model-switching actions", async () => {
|
|
103
103
|
const agent_path = await fs.mkdtemp(
|
|
104
104
|
path.join(os.tmpdir(), "downcity-agent-model-switching-"),
|
|
105
105
|
);
|
|
@@ -113,7 +113,7 @@ test("session.set persists and publishes model-switching operations", async () =
|
|
|
113
113
|
});
|
|
114
114
|
const events = [];
|
|
115
115
|
const unsubscribe = session.subscribe((event) => {
|
|
116
|
-
if (event.type === "
|
|
116
|
+
if (event.type === "action") events.push(event);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
await session.set({
|
|
@@ -125,24 +125,25 @@ test("session.set persists and publishes model-switching operations", async () =
|
|
|
125
125
|
unsubscribe();
|
|
126
126
|
|
|
127
127
|
assert.deepEqual(
|
|
128
|
-
events.map((event) => `${event.
|
|
129
|
-
["model
|
|
128
|
+
events.map((event) => `${event.title}:${event.state}`),
|
|
129
|
+
["Switching session model:running", "Session model switched:completed"],
|
|
130
130
|
);
|
|
131
|
+
assert.equal(new Set(events.map((event) => event.id)).size, 1);
|
|
131
132
|
|
|
132
133
|
const history = await session.history({ view: "timeline" });
|
|
133
|
-
const
|
|
134
|
-
(item) => item.role === "
|
|
134
|
+
const action_events = history.items.filter(
|
|
135
|
+
(item) => item.role === "action",
|
|
135
136
|
);
|
|
136
137
|
assert.deepEqual(
|
|
137
|
-
|
|
138
|
-
["model
|
|
138
|
+
action_events.map((event) => `${event.actionTitle}:${event.actionState}`),
|
|
139
|
+
["Session model switched:completed"],
|
|
139
140
|
);
|
|
140
141
|
} finally {
|
|
141
142
|
await agent.dispose();
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
test("session.fork persists and publishes history-forking
|
|
146
|
+
test("session.fork persists and publishes history-forking actions on source session", async () => {
|
|
146
147
|
const agent_path = await fs.mkdtemp(
|
|
147
148
|
path.join(os.tmpdir(), "downcity-agent-history-forking-"),
|
|
148
149
|
);
|
|
@@ -164,7 +165,7 @@ test("session.fork persists and publishes history-forking operations on source s
|
|
|
164
165
|
|
|
165
166
|
const events = [];
|
|
166
167
|
const unsubscribe = session.subscribe((event) => {
|
|
167
|
-
if (event.type === "
|
|
168
|
+
if (event.type === "action") {
|
|
168
169
|
events.push(event);
|
|
169
170
|
}
|
|
170
171
|
});
|
|
@@ -172,25 +173,26 @@ test("session.fork persists and publishes history-forking operations on source s
|
|
|
172
173
|
unsubscribe();
|
|
173
174
|
|
|
174
175
|
assert.deepEqual(
|
|
175
|
-
events.map((event) => `${event.
|
|
176
|
-
["history
|
|
176
|
+
events.map((event) => `${event.title}:${event.state}`),
|
|
177
|
+
["Forking session history:running", "Session history forked:completed"],
|
|
177
178
|
);
|
|
179
|
+
assert.equal(new Set(events.map((event) => event.id)).size, 1);
|
|
178
180
|
assert.notEqual(forked.id, session.id);
|
|
179
181
|
|
|
180
182
|
const source_history = await session.history({ view: "timeline" });
|
|
181
|
-
const
|
|
182
|
-
(item) => item.
|
|
183
|
+
const source_fork_actions = source_history.items.filter(
|
|
184
|
+
(item) => item.role === "action" && item.actionTitle === "Session history forked",
|
|
183
185
|
);
|
|
184
186
|
assert.deepEqual(
|
|
185
|
-
|
|
186
|
-
["
|
|
187
|
+
source_fork_actions.map((event) => event.actionState),
|
|
188
|
+
["completed"],
|
|
187
189
|
);
|
|
188
190
|
|
|
189
191
|
const forked_history = await forked.history({ view: "timeline" });
|
|
190
|
-
const
|
|
191
|
-
(item) => item.
|
|
192
|
+
const forked_model_actions = forked_history.items.filter(
|
|
193
|
+
(item) => item.role === "action" && item.actionTitle === "Session model switched",
|
|
192
194
|
);
|
|
193
|
-
assert.equal(
|
|
195
|
+
assert.equal(forked_model_actions.length, 1);
|
|
194
196
|
} finally {
|
|
195
197
|
await agent.dispose();
|
|
196
198
|
}
|
package/src/executor/Executor.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { JsonObject } from "@/types/common/Json.js";
|
|
|
28
28
|
import type { SessionMessageV1 } from "@/executor/types/SessionMessages.js";
|
|
29
29
|
import type { SessionExecutor } from "@/executor/types/SessionExecutor.js";
|
|
30
30
|
import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
|
|
31
|
-
import type {
|
|
31
|
+
import type { AgentSessionActionRecord } from "@/types/sdk/AgentSessionAction.js";
|
|
32
32
|
import type {
|
|
33
33
|
SessionExecuteInput,
|
|
34
34
|
SessionRunResult,
|
|
@@ -362,7 +362,7 @@ export class Executor implements SessionExecutor {
|
|
|
362
362
|
const composed_context = await this.contextComposer.compose(run_context);
|
|
363
363
|
const tools = this.bind_run_scope_to_tools(composed_context.tools, run_context);
|
|
364
364
|
const system = await this.systemComposer.resolve(run_context);
|
|
365
|
-
let
|
|
365
|
+
let compaction_action_id = "";
|
|
366
366
|
|
|
367
367
|
try {
|
|
368
368
|
if (retry_count > 0) {
|
|
@@ -371,13 +371,13 @@ export class Executor implements SessionExecutor {
|
|
|
371
371
|
});
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
const
|
|
375
|
-
|
|
374
|
+
const emit_compaction_action = async (
|
|
375
|
+
action: AgentSessionActionRecord,
|
|
376
376
|
): Promise<void> => {
|
|
377
|
-
if (
|
|
378
|
-
|
|
377
|
+
if (action.state === "running") {
|
|
378
|
+
compaction_action_id = action.id;
|
|
379
379
|
}
|
|
380
|
-
await this.
|
|
380
|
+
await this.emitAction(run_context, action);
|
|
381
381
|
};
|
|
382
382
|
|
|
383
383
|
await this.compactionComposer.run({
|
|
@@ -385,17 +385,16 @@ export class Executor implements SessionExecutor {
|
|
|
385
385
|
model,
|
|
386
386
|
system,
|
|
387
387
|
retryCount: retry_count,
|
|
388
|
-
|
|
388
|
+
onAction: emit_compaction_action,
|
|
389
389
|
});
|
|
390
390
|
} catch (error) {
|
|
391
|
-
await this.
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
await this.emitAction(run_context, {
|
|
392
|
+
id:
|
|
393
|
+
compaction_action_id ||
|
|
394
394
|
`compacting:${this.sessionId}:failed:${Date.now()}:${generateId()}`,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
error: error instanceof Error ? error.message : String(error),
|
|
395
|
+
title: "Session history compact failed",
|
|
396
|
+
description: error instanceof Error ? error.message : String(error),
|
|
397
|
+
state: "failed",
|
|
399
398
|
visible: false,
|
|
400
399
|
});
|
|
401
400
|
// 压缩失败不阻断主流程,继续使用当前历史消息执行。
|
|
@@ -492,8 +491,8 @@ export class Executor implements SessionExecutor {
|
|
|
492
491
|
...(typeof input?.onUiMessageChunkCallback === "function"
|
|
493
492
|
? { onUiMessageChunkCallback: input.onUiMessageChunkCallback }
|
|
494
493
|
: {}),
|
|
495
|
-
...(typeof input?.
|
|
496
|
-
? {
|
|
494
|
+
...(typeof input?.onActionCallback === "function"
|
|
495
|
+
? { onActionCallback: input.onActionCallback }
|
|
497
496
|
: {}),
|
|
498
497
|
...(input?.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
499
498
|
injectedUserMessages: Array.isArray(input?.injectedUserMessages)
|
|
@@ -511,22 +510,22 @@ export class Executor implements SessionExecutor {
|
|
|
511
510
|
}
|
|
512
511
|
|
|
513
512
|
/**
|
|
514
|
-
* 发布一次 session
|
|
513
|
+
* 发布一次 session action。
|
|
515
514
|
*/
|
|
516
|
-
private async
|
|
515
|
+
private async emitAction(
|
|
517
516
|
run_context: SessionRunContext,
|
|
518
|
-
|
|
517
|
+
action: AgentSessionActionRecord,
|
|
519
518
|
): Promise<void> {
|
|
520
|
-
if (typeof run_context.
|
|
521
|
-
const
|
|
522
|
-
String(
|
|
523
|
-
`
|
|
524
|
-
await run_context.
|
|
525
|
-
type: "
|
|
519
|
+
if (typeof run_context.onActionCallback !== "function") return;
|
|
520
|
+
const action_id =
|
|
521
|
+
String(action.id || "").trim() ||
|
|
522
|
+
`action:${this.sessionId}:${Date.now()}:${generateId()}`;
|
|
523
|
+
await run_context.onActionCallback({
|
|
524
|
+
type: "action",
|
|
526
525
|
sessionId: run_context.sessionId || this.sessionId,
|
|
527
|
-
...
|
|
528
|
-
|
|
529
|
-
...(run_context.turnId && !
|
|
526
|
+
...action,
|
|
527
|
+
id: action_id,
|
|
528
|
+
...(run_context.turnId && !action.turnId
|
|
530
529
|
? { turnId: run_context.turnId }
|
|
531
530
|
: {}),
|
|
532
531
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { LanguageModel, SystemModelMessage } from "ai";
|
|
10
10
|
import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type { AgentSessionActionRecord } from "@/types/sdk/AgentSessionAction.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* compaction Composer 执行输入。
|
|
@@ -35,13 +35,13 @@ export type SessionCompactionInput = {
|
|
|
35
35
|
retryCount: number;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* 可选
|
|
38
|
+
* 可选 action 发布回调。
|
|
39
39
|
*
|
|
40
40
|
* 说明(中文)
|
|
41
|
-
* - 默认 compaction Composer 可在真正开始压缩时发布
|
|
42
|
-
* - 回调由上层 Session 负责转成订阅事件与
|
|
41
|
+
* - 默认 compaction Composer 可在真正开始压缩时发布 action。
|
|
42
|
+
* - 回调由上层 Session 负责转成订阅事件与 action message。
|
|
43
43
|
*/
|
|
44
|
-
|
|
44
|
+
onAction?: (action: AgentSessionActionRecord) => Promise<void>;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -79,7 +79,7 @@ export class JsonlSessionCompactionComposer implements SessionCompactionComposer
|
|
|
79
79
|
maxInputTokensApprox: policy.maxInputTokensApprox,
|
|
80
80
|
archiveOnCompact: policy.archiveOnCompact,
|
|
81
81
|
compactRatio: policy.compactRatio,
|
|
82
|
-
...(input.
|
|
82
|
+
...(input.onAction ? { onAction: input.onAction } : {}),
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -20,10 +20,14 @@ import { getLogger } from "@/utils/logger/Logger.js";
|
|
|
20
20
|
import type {
|
|
21
21
|
SessionMessageV1,
|
|
22
22
|
SessionMetadataV1,
|
|
23
|
+
SessionModelMessageV1,
|
|
24
|
+
} from "@/executor/types/SessionMessages.js";
|
|
25
|
+
import {
|
|
26
|
+
isSessionActionMessage,
|
|
27
|
+
isSessionModelMessage,
|
|
23
28
|
} from "@/executor/types/SessionMessages.js";
|
|
24
|
-
import { isSessionOperationMessage } from "@/executor/types/SessionMessages.js";
|
|
25
29
|
import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
|
|
26
|
-
import type {
|
|
30
|
+
import type { AgentSessionActionRecord } from "@/types/sdk/AgentSessionAction.js";
|
|
27
31
|
|
|
28
32
|
export type SessionCompactParams = {
|
|
29
33
|
model: LanguageModel;
|
|
@@ -32,7 +36,7 @@ export type SessionCompactParams = {
|
|
|
32
36
|
maxInputTokensApprox: number;
|
|
33
37
|
archiveOnCompact: boolean;
|
|
34
38
|
compactRatio: number;
|
|
35
|
-
|
|
39
|
+
onAction?: (action: AgentSessionActionRecord) => Promise<void>;
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
type SessionCompactDeps = {
|
|
@@ -63,17 +67,16 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
63
67
|
params: SessionCompactParams,
|
|
64
68
|
): Promise<{ compacted: boolean; reason?: string }> {
|
|
65
69
|
const logger = getLogger(deps.rootPath, "info");
|
|
66
|
-
const
|
|
67
|
-
let
|
|
70
|
+
const action_id = `compacting:${deps.sessionId}:${Date.now()}:${generateId()}`;
|
|
71
|
+
let action_started = false;
|
|
68
72
|
|
|
69
|
-
const
|
|
70
|
-
|
|
73
|
+
const publish_action = async (
|
|
74
|
+
action: Omit<AgentSessionActionRecord, "id">,
|
|
71
75
|
): Promise<void> => {
|
|
72
|
-
if (typeof params.
|
|
73
|
-
await params.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
...operation,
|
|
76
|
+
if (typeof params.onAction !== "function") return;
|
|
77
|
+
await params.onAction({
|
|
78
|
+
id: action_id,
|
|
79
|
+
...action,
|
|
77
80
|
});
|
|
78
81
|
};
|
|
79
82
|
|
|
@@ -81,12 +84,10 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
81
84
|
// phase 1:snapshot(短锁)
|
|
82
85
|
// - 仅负责拿一致性快照,不做耗时的模型调用。
|
|
83
86
|
// - 目的是把锁持有时间降到最低。
|
|
84
|
-
let snapshot:
|
|
87
|
+
let snapshot: SessionModelMessageV1[] = [];
|
|
85
88
|
let snapshotTailId = "";
|
|
86
89
|
await deps.withWriteLock(async () => {
|
|
87
|
-
snapshot = (await deps.loadAll()).filter(
|
|
88
|
-
(message) => !isSessionOperationMessage(message),
|
|
89
|
-
);
|
|
90
|
+
snapshot = (await deps.loadAll()).filter(isSessionModelMessage);
|
|
90
91
|
snapshotTailId =
|
|
91
92
|
snapshot.length > 0
|
|
92
93
|
? String(snapshot[snapshot.length - 1].id || "")
|
|
@@ -118,11 +119,10 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
118
119
|
const older = snapshot.slice(0, compactCount);
|
|
119
120
|
if (older.length === 0) return { compacted: false, reason: "nothing_to_compact" };
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
progress: 0,
|
|
122
|
+
action_started = true;
|
|
123
|
+
await publish_action({
|
|
124
|
+
title: "Compacting session history",
|
|
125
|
+
state: "running",
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
const olderTextAll = extractPlainTextFromMessages(older);
|
|
@@ -163,11 +163,10 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
163
163
|
},
|
|
164
164
|
);
|
|
165
165
|
summary = "(系统自动压缩:摘要生成失败,已丢弃更早历史,仅保留最近对话。)";
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
progress: 0.5,
|
|
166
|
+
await publish_action({
|
|
167
|
+
title: "Compacting session history",
|
|
168
|
+
description: "Summary generation failed; using a fallback summary.",
|
|
169
|
+
state: "running",
|
|
171
170
|
});
|
|
172
171
|
}
|
|
173
172
|
|
|
@@ -191,7 +190,7 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
191
190
|
void snapshotTailId;
|
|
192
191
|
|
|
193
192
|
const current_model_messages = current.filter(
|
|
194
|
-
|
|
193
|
+
isSessionModelMessage,
|
|
195
194
|
);
|
|
196
195
|
const currentCompactCount = resolveCompactCount(
|
|
197
196
|
current_model_messages.length,
|
|
@@ -242,15 +241,11 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
242
241
|
});
|
|
243
242
|
});
|
|
244
243
|
|
|
245
|
-
if (
|
|
246
|
-
await
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
result: {
|
|
251
|
-
archiveId,
|
|
252
|
-
compactedCount: older.length,
|
|
253
|
-
},
|
|
244
|
+
if (action_started) {
|
|
245
|
+
await publish_action({
|
|
246
|
+
title: "Session history compacted",
|
|
247
|
+
description: `Compacted ${String(older.length)} earlier messages into ${archiveId}.`,
|
|
248
|
+
state: "completed",
|
|
254
249
|
});
|
|
255
250
|
}
|
|
256
251
|
|
|
@@ -302,7 +297,7 @@ function estimateTokensApproxFromText(text: string): number {
|
|
|
302
297
|
* - 统一把 user / assistant 内容线性化,作为 compact 摘要输入。
|
|
303
298
|
* - tool 原始结构不会原样输出,避免把噪声日志喂给摘要模型。
|
|
304
299
|
*/
|
|
305
|
-
function extractPlainTextFromMessages(messages:
|
|
300
|
+
function extractPlainTextFromMessages(messages: SessionModelMessageV1[]): string {
|
|
306
301
|
const lines: string[] = [];
|
|
307
302
|
for (const m of messages) {
|
|
308
303
|
if (!m || typeof m !== "object") continue;
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Tool } from "ai";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
isSessionActionMessage,
|
|
13
|
+
isSessionModelMessage,
|
|
13
14
|
type SessionMessageV1,
|
|
15
|
+
type SessionModelMessageV1,
|
|
14
16
|
} from "@/executor/types/SessionMessages.js";
|
|
15
17
|
import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
|
|
16
18
|
import type {
|
|
@@ -49,7 +51,7 @@ export class JsonlSessionHistoryComposer implements SessionHistoryComposer {
|
|
|
49
51
|
return tools && typeof tools === "object" ? { ...tools } : {};
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
private readUserSessionMessageText(message:
|
|
54
|
+
private readUserSessionMessageText(message: SessionModelMessageV1): string {
|
|
53
55
|
if (!message || typeof message !== "object" || message.role !== "user") {
|
|
54
56
|
return "";
|
|
55
57
|
}
|
|
@@ -76,7 +78,7 @@ export class JsonlSessionHistoryComposer implements SessionHistoryComposer {
|
|
|
76
78
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
77
79
|
const item = messages[index];
|
|
78
80
|
if (!item || typeof item !== "object") continue;
|
|
79
|
-
if (item.role !== "user") continue;
|
|
81
|
+
if (!isSessionModelMessage(item) || item.role !== "user") continue;
|
|
80
82
|
return this.readUserSessionMessageText(item) === target;
|
|
81
83
|
}
|
|
82
84
|
return false;
|
|
@@ -85,6 +87,8 @@ export class JsonlSessionHistoryComposer implements SessionHistoryComposer {
|
|
|
85
87
|
private sanitizeMessages(messages: SessionMessageV1[]): SessionMessageV1[] {
|
|
86
88
|
if (!Array.isArray(messages)) return [];
|
|
87
89
|
return messages
|
|
90
|
+
.filter((message) => !isSessionActionMessage(message))
|
|
91
|
+
.filter(isSessionModelMessage)
|
|
88
92
|
.map((message) => {
|
|
89
93
|
const parts = Array.isArray(message.parts)
|
|
90
94
|
? message.parts.filter((part) => part && typeof part === "object")
|
|
@@ -94,7 +98,6 @@ export class JsonlSessionHistoryComposer implements SessionHistoryComposer {
|
|
|
94
98
|
parts,
|
|
95
99
|
};
|
|
96
100
|
})
|
|
97
|
-
.filter((message) => !isSessionOperationMessage(message))
|
|
98
101
|
.filter((message) => Array.isArray(message.parts) && message.parts.length > 0);
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -187,7 +187,7 @@ export async function hydrateFileUrlPartsForModel(
|
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
const nextParts:
|
|
190
|
+
const nextParts: SessionModelMessageV1["parts"] = [];
|
|
191
191
|
let changed = false;
|
|
192
192
|
for (const part of parts) {
|
|
193
193
|
if (!isFileUIPart(part as FileUIPart)) {
|
|
@@ -196,7 +196,7 @@ export async function hydrateFileUrlPartsForModel(
|
|
|
196
196
|
}
|
|
197
197
|
const nextPart = await hydrateFileUrlPart(part as FileUIPart, projectRoot);
|
|
198
198
|
if (nextPart !== part) changed = true;
|
|
199
|
-
nextParts.push(nextPart as
|
|
199
|
+
nextParts.push(nextPart as SessionModelMessageV1["parts"][number]);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
out.push(changed ? { ...message, parts: nextParts } : message);
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
*
|
|
29
29
|
* 关键点(中文)
|
|
30
30
|
* - 用途:从 onStepCallback 返回的消息里挑出可并入推理上下文的 user 文本。
|
|
31
|
-
* - 输入:任意 SessionMessageV1[](可能混有 assistant/tool/空消息)。
|
|
31
|
+
* - 输入:任意 SessionMessageV1[](可能混有 assistant/tool/action/空消息)。
|
|
32
32
|
* - 输出:只包含“非空 user 文本”的消息数组。
|
|
33
33
|
*/
|
|
34
34
|
export function pickMergedUserMessages(
|
|
@@ -40,7 +40,7 @@ export function pickMergedUserMessages(
|
|
|
40
40
|
// 逐条过滤消息。
|
|
41
41
|
return messages.filter((message) => {
|
|
42
42
|
// 防御 1:消息必须是对象。
|
|
43
|
-
if (!message
|
|
43
|
+
if (!isSessionModelMessage(message)) return false;
|
|
44
44
|
|
|
45
45
|
// 防御 2:只接受 user 角色。
|
|
46
46
|
if (message.role !== "user") return false;
|
|
@@ -80,7 +80,7 @@ export async function toModelMessages(
|
|
|
80
80
|
// 空输入快速返回,避免调用转换器的额外开销。
|
|
81
81
|
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
82
82
|
|
|
83
|
-
//
|
|
83
|
+
// action message 只服务前端时间线,不能传入模型。
|
|
84
84
|
const model_messages = messages.filter(isSessionModelMessage);
|
|
85
85
|
if (model_messages.length === 0) return [];
|
|
86
86
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 每个 step 前插入 `step-start`,便于在 UI 中保留多 step 边界。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { SessionModelMessageV1 } from "@/executor/types/SessionMessages.js";
|
|
11
11
|
|
|
12
12
|
type UnknownRecord = Record<string, unknown>;
|
|
13
13
|
|
|
@@ -112,8 +112,8 @@ export function buildSessionStepParts(params: {
|
|
|
112
112
|
stepResult?: unknown;
|
|
113
113
|
text: string;
|
|
114
114
|
visibility?: "visible" | "internal";
|
|
115
|
-
}):
|
|
116
|
-
const out:
|
|
115
|
+
}): SessionModelMessageV1["parts"] {
|
|
116
|
+
const out: SessionModelMessageV1["parts"] = [];
|
|
117
117
|
const stepRecord = toRecord(params.stepResult) || {};
|
|
118
118
|
const toolCalls = Array.isArray(stepRecord.toolCalls) ? stepRecord.toolCalls : [];
|
|
119
119
|
const toolResults = Array.isArray(stepRecord.toolResults)
|
|
@@ -171,7 +171,7 @@ export function buildSessionStepParts(params: {
|
|
|
171
171
|
state: "output-available",
|
|
172
172
|
output,
|
|
173
173
|
}),
|
|
174
|
-
} as
|
|
174
|
+
} as SessionModelMessageV1["parts"][number],
|
|
175
175
|
);
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -183,7 +183,7 @@ export function buildSessionStepParts(params: {
|
|
|
183
183
|
{
|
|
184
184
|
type: toDataPartType(type),
|
|
185
185
|
data: resolveAcpEventData(acpEvent),
|
|
186
|
-
} as
|
|
186
|
+
} as SessionModelMessageV1["parts"][number],
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { generateId } from "@/utils/Id.js";
|
|
11
11
|
import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
|
|
12
|
-
import type {
|
|
12
|
+
import type { SessionModelMessageV1 } from "@/executor/types/SessionMessages.js";
|
|
13
|
+
import { isSessionModelMessage } from "@/executor/types/SessionMessages.js";
|
|
13
14
|
|
|
14
15
|
interface ExecutorInflightServiceOptions {
|
|
15
16
|
/**
|
|
@@ -51,7 +52,7 @@ export class ExecutorInflightService {
|
|
|
51
52
|
* 把 step/tool 过程增量写入当前运行中的 assistant 快照。
|
|
52
53
|
*/
|
|
53
54
|
async append_assistant_step_parts(
|
|
54
|
-
parts:
|
|
55
|
+
parts: SessionModelMessageV1["parts"],
|
|
55
56
|
): Promise<void> {
|
|
56
57
|
const normalized_parts = Array.isArray(parts)
|
|
57
58
|
? parts.filter((part) => part && typeof part === "object")
|
|
@@ -59,11 +60,14 @@ export class ExecutorInflightService {
|
|
|
59
60
|
if (normalized_parts.length === 0) return;
|
|
60
61
|
|
|
61
62
|
const current_inflight = await this.history_store.readInflight();
|
|
62
|
-
const
|
|
63
|
+
const normalized_inflight = isSessionModelMessage(current_inflight)
|
|
64
|
+
? current_inflight
|
|
65
|
+
: null;
|
|
66
|
+
const next_message: SessionModelMessageV1 = normalized_inflight
|
|
63
67
|
? {
|
|
64
|
-
...
|
|
68
|
+
...normalized_inflight,
|
|
65
69
|
metadata: {
|
|
66
|
-
...(
|
|
70
|
+
...(normalized_inflight.metadata || {
|
|
67
71
|
v: 1 as const,
|
|
68
72
|
ts: Date.now(),
|
|
69
73
|
sessionId: this.session_id,
|
|
@@ -74,8 +78,8 @@ export class ExecutorInflightService {
|
|
|
74
78
|
kind: "normal",
|
|
75
79
|
},
|
|
76
80
|
parts: [
|
|
77
|
-
...(Array.isArray(
|
|
78
|
-
?
|
|
81
|
+
...(Array.isArray(normalized_inflight.parts)
|
|
82
|
+
? normalized_inflight.parts
|
|
79
83
|
: []),
|
|
80
84
|
...normalized_parts,
|
|
81
85
|
],
|