@downcity/agent 1.1.216 → 1.1.222
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/agent/local/Agent.d.ts +2 -0
- package/bin/agent/local/Agent.d.ts.map +1 -1
- package/bin/agent/local/Agent.js +6 -0
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +6 -0
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +8 -1
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +11 -7
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +36 -11
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +9 -3
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +6 -2
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +38 -11
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +6 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/types/SessionHistoryMeta.d.ts +2 -0
- package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
- package/bin/executor/types/SessionRecords.d.ts +2 -0
- package/bin/executor/types/SessionRecords.d.ts.map +1 -1
- package/bin/executor/types/SessionRecords.js.map +1 -1
- package/bin/index.d.ts +5 -2
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -0
- package/bin/index.js.map +1 -1
- package/bin/rpc/Client.d.ts +18 -4
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +25 -3
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +13 -3
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +48 -18
- package/bin/session/Session.js.map +1 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +3 -0
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +45 -0
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
- package/bin/session/recorder/JsonlSessionMessageStore.js +127 -0
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
- package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
- package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
- package/bin/session/recorder/SessionMessageCodec.js +252 -0
- package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
- package/bin/session/recorder/SessionMessageReducer.d.ts +12 -0
- package/bin/session/recorder/SessionMessageReducer.d.ts.map +1 -0
- package/bin/session/recorder/SessionMessageReducer.js +109 -0
- package/bin/session/recorder/SessionMessageReducer.js.map +1 -0
- package/bin/session/recorder/SessionRecorder.d.ts +177 -0
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorder.js +610 -0
- package/bin/session/recorder/SessionRecorder.js.map +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.js +141 -0
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
- package/bin/session/runtime/SessionMessageEventHub.d.ts +15 -0
- package/bin/session/runtime/SessionMessageEventHub.d.ts.map +1 -0
- package/bin/session/runtime/SessionMessageEventHub.js +28 -0
- package/bin/session/runtime/SessionMessageEventHub.js.map +1 -0
- package/bin/session/runtime/SessionPromptRuntime.d.ts +7 -13
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +13 -13
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +15 -3
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +165 -76
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +4 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +82 -52
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts +8 -3
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +13 -5
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/bin/session/storage/Metadata.d.ts +2 -0
- package/bin/session/storage/Metadata.d.ts.map +1 -1
- package/bin/session/storage/Metadata.js +10 -0
- package/bin/session/storage/Metadata.js.map +1 -1
- package/bin/session/storage/Persistence.d.ts +6 -0
- package/bin/session/storage/Persistence.d.ts.map +1 -1
- package/bin/session/storage/Persistence.js +12 -0
- package/bin/session/storage/Persistence.js.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.d.ts +3 -2
- package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
- package/bin/types/agent/AgentOptions.d.ts +10 -0
- package/bin/types/agent/AgentOptions.d.ts.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +12 -4
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +13 -1
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +32 -6
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +4 -3
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
- package/bin/types/session/SessionMessage.d.ts +217 -0
- package/bin/types/session/SessionMessage.d.ts.map +1 -0
- package/bin/types/session/SessionMessage.js +8 -0
- package/bin/types/session/SessionMessage.js.map +1 -0
- package/bin/types/session/SessionMessageMutation.d.ts +92 -0
- package/bin/types/session/SessionMessageMutation.d.ts.map +1 -0
- package/bin/types/session/SessionMessageMutation.js +21 -0
- package/bin/types/session/SessionMessageMutation.js.map +1 -0
- package/bin/types/session/SessionOptions.d.ts +3 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/agent-env.test.mjs +4 -4
- package/scripts/remote-session-reconnect.test.mjs +33 -0
- package/scripts/session-action-message.test.mjs +148 -27
- package/scripts/session-list-title.test.mjs +6 -19
- package/scripts/session-prompt-runtime.test.mjs +4 -16
- package/scripts/session-recorder.test.mjs +245 -0
- package/scripts/session-title-event.test.mjs +10 -18
- package/src/agent/local/Agent.ts +6 -0
- package/src/agent/local/services/AgentSessions.ts +14 -1
- package/src/agent/remote/RemoteSession.ts +52 -16
- package/src/agent/remote/RemoteTransport.ts +20 -6
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +67 -13
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +27 -6
- package/src/executor/types/SessionHistoryMeta.ts +2 -0
- package/src/executor/types/SessionRecords.ts +2 -0
- package/src/index.ts +23 -5
- package/src/rpc/Client.ts +47 -8
- package/src/session/Session.ts +66 -31
- package/src/session/browse/Browse.ts +3 -0
- package/src/session/recorder/JsonlSessionMessageStore.ts +162 -0
- package/src/session/recorder/SessionMessageCodec.ts +279 -0
- package/src/session/recorder/SessionMessageReducer.ts +155 -0
- package/src/session/recorder/SessionRecorder.ts +812 -0
- package/src/session/recorder/SessionRecorderCompaction.ts +187 -0
- package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
- package/src/session/runtime/SessionMessageEventHub.ts +35 -0
- package/src/session/runtime/SessionPromptRuntime.ts +23 -24
- package/src/session/services/SessionStateService.ts +189 -84
- package/src/session/services/SessionTurnService.ts +88 -59
- package/src/session/services/SessionViewService.ts +28 -13
- package/src/session/storage/Metadata.ts +13 -0
- package/src/session/storage/Persistence.ts +18 -0
- package/src/session/storage/RuntimeSessionPort.ts +6 -4
- package/src/types/agent/AgentOptions.ts +12 -0
- package/src/types/agent/SessionActor.ts +28 -5
- package/src/types/agent/SessionTypes.ts +13 -1
- package/src/types/rpc/RpcProtocol.ts +34 -6
- package/src/types/runtime/agent/AgentContext.ts +8 -2
- package/src/types/sdk/AgentSessionEvent.ts +2 -0
- package/src/types/session/SessionMessage.ts +247 -0
- package/src/types/session/SessionMessageMutation.ts +121 -0
- package/src/types/session/SessionOptions.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/session/SessionEventMapper.d.ts +0 -32
- package/bin/session/SessionEventMapper.d.ts.map +0 -1
- package/bin/session/SessionEventMapper.js +0 -131
- package/bin/session/SessionEventMapper.js.map +0 -1
- package/bin/types/sdk/InternalUiChunkEvent.d.ts +0 -100
- package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
- package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
- package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
- package/scripts/session-history-archive.test.mjs +0 -188
- package/src/session/SessionEventMapper.ts +0 -148
- package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
|
@@ -19,10 +19,9 @@ import {
|
|
|
19
19
|
} from "@/session/storage/Metadata.js";
|
|
20
20
|
import { touchSessionMetadata } from "@/session/storage/Persistence.js";
|
|
21
21
|
import { ensureSessionTitle } from "@/session/SessionTitle.js";
|
|
22
|
-
import { persistSdkAssistantResult } from "@/session/storage/Persistence.js";
|
|
23
22
|
import { hydrateUserPromptFileParts } from "@executor/messages/SessionAttachmentMapper.js";
|
|
24
23
|
import type { Executor } from "@executor/Executor.js";
|
|
25
|
-
import type {
|
|
24
|
+
import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
|
|
26
25
|
import type {
|
|
27
26
|
AgentSessionConfigSnapshot,
|
|
28
27
|
AgentSessionSetInput,
|
|
@@ -38,10 +37,20 @@ import type {
|
|
|
38
37
|
SessionMessageRecordV1,
|
|
39
38
|
SessionUserMessageV1,
|
|
40
39
|
} from "@/executor/types/SessionRecords.js";
|
|
41
|
-
import { to_session_action_record } from "@/executor/types/SessionRecords.js";
|
|
42
40
|
import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
|
|
43
41
|
import { generateId } from "@/utils/Id.js";
|
|
44
42
|
import type { Logger } from "@/utils/logger/Logger.js";
|
|
43
|
+
import type { AgentModel } from "@/model/CityModelAdapter.js";
|
|
44
|
+
import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
|
|
45
|
+
import { normalize_session_user_parts } from "@/session/recorder/SessionRecorder.js";
|
|
46
|
+
import {
|
|
47
|
+
from_ui_assistant_parts,
|
|
48
|
+
from_ui_user_parts,
|
|
49
|
+
to_executor_ui_message,
|
|
50
|
+
} from "@/session/recorder/SessionMessageCodec.js";
|
|
51
|
+
import fs from "fs-extra";
|
|
52
|
+
import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
|
|
53
|
+
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
45
54
|
|
|
46
55
|
type SessionStateServiceOptions = {
|
|
47
56
|
/**
|
|
@@ -62,7 +71,10 @@ type SessionStateServiceOptions = {
|
|
|
62
71
|
/**
|
|
63
72
|
* 当前 session 历史事实源。
|
|
64
73
|
*/
|
|
65
|
-
history_store:
|
|
74
|
+
history_store: SessionHistoryStore;
|
|
75
|
+
|
|
76
|
+
/** 当前 Session Message Recorder。 */
|
|
77
|
+
recorder: SessionRecorder;
|
|
66
78
|
|
|
67
79
|
/**
|
|
68
80
|
* 当前 session 执行器。
|
|
@@ -84,6 +96,9 @@ type SessionStateServiceOptions = {
|
|
|
84
96
|
*/
|
|
85
97
|
ensure_configured_hook?: () => Promise<void>;
|
|
86
98
|
|
|
99
|
+
/** 按稳定 ID 解析当前 Session 的运行时模型。 */
|
|
100
|
+
resolve_model?: (model_id: string) => Promise<AgentModel>;
|
|
101
|
+
|
|
87
102
|
/**
|
|
88
103
|
* 发布 session 事件。
|
|
89
104
|
*/
|
|
@@ -110,11 +125,13 @@ export class SessionStateService {
|
|
|
110
125
|
private readonly agent_id: string;
|
|
111
126
|
private readonly project_root: string;
|
|
112
127
|
private readonly session_id: string;
|
|
113
|
-
private readonly history_store:
|
|
128
|
+
private readonly history_store: SessionHistoryStore;
|
|
129
|
+
private readonly recorder: SessionRecorder;
|
|
114
130
|
private readonly executor: Executor;
|
|
115
131
|
private readonly state: SessionLocalState;
|
|
116
132
|
private readonly logger: Logger;
|
|
117
133
|
private readonly ensure_configured_hook?: SessionStateServiceOptions["ensure_configured_hook"];
|
|
134
|
+
private readonly resolve_model?: SessionStateServiceOptions["resolve_model"];
|
|
118
135
|
private readonly publish_event: SessionStateServiceOptions["publish_event"];
|
|
119
136
|
|
|
120
137
|
constructor(options: SessionStateServiceOptions) {
|
|
@@ -122,10 +139,12 @@ export class SessionStateService {
|
|
|
122
139
|
this.project_root = options.project_root;
|
|
123
140
|
this.session_id = options.session_id;
|
|
124
141
|
this.history_store = options.history_store;
|
|
142
|
+
this.recorder = options.recorder;
|
|
125
143
|
this.executor = options.executor;
|
|
126
144
|
this.state = options.state;
|
|
127
145
|
this.logger = options.logger;
|
|
128
146
|
this.ensure_configured_hook = options.ensure_configured_hook;
|
|
147
|
+
this.resolve_model = options.resolve_model;
|
|
129
148
|
this.publish_event = options.publish_event;
|
|
130
149
|
}
|
|
131
150
|
|
|
@@ -189,7 +208,9 @@ export class SessionStateService {
|
|
|
189
208
|
...(metadata.modelLabel
|
|
190
209
|
? { modelLabel: metadata.modelLabel }
|
|
191
210
|
: {}),
|
|
211
|
+
...(metadata.modelId ? { modelId: metadata.modelId } : {}),
|
|
192
212
|
};
|
|
213
|
+
await this.restore_persisted_model();
|
|
193
214
|
})();
|
|
194
215
|
await this.state.initializePromise;
|
|
195
216
|
}
|
|
@@ -231,46 +252,57 @@ export class SessionStateService {
|
|
|
231
252
|
async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
|
|
232
253
|
const should_emit_action = options?.emit_action !== false;
|
|
233
254
|
const previous_model_label = this.state.sessionConfig.modelLabel;
|
|
234
|
-
const
|
|
235
|
-
|
|
255
|
+
const previous_model_id = String(
|
|
256
|
+
this.state.sessionConfig.modelId || previous_model_label || "",
|
|
257
|
+
).trim();
|
|
258
|
+
const requested_model_id = String(input.modelId || "").trim();
|
|
259
|
+
const next_model = input.model || (requested_model_id
|
|
260
|
+
? await this.resolve_model_by_id(requested_model_id)
|
|
261
|
+
: undefined);
|
|
262
|
+
const next_model_label = next_model
|
|
263
|
+
? inferAgentModelLabel(next_model)
|
|
236
264
|
: undefined;
|
|
265
|
+
const next_model_id = requested_model_id || next_model_label || "";
|
|
237
266
|
const should_emit_model_switch_action = Boolean(
|
|
238
|
-
|
|
267
|
+
next_model &&
|
|
239
268
|
should_emit_action &&
|
|
240
269
|
this.state.sessionConfig.model &&
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
270
|
+
previous_model_id &&
|
|
271
|
+
next_model_id &&
|
|
272
|
+
previous_model_id !== next_model_id,
|
|
244
273
|
);
|
|
274
|
+
const previous_model_name = previous_model_label || previous_model_id;
|
|
275
|
+
const next_model_name = next_model_label || next_model_id;
|
|
245
276
|
const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
|
|
246
277
|
|
|
247
278
|
if (should_emit_model_switch_action) {
|
|
248
279
|
await this.emit_action_event({
|
|
249
280
|
id: action_id,
|
|
250
|
-
title:
|
|
251
|
-
description: next_model_label
|
|
252
|
-
? `Switching to ${next_model_label}.`
|
|
253
|
-
: undefined,
|
|
281
|
+
title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
|
|
254
282
|
state: "running",
|
|
255
283
|
});
|
|
256
284
|
}
|
|
257
285
|
|
|
258
286
|
try {
|
|
259
|
-
if (
|
|
260
|
-
this.state.sessionConfig.model = normalizeAgentModel(
|
|
287
|
+
if (next_model) {
|
|
288
|
+
this.state.sessionConfig.model = normalizeAgentModel(next_model);
|
|
261
289
|
this.state.sessionConfig.modelLabel = next_model_label;
|
|
262
290
|
}
|
|
291
|
+
if (next_model_id) {
|
|
292
|
+
this.state.sessionConfig.modelId = next_model_id;
|
|
293
|
+
}
|
|
263
294
|
await patchSessionModelLabel({
|
|
264
295
|
projectRoot: this.project_root,
|
|
265
296
|
agentId: this.agent_id,
|
|
266
297
|
sessionId: this.session_id,
|
|
267
298
|
model: this.state.sessionConfig.model,
|
|
299
|
+
modelId: this.state.sessionConfig.modelId,
|
|
268
300
|
});
|
|
269
301
|
} catch (error) {
|
|
270
302
|
if (should_emit_model_switch_action) {
|
|
271
303
|
await this.emit_action_event({
|
|
272
304
|
id: action_id,
|
|
273
|
-
title:
|
|
305
|
+
title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
|
|
274
306
|
description: error instanceof Error ? error.message : String(error),
|
|
275
307
|
state: "failed",
|
|
276
308
|
});
|
|
@@ -281,15 +313,29 @@ export class SessionStateService {
|
|
|
281
313
|
if (should_emit_model_switch_action) {
|
|
282
314
|
await this.emit_action_event({
|
|
283
315
|
id: action_id,
|
|
284
|
-
title:
|
|
285
|
-
description: this.state.sessionConfig.modelLabel
|
|
286
|
-
? `Using ${this.state.sessionConfig.modelLabel}.`
|
|
287
|
-
: undefined,
|
|
316
|
+
title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
|
|
288
317
|
state: "completed",
|
|
289
318
|
});
|
|
290
319
|
}
|
|
291
320
|
}
|
|
292
321
|
|
|
322
|
+
/** 恢复 metadata 中持久化的模型覆盖。 */
|
|
323
|
+
private async restore_persisted_model(): Promise<void> {
|
|
324
|
+
const model_id = String(this.state.sessionConfig.modelId || "").trim();
|
|
325
|
+
if (!model_id || this.state.sessionConfig.model) return;
|
|
326
|
+
const model = await this.resolve_model_by_id(model_id);
|
|
327
|
+
this.state.sessionConfig.model = normalizeAgentModel(model);
|
|
328
|
+
this.state.sessionConfig.modelLabel = inferAgentModelLabel(model);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 使用宿主 resolver 解析稳定模型 ID。 */
|
|
332
|
+
private async resolve_model_by_id(model_id: string): Promise<AgentModel> {
|
|
333
|
+
if (!this.resolve_model) {
|
|
334
|
+
throw new Error("Session model resolver is not configured.");
|
|
335
|
+
}
|
|
336
|
+
return await this.resolve_model(model_id);
|
|
337
|
+
}
|
|
338
|
+
|
|
293
339
|
/**
|
|
294
340
|
* 追加一条 user 消息并刷新标题与 metadata。
|
|
295
341
|
*/
|
|
@@ -297,7 +343,20 @@ export class SessionStateService {
|
|
|
297
343
|
message?: SessionRecordV1 | null;
|
|
298
344
|
text?: string;
|
|
299
345
|
}): Promise<void> {
|
|
300
|
-
|
|
346
|
+
const parts = params.message && "role" in params.message
|
|
347
|
+
? from_ui_user_parts(params.message.parts)
|
|
348
|
+
: [{
|
|
349
|
+
part_id: `external-user-text:${Date.now()}`,
|
|
350
|
+
type: "text" as const,
|
|
351
|
+
text: String(params.text || "").trim(),
|
|
352
|
+
state: "done" as const,
|
|
353
|
+
}];
|
|
354
|
+
if (parts.length === 0) return;
|
|
355
|
+
await this.recorder.append_user_message({
|
|
356
|
+
turn_id: `external:${this.session_id}:${Date.now()}`,
|
|
357
|
+
input_type: "prompt",
|
|
358
|
+
parts,
|
|
359
|
+
});
|
|
301
360
|
await this.ensure_title_from_history({ generate: true });
|
|
302
361
|
await this.touch_metadata();
|
|
303
362
|
}
|
|
@@ -309,7 +368,16 @@ export class SessionStateService {
|
|
|
309
368
|
message?: SessionRecordV1 | null;
|
|
310
369
|
fallbackText?: string;
|
|
311
370
|
}): Promise<void> {
|
|
312
|
-
|
|
371
|
+
const parts = params.message && "role" in params.message
|
|
372
|
+
? from_ui_assistant_parts(params.message.parts)
|
|
373
|
+
: [{
|
|
374
|
+
part_id: `external-assistant-text:${Date.now()}`,
|
|
375
|
+
type: "text" as const,
|
|
376
|
+
text: String(params.fallbackText || "").trim(),
|
|
377
|
+
state: "done" as const,
|
|
378
|
+
}];
|
|
379
|
+
if (parts.length === 0) return;
|
|
380
|
+
await this.recorder.append_completed_assistant_message({ parts });
|
|
313
381
|
await this.touch_metadata();
|
|
314
382
|
}
|
|
315
383
|
|
|
@@ -317,11 +385,29 @@ export class SessionStateService {
|
|
|
317
385
|
* 仅刷新当前 session metadata。
|
|
318
386
|
*/
|
|
319
387
|
async touch_metadata(): Promise<void> {
|
|
388
|
+
const message_page = await this.recorder.list_messages({
|
|
389
|
+
limit: 500,
|
|
390
|
+
include_internal: true,
|
|
391
|
+
});
|
|
392
|
+
const messages_path = getSdkAgentSessionMessagesPath(
|
|
393
|
+
this.project_root,
|
|
394
|
+
this.agent_id,
|
|
395
|
+
this.session_id,
|
|
396
|
+
);
|
|
397
|
+
const history_bytes = await fs.stat(messages_path)
|
|
398
|
+
.then((file_stat) => file_stat.size)
|
|
399
|
+
.catch(() => 0);
|
|
400
|
+
const preview_text = resolve_message_preview(
|
|
401
|
+
message_page.items[message_page.items.length - 1],
|
|
402
|
+
).slice(0, 180);
|
|
320
403
|
await touchSessionMetadata({
|
|
321
404
|
projectRoot: this.project_root,
|
|
322
405
|
agentId: this.agent_id,
|
|
323
406
|
sessionId: this.session_id,
|
|
324
407
|
sessionConfig: this.state.sessionConfig,
|
|
408
|
+
message_count: message_page.total,
|
|
409
|
+
history_bytes,
|
|
410
|
+
...(preview_text ? { preview_text } : {}),
|
|
325
411
|
});
|
|
326
412
|
}
|
|
327
413
|
|
|
@@ -368,14 +454,8 @@ export class SessionStateService {
|
|
|
368
454
|
async persist_assistant_result(
|
|
369
455
|
assistant_message?: SessionMessageRecordV1 | null,
|
|
370
456
|
): Promise<void> {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
agentId: this.agent_id,
|
|
374
|
-
sessionId: this.session_id,
|
|
375
|
-
sessionConfig: this.state.sessionConfig,
|
|
376
|
-
executor: this.executor,
|
|
377
|
-
assistantMessage: assistant_message,
|
|
378
|
-
});
|
|
457
|
+
void assistant_message;
|
|
458
|
+
await this.touch_metadata();
|
|
379
459
|
}
|
|
380
460
|
|
|
381
461
|
/**
|
|
@@ -384,8 +464,23 @@ export class SessionStateService {
|
|
|
384
464
|
async persist_action_event(
|
|
385
465
|
event: SessionActionRecordV1,
|
|
386
466
|
): Promise<void> {
|
|
387
|
-
const
|
|
388
|
-
|
|
467
|
+
const existing = this.recorder.get_message(event.id);
|
|
468
|
+
if (!existing) {
|
|
469
|
+
const writer = await this.recorder.open_action_message({
|
|
470
|
+
message_id: event.id,
|
|
471
|
+
turn_id: event.metadata.turnId,
|
|
472
|
+
action_type: infer_action_type(event.id),
|
|
473
|
+
title: event.title,
|
|
474
|
+
description: event.description,
|
|
475
|
+
});
|
|
476
|
+
if (event.state === "completed") await writer.complete();
|
|
477
|
+
if (event.state === "failed") await writer.fail(event.description || event.title);
|
|
478
|
+
} else if (existing.type === "action" && event.state !== "running") {
|
|
479
|
+
await this.recorder.update_action_message(event.id, event.state, {
|
|
480
|
+
title: event.title,
|
|
481
|
+
description: event.description,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
389
484
|
await this.touch_metadata();
|
|
390
485
|
}
|
|
391
486
|
|
|
@@ -393,21 +488,26 @@ export class SessionStateService {
|
|
|
393
488
|
* 写入并发布一条 action。
|
|
394
489
|
*/
|
|
395
490
|
async emit_action_event(input: EmitActionInput): Promise<void> {
|
|
396
|
-
const
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
491
|
+
const action_id = String(input.id || "").trim() ||
|
|
492
|
+
`action:${this.session_id}:${Date.now()}`;
|
|
493
|
+
const turn_id = "turnId" in input
|
|
494
|
+
? input.turnId
|
|
495
|
+
: input.metadata?.turnId;
|
|
496
|
+
await this.persist_action_event({
|
|
497
|
+
type: "action",
|
|
498
|
+
id: action_id,
|
|
499
|
+
title: input.title,
|
|
500
|
+
...(input.description ? { description: input.description } : {}),
|
|
501
|
+
state: input.state,
|
|
502
|
+
metadata: {
|
|
503
|
+
v: 1,
|
|
504
|
+
ts: input.metadata?.ts || Date.now(),
|
|
505
|
+
sessionId: this.session_id,
|
|
506
|
+
...(turn_id
|
|
507
|
+
? { turnId: turn_id }
|
|
508
|
+
: {}),
|
|
402
509
|
},
|
|
403
|
-
|
|
404
|
-
);
|
|
405
|
-
try {
|
|
406
|
-
await this.persist_action_event(event);
|
|
407
|
-
} catch {
|
|
408
|
-
// action 持久化失败不应阻断宿主操作。
|
|
409
|
-
}
|
|
410
|
-
this.publish_event(event);
|
|
510
|
+
});
|
|
411
511
|
}
|
|
412
512
|
|
|
413
513
|
/**
|
|
@@ -415,43 +515,22 @@ export class SessionStateService {
|
|
|
415
515
|
*/
|
|
416
516
|
async create_and_persist_user_prompt_message(
|
|
417
517
|
input: AgentSessionPromptInput,
|
|
518
|
+
turn_id: string,
|
|
519
|
+
input_type: "prompt" | "steer" = "prompt",
|
|
418
520
|
): Promise<SessionUserMessageV1> {
|
|
419
521
|
const query = input.query;
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
await this.ensure_title_from_history({ generate: true });
|
|
431
|
-
await this.touch_metadata();
|
|
432
|
-
return message;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
|
|
436
|
-
const parts = await hydrateUserPromptFileParts(
|
|
437
|
-
Array.isArray(query) ? query : [],
|
|
438
|
-
this.project_root,
|
|
439
|
-
);
|
|
440
|
-
const message: SessionUserMessageV1 = {
|
|
441
|
-
id: `u:${this.session_id}:${generateId()}`,
|
|
442
|
-
role: "user",
|
|
443
|
-
parts,
|
|
444
|
-
metadata: {
|
|
445
|
-
v: 1,
|
|
446
|
-
ts: Date.now(),
|
|
447
|
-
sessionId: this.session_id,
|
|
448
|
-
source: "ingress",
|
|
449
|
-
kind: "normal",
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
await this.executor.append_user_message({
|
|
453
|
-
message,
|
|
522
|
+
const ui_parts = typeof query === "string"
|
|
523
|
+
? [{ type: "text" as const, text: query.trim() }]
|
|
524
|
+
: await hydrateUserPromptFileParts(
|
|
525
|
+
Array.isArray(query) ? query : [],
|
|
526
|
+
this.project_root,
|
|
527
|
+
);
|
|
528
|
+
const canonical = await this.recorder.append_user_message({
|
|
529
|
+
turn_id,
|
|
530
|
+
input_type,
|
|
531
|
+
parts: normalize_session_user_parts(ui_parts),
|
|
454
532
|
});
|
|
533
|
+
const message = to_executor_ui_message(canonical) as SessionUserMessageV1;
|
|
455
534
|
await this.ensure_title_from_history({ generate: true });
|
|
456
535
|
await this.touch_metadata();
|
|
457
536
|
return message;
|
|
@@ -468,10 +547,36 @@ export class SessionStateService {
|
|
|
468
547
|
: [];
|
|
469
548
|
if (normalized_messages.length <= 0) return;
|
|
470
549
|
for (const message of normalized_messages) {
|
|
471
|
-
await this.
|
|
472
|
-
message,
|
|
550
|
+
await this.recorder.append_user_message({
|
|
551
|
+
turn_id: String(message.metadata?.turnId || `deferred:${this.session_id}:${Date.now()}`),
|
|
552
|
+
input_type: "steer",
|
|
553
|
+
parts: from_ui_user_parts(message.parts),
|
|
473
554
|
});
|
|
474
555
|
}
|
|
475
556
|
await this.touch_metadata();
|
|
476
557
|
}
|
|
477
558
|
}
|
|
559
|
+
|
|
560
|
+
function resolve_message_preview(message: SessionMessage | undefined): string {
|
|
561
|
+
if (!message) return "";
|
|
562
|
+
if (message.type === "user") {
|
|
563
|
+
return message.parts
|
|
564
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
565
|
+
.join("")
|
|
566
|
+
.trim();
|
|
567
|
+
}
|
|
568
|
+
if (message.type === "assistant") {
|
|
569
|
+
return message.parts
|
|
570
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
571
|
+
.join("")
|
|
572
|
+
.trim();
|
|
573
|
+
}
|
|
574
|
+
if (message.type === "action") {
|
|
575
|
+
return [message.title, message.description].filter(Boolean).join("\n");
|
|
576
|
+
}
|
|
577
|
+
return message.message.trim();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function infer_action_type(message_id: string): string {
|
|
581
|
+
return String(message_id || "").split(":")[0] || "action";
|
|
582
|
+
}
|
|
@@ -23,11 +23,15 @@ import type {
|
|
|
23
23
|
SessionMessageRecordV1,
|
|
24
24
|
SessionUserMessageV1,
|
|
25
25
|
} from "@/executor/types/SessionRecords.js";
|
|
26
|
-
import { mapAgentEventToSessionEvent, mapUiMessageChunkToAgentEvent } from "@/session/SessionEventMapper.js";
|
|
27
26
|
import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
|
|
28
27
|
import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
|
|
29
28
|
import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
|
|
30
29
|
import { SessionStateService } from "@/session/services/SessionStateService.js";
|
|
30
|
+
import {
|
|
31
|
+
SessionAssistantMessageWriter,
|
|
32
|
+
SessionRecorder,
|
|
33
|
+
} from "@/session/recorder/SessionRecorder.js";
|
|
34
|
+
import { from_ui_assistant_parts } from "@/session/recorder/SessionMessageCodec.js";
|
|
31
35
|
|
|
32
36
|
type SessionTurnServiceOptions = {
|
|
33
37
|
/**
|
|
@@ -54,6 +58,9 @@ type SessionTurnServiceOptions = {
|
|
|
54
58
|
* 当前 session 共享事件总线。
|
|
55
59
|
*/
|
|
56
60
|
event_hub: SessionEventHub;
|
|
61
|
+
|
|
62
|
+
/** 当前 Session Message Recorder。 */
|
|
63
|
+
recorder: SessionRecorder;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -65,6 +72,7 @@ export class SessionTurnService {
|
|
|
65
72
|
private readonly executor: Executor;
|
|
66
73
|
private readonly state_service: SessionStateService;
|
|
67
74
|
private readonly event_hub: SessionEventHub;
|
|
75
|
+
private readonly recorder: SessionRecorder;
|
|
68
76
|
private readonly prompt_runtime: SessionPromptRuntime;
|
|
69
77
|
|
|
70
78
|
constructor(options: SessionTurnServiceOptions) {
|
|
@@ -73,23 +81,26 @@ export class SessionTurnService {
|
|
|
73
81
|
this.executor = options.executor;
|
|
74
82
|
this.state_service = options.state_service;
|
|
75
83
|
this.event_hub = options.event_hub;
|
|
84
|
+
this.recorder = options.recorder;
|
|
76
85
|
this.prompt_runtime = new SessionPromptRuntime({
|
|
77
86
|
sessionId: this.session_id,
|
|
78
87
|
publish: (event) => {
|
|
79
88
|
this.publish_event(event);
|
|
80
89
|
},
|
|
81
|
-
createAndPersistUserMessage: async (input) => {
|
|
90
|
+
createAndPersistUserMessage: async (input, turn_id, input_type) => {
|
|
82
91
|
return await this.state_service.create_and_persist_user_prompt_message(
|
|
83
92
|
input,
|
|
93
|
+
turn_id,
|
|
94
|
+
input_type,
|
|
84
95
|
);
|
|
85
96
|
},
|
|
86
|
-
|
|
87
|
-
await this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
appendErrorMessage: async ({ turn_id, message }) => {
|
|
98
|
+
await this.recorder.append_error_message({
|
|
99
|
+
scope: "turn",
|
|
100
|
+
turn_id,
|
|
101
|
+
code: "turn_execution_failed",
|
|
102
|
+
message,
|
|
103
|
+
recoverable: true,
|
|
93
104
|
});
|
|
94
105
|
},
|
|
95
106
|
executeTurn: async ({ turnId, promptInput, onStepMerge, abortSignal }) => {
|
|
@@ -157,64 +168,38 @@ export class SessionTurnService {
|
|
|
157
168
|
assistantMessage?: SessionMessageRecordV1 | null;
|
|
158
169
|
error?: string;
|
|
159
170
|
}> {
|
|
160
|
-
const
|
|
171
|
+
const assistant_writer_ref: {
|
|
172
|
+
current: SessionAssistantMessageWriter | null;
|
|
173
|
+
} = { current: null };
|
|
174
|
+
let assistant_segment_index = 0;
|
|
175
|
+
const ensure_assistant_writer = async (): Promise<SessionAssistantMessageWriter> => {
|
|
176
|
+
if (assistant_writer_ref.current) return assistant_writer_ref.current;
|
|
177
|
+
assistant_segment_index += 1;
|
|
178
|
+
assistant_writer_ref.current = await this.recorder.open_assistant_message({
|
|
179
|
+
turn_id: input.turnId,
|
|
180
|
+
segment_index: assistant_segment_index,
|
|
181
|
+
});
|
|
182
|
+
return assistant_writer_ref.current;
|
|
183
|
+
};
|
|
161
184
|
const run_context: SessionRunContext = {
|
|
162
185
|
turnId: input.turnId,
|
|
163
186
|
sessionId: this.session_id,
|
|
164
187
|
projectRoot: this.project_root,
|
|
165
|
-
onStepCallback:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
...(step.visibility ? { visibility: step.visibility } : {}),
|
|
173
|
-
});
|
|
188
|
+
onStepCallback: async () => {
|
|
189
|
+
const merged = await input.onStepMerge();
|
|
190
|
+
if (merged.length > 0 && assistant_writer_ref.current) {
|
|
191
|
+
await assistant_writer_ref.current.complete();
|
|
192
|
+
assistant_writer_ref.current = null;
|
|
193
|
+
}
|
|
194
|
+
return merged;
|
|
174
195
|
},
|
|
175
196
|
onUiMessageChunkCallback: async (chunk) => {
|
|
176
|
-
if (chunk.type
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
const event = mapUiMessageChunkToAgentEvent(chunk);
|
|
181
|
-
if (!event) return;
|
|
182
|
-
const resolved_event =
|
|
183
|
-
(
|
|
184
|
-
event.type === "tool-result" ||
|
|
185
|
-
event.type === "tool-error"
|
|
186
|
-
) &&
|
|
187
|
-
event.toolName === "unknown"
|
|
188
|
-
? {
|
|
189
|
-
...event,
|
|
190
|
-
toolName:
|
|
191
|
-
tool_name_by_call_id.get(event.toolCallId) || event.toolName,
|
|
192
|
-
}
|
|
193
|
-
: event;
|
|
194
|
-
if (
|
|
195
|
-
resolved_event.type === "tool-call" ||
|
|
196
|
-
resolved_event.type === "tool-error"
|
|
197
|
-
) {
|
|
198
|
-
tool_name_by_call_id.set(
|
|
199
|
-
resolved_event.toolCallId,
|
|
200
|
-
resolved_event.toolName,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
const session_event = mapAgentEventToSessionEvent({
|
|
204
|
-
event: resolved_event,
|
|
205
|
-
turnId: input.turnId,
|
|
206
|
-
});
|
|
207
|
-
if (session_event) {
|
|
208
|
-
this.publish_event(session_event);
|
|
209
|
-
}
|
|
197
|
+
if (!is_assistant_content_chunk(chunk.type)) return;
|
|
198
|
+
const writer = await ensure_assistant_writer();
|
|
199
|
+
await writer.apply_chunk(chunk);
|
|
210
200
|
},
|
|
211
201
|
onActionCallback: async (event) => {
|
|
212
|
-
|
|
213
|
-
await this.state_service.persist_action_event(event);
|
|
214
|
-
} catch {
|
|
215
|
-
// action 持久化失败不应阻断当前 turn。
|
|
216
|
-
}
|
|
217
|
-
this.publish_event(event);
|
|
202
|
+
await this.state_service.persist_action_event(event);
|
|
218
203
|
},
|
|
219
204
|
injectedUserMessages: [],
|
|
220
205
|
deferredPersistedUserMessages: [],
|
|
@@ -227,6 +212,37 @@ export class SessionTurnService {
|
|
|
227
212
|
query: executor_query,
|
|
228
213
|
runContext: run_context,
|
|
229
214
|
});
|
|
215
|
+
const final_assistant_writer = assistant_writer_ref.current;
|
|
216
|
+
if (final_assistant_writer) {
|
|
217
|
+
if (input.abortSignal?.aborted) {
|
|
218
|
+
await final_assistant_writer.stop();
|
|
219
|
+
} else if (result.success) {
|
|
220
|
+
await final_assistant_writer.complete();
|
|
221
|
+
} else {
|
|
222
|
+
await final_assistant_writer.fail(result.error);
|
|
223
|
+
}
|
|
224
|
+
} else if (result.assistantMessage) {
|
|
225
|
+
const parts = from_ui_assistant_parts(result.assistantMessage.parts);
|
|
226
|
+
if (parts.length > 0) {
|
|
227
|
+
assistant_segment_index += 1;
|
|
228
|
+
const fallback_writer = await this.recorder.open_assistant_message({
|
|
229
|
+
turn_id: input.turnId,
|
|
230
|
+
segment_index: assistant_segment_index,
|
|
231
|
+
});
|
|
232
|
+
for (const part of parts) await fallback_writer.upsert_part(part);
|
|
233
|
+
if (result.success) await fallback_writer.complete();
|
|
234
|
+
else await fallback_writer.fail(result.error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!result.success && !input.abortSignal?.aborted && result.error) {
|
|
238
|
+
await this.recorder.append_error_message({
|
|
239
|
+
scope: "turn",
|
|
240
|
+
turn_id: input.turnId,
|
|
241
|
+
code: "turn_execution_failed",
|
|
242
|
+
message: result.error,
|
|
243
|
+
recoverable: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
230
246
|
await this.state_service.persist_assistant_result(result.assistantMessage);
|
|
231
247
|
await this.state_service.persist_deferred_user_messages(
|
|
232
248
|
result.deferredPersistedUserMessages,
|
|
@@ -243,3 +259,16 @@ export class SessionTurnService {
|
|
|
243
259
|
};
|
|
244
260
|
}
|
|
245
261
|
}
|
|
262
|
+
|
|
263
|
+
function is_assistant_content_chunk(type: string): boolean {
|
|
264
|
+
return (
|
|
265
|
+
type === "text-start" ||
|
|
266
|
+
type === "text-delta" ||
|
|
267
|
+
type === "text-end" ||
|
|
268
|
+
type === "reasoning-start" ||
|
|
269
|
+
type === "reasoning-delta" ||
|
|
270
|
+
type === "reasoning-end" ||
|
|
271
|
+
type.startsWith("tool-") ||
|
|
272
|
+
type === "file"
|
|
273
|
+
);
|
|
274
|
+
}
|