@downcity/agent 1.1.217 → 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/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +1 -4
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +9 -3
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +19 -4
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +7 -3
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +5 -2
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +25 -11
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +5 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +5 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.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 +11 -4
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +14 -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 +47 -18
- package/bin/session/Session.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 +7 -3
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +132 -72
- 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 +10 -5
- package/bin/session/services/SessionViewService.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 +7 -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/SessionActor.d.ts +10 -4
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +20 -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/package.json +3 -3
- package/scripts/agent-env.test.mjs +4 -4
- package/scripts/session-action-message.test.mjs +98 -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/services/AgentSessions.ts +1 -3
- package/src/agent/remote/RemoteSession.ts +35 -7
- package/src/agent/remote/RemoteTransport.ts +17 -6
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +45 -13
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +19 -6
- package/src/executor/types/SessionRecords.ts +2 -0
- package/src/index.ts +23 -5
- package/src/rpc/Client.ts +29 -8
- package/src/session/Session.ts +65 -31
- 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 +151 -80
- package/src/session/services/SessionTurnService.ts +88 -59
- package/src/session/services/SessionViewService.ts +25 -13
- package/src/session/storage/Persistence.ts +13 -0
- package/src/session/storage/RuntimeSessionPort.ts +6 -4
- package/src/types/agent/SessionActor.ts +26 -6
- package/src/types/rpc/RpcProtocol.ts +20 -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/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,11 +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";
|
|
45
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";
|
|
46
54
|
|
|
47
55
|
type SessionStateServiceOptions = {
|
|
48
56
|
/**
|
|
@@ -63,7 +71,10 @@ type SessionStateServiceOptions = {
|
|
|
63
71
|
/**
|
|
64
72
|
* 当前 session 历史事实源。
|
|
65
73
|
*/
|
|
66
|
-
history_store:
|
|
74
|
+
history_store: SessionHistoryStore;
|
|
75
|
+
|
|
76
|
+
/** 当前 Session Message Recorder。 */
|
|
77
|
+
recorder: SessionRecorder;
|
|
67
78
|
|
|
68
79
|
/**
|
|
69
80
|
* 当前 session 执行器。
|
|
@@ -114,7 +125,8 @@ export class SessionStateService {
|
|
|
114
125
|
private readonly agent_id: string;
|
|
115
126
|
private readonly project_root: string;
|
|
116
127
|
private readonly session_id: string;
|
|
117
|
-
private readonly history_store:
|
|
128
|
+
private readonly history_store: SessionHistoryStore;
|
|
129
|
+
private readonly recorder: SessionRecorder;
|
|
118
130
|
private readonly executor: Executor;
|
|
119
131
|
private readonly state: SessionLocalState;
|
|
120
132
|
private readonly logger: Logger;
|
|
@@ -127,6 +139,7 @@ export class SessionStateService {
|
|
|
127
139
|
this.project_root = options.project_root;
|
|
128
140
|
this.session_id = options.session_id;
|
|
129
141
|
this.history_store = options.history_store;
|
|
142
|
+
this.recorder = options.recorder;
|
|
130
143
|
this.executor = options.executor;
|
|
131
144
|
this.state = options.state;
|
|
132
145
|
this.logger = options.logger;
|
|
@@ -197,6 +210,7 @@ export class SessionStateService {
|
|
|
197
210
|
: {}),
|
|
198
211
|
...(metadata.modelId ? { modelId: metadata.modelId } : {}),
|
|
199
212
|
};
|
|
213
|
+
await this.restore_persisted_model();
|
|
200
214
|
})();
|
|
201
215
|
await this.state.initializePromise;
|
|
202
216
|
}
|
|
@@ -206,7 +220,6 @@ export class SessionStateService {
|
|
|
206
220
|
*/
|
|
207
221
|
async ensure_ready_for_execution(): Promise<void> {
|
|
208
222
|
await this.initialize();
|
|
209
|
-
await this.restore_persisted_model();
|
|
210
223
|
if (this.state.ensureConfiguredPromise) {
|
|
211
224
|
await this.state.ensureConfiguredPromise;
|
|
212
225
|
return;
|
|
@@ -239,6 +252,9 @@ export class SessionStateService {
|
|
|
239
252
|
async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
|
|
240
253
|
const should_emit_action = options?.emit_action !== false;
|
|
241
254
|
const previous_model_label = this.state.sessionConfig.modelLabel;
|
|
255
|
+
const previous_model_id = String(
|
|
256
|
+
this.state.sessionConfig.modelId || previous_model_label || "",
|
|
257
|
+
).trim();
|
|
242
258
|
const requested_model_id = String(input.modelId || "").trim();
|
|
243
259
|
const next_model = input.model || (requested_model_id
|
|
244
260
|
? await this.resolve_model_by_id(requested_model_id)
|
|
@@ -251,19 +267,18 @@ export class SessionStateService {
|
|
|
251
267
|
next_model &&
|
|
252
268
|
should_emit_action &&
|
|
253
269
|
this.state.sessionConfig.model &&
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
previous_model_id &&
|
|
271
|
+
next_model_id &&
|
|
272
|
+
previous_model_id !== next_model_id,
|
|
257
273
|
);
|
|
274
|
+
const previous_model_name = previous_model_label || previous_model_id;
|
|
275
|
+
const next_model_name = next_model_label || next_model_id;
|
|
258
276
|
const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
|
|
259
277
|
|
|
260
278
|
if (should_emit_model_switch_action) {
|
|
261
279
|
await this.emit_action_event({
|
|
262
280
|
id: action_id,
|
|
263
|
-
title:
|
|
264
|
-
description: next_model_label
|
|
265
|
-
? `Switching to ${next_model_label}.`
|
|
266
|
-
: undefined,
|
|
281
|
+
title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
|
|
267
282
|
state: "running",
|
|
268
283
|
});
|
|
269
284
|
}
|
|
@@ -287,7 +302,7 @@ export class SessionStateService {
|
|
|
287
302
|
if (should_emit_model_switch_action) {
|
|
288
303
|
await this.emit_action_event({
|
|
289
304
|
id: action_id,
|
|
290
|
-
title:
|
|
305
|
+
title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
|
|
291
306
|
description: error instanceof Error ? error.message : String(error),
|
|
292
307
|
state: "failed",
|
|
293
308
|
});
|
|
@@ -298,10 +313,7 @@ export class SessionStateService {
|
|
|
298
313
|
if (should_emit_model_switch_action) {
|
|
299
314
|
await this.emit_action_event({
|
|
300
315
|
id: action_id,
|
|
301
|
-
title:
|
|
302
|
-
description: this.state.sessionConfig.modelLabel
|
|
303
|
-
? `Using ${this.state.sessionConfig.modelLabel}.`
|
|
304
|
-
: undefined,
|
|
316
|
+
title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
|
|
305
317
|
state: "completed",
|
|
306
318
|
});
|
|
307
319
|
}
|
|
@@ -331,7 +343,20 @@ export class SessionStateService {
|
|
|
331
343
|
message?: SessionRecordV1 | null;
|
|
332
344
|
text?: string;
|
|
333
345
|
}): Promise<void> {
|
|
334
|
-
|
|
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
|
+
});
|
|
335
360
|
await this.ensure_title_from_history({ generate: true });
|
|
336
361
|
await this.touch_metadata();
|
|
337
362
|
}
|
|
@@ -343,7 +368,16 @@ export class SessionStateService {
|
|
|
343
368
|
message?: SessionRecordV1 | null;
|
|
344
369
|
fallbackText?: string;
|
|
345
370
|
}): Promise<void> {
|
|
346
|
-
|
|
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 });
|
|
347
381
|
await this.touch_metadata();
|
|
348
382
|
}
|
|
349
383
|
|
|
@@ -351,11 +385,29 @@ export class SessionStateService {
|
|
|
351
385
|
* 仅刷新当前 session metadata。
|
|
352
386
|
*/
|
|
353
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);
|
|
354
403
|
await touchSessionMetadata({
|
|
355
404
|
projectRoot: this.project_root,
|
|
356
405
|
agentId: this.agent_id,
|
|
357
406
|
sessionId: this.session_id,
|
|
358
407
|
sessionConfig: this.state.sessionConfig,
|
|
408
|
+
message_count: message_page.total,
|
|
409
|
+
history_bytes,
|
|
410
|
+
...(preview_text ? { preview_text } : {}),
|
|
359
411
|
});
|
|
360
412
|
}
|
|
361
413
|
|
|
@@ -402,14 +454,8 @@ export class SessionStateService {
|
|
|
402
454
|
async persist_assistant_result(
|
|
403
455
|
assistant_message?: SessionMessageRecordV1 | null,
|
|
404
456
|
): Promise<void> {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
agentId: this.agent_id,
|
|
408
|
-
sessionId: this.session_id,
|
|
409
|
-
sessionConfig: this.state.sessionConfig,
|
|
410
|
-
executor: this.executor,
|
|
411
|
-
assistantMessage: assistant_message,
|
|
412
|
-
});
|
|
457
|
+
void assistant_message;
|
|
458
|
+
await this.touch_metadata();
|
|
413
459
|
}
|
|
414
460
|
|
|
415
461
|
/**
|
|
@@ -418,8 +464,23 @@ export class SessionStateService {
|
|
|
418
464
|
async persist_action_event(
|
|
419
465
|
event: SessionActionRecordV1,
|
|
420
466
|
): Promise<void> {
|
|
421
|
-
const
|
|
422
|
-
|
|
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
|
+
}
|
|
423
484
|
await this.touch_metadata();
|
|
424
485
|
}
|
|
425
486
|
|
|
@@ -427,21 +488,26 @@ export class SessionStateService {
|
|
|
427
488
|
* 写入并发布一条 action。
|
|
428
489
|
*/
|
|
429
490
|
async emit_action_event(input: EmitActionInput): Promise<void> {
|
|
430
|
-
const
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
: {}),
|
|
436
509
|
},
|
|
437
|
-
|
|
438
|
-
);
|
|
439
|
-
try {
|
|
440
|
-
await this.persist_action_event(event);
|
|
441
|
-
} catch {
|
|
442
|
-
// action 持久化失败不应阻断宿主操作。
|
|
443
|
-
}
|
|
444
|
-
this.publish_event(event);
|
|
510
|
+
});
|
|
445
511
|
}
|
|
446
512
|
|
|
447
513
|
/**
|
|
@@ -449,43 +515,22 @@ export class SessionStateService {
|
|
|
449
515
|
*/
|
|
450
516
|
async create_and_persist_user_prompt_message(
|
|
451
517
|
input: AgentSessionPromptInput,
|
|
518
|
+
turn_id: string,
|
|
519
|
+
input_type: "prompt" | "steer" = "prompt",
|
|
452
520
|
): Promise<SessionUserMessageV1> {
|
|
453
521
|
const query = input.query;
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
await this.ensure_title_from_history({ generate: true });
|
|
465
|
-
await this.touch_metadata();
|
|
466
|
-
return message;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
|
|
470
|
-
const parts = await hydrateUserPromptFileParts(
|
|
471
|
-
Array.isArray(query) ? query : [],
|
|
472
|
-
this.project_root,
|
|
473
|
-
);
|
|
474
|
-
const message: SessionUserMessageV1 = {
|
|
475
|
-
id: `u:${this.session_id}:${generateId()}`,
|
|
476
|
-
role: "user",
|
|
477
|
-
parts,
|
|
478
|
-
metadata: {
|
|
479
|
-
v: 1,
|
|
480
|
-
ts: Date.now(),
|
|
481
|
-
sessionId: this.session_id,
|
|
482
|
-
source: "ingress",
|
|
483
|
-
kind: "normal",
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
await this.executor.append_user_message({
|
|
487
|
-
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),
|
|
488
532
|
});
|
|
533
|
+
const message = to_executor_ui_message(canonical) as SessionUserMessageV1;
|
|
489
534
|
await this.ensure_title_from_history({ generate: true });
|
|
490
535
|
await this.touch_metadata();
|
|
491
536
|
return message;
|
|
@@ -502,10 +547,36 @@ export class SessionStateService {
|
|
|
502
547
|
: [];
|
|
503
548
|
if (normalized_messages.length <= 0) return;
|
|
504
549
|
for (const message of normalized_messages) {
|
|
505
|
-
await this.
|
|
506
|
-
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),
|
|
507
554
|
});
|
|
508
555
|
}
|
|
509
556
|
await this.touch_metadata();
|
|
510
557
|
}
|
|
511
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
|
+
}
|