@downcity/agent 1.1.222 → 1.1.224
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 +1 -34
- package/bin/agent/local/Agent.d.ts.map +1 -1
- package/bin/agent/local/Agent.js +1 -44
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
- package/bin/agent/local/services/AgentAssemblyService.js +35 -2
- package/bin/agent/local/services/AgentAssemblyService.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 +42 -0
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteAgent.d.ts +1 -35
- package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
- package/bin/agent/remote/RemoteAgent.js +1 -37
- package/bin/agent/remote/RemoteAgent.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +24 -45
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +74 -121
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +11 -27
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +7 -21
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -47
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +7 -20
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -17
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -2
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionHistoryPaths.d.ts +1 -1
- package/bin/index.d.ts +3 -3
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- package/bin/index.js.map +1 -1
- package/bin/rpc/Client.d.ts +7 -36
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +28 -78
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +16 -8
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +66 -21
- package/bin/session/Session.js.map +1 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +49 -18
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +30 -24
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.js +163 -59
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
- package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -1
- package/bin/session/recorder/SessionMessageCodec.js +6 -1
- package/bin/session/recorder/SessionMessageCodec.js.map +1 -1
- package/bin/session/recorder/SessionRecorder.d.ts +20 -11
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +148 -103
- package/bin/session/recorder/SessionRecorder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +5 -5
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.js +5 -5
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
- package/bin/session/runtime/SessionEventHub.d.ts +12 -5
- package/bin/session/runtime/SessionEventHub.d.ts.map +1 -1
- package/bin/session/runtime/SessionEventHub.js +11 -6
- package/bin/session/runtime/SessionEventHub.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +2 -2
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +35 -17
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +2 -2
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +6 -2
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +2 -7
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +2 -8
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/storage/Paths.d.ts +1 -1
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +2 -2
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.d.ts +3 -4
- package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.js +2 -2
- package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +11 -7
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +35 -62
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +4 -4
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/session/SessionApproval.d.ts +97 -0
- package/bin/types/session/SessionApproval.d.ts.map +1 -0
- package/bin/types/session/SessionApproval.js +7 -0
- package/bin/types/session/SessionApproval.js.map +1 -0
- package/bin/types/session/SessionMessage.d.ts +10 -4
- package/bin/types/session/SessionMessage.d.ts.map +1 -1
- package/bin/types/session/SessionMutation.d.ts +116 -0
- package/bin/types/session/SessionMutation.d.ts.map +1 -0
- package/bin/types/session/SessionMutation.js +19 -0
- package/bin/types/session/SessionMutation.js.map +1 -0
- package/bin/types/session/SessionOptions.d.ts +9 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/remote-session-reconnect.test.mjs +7 -3
- package/scripts/session-history-inflight.test.mjs +2 -2
- package/scripts/session-prompt-runtime.test.mjs +3 -3
- package/scripts/session-recorder.test.mjs +100 -146
- package/src/agent/local/Agent.ts +1 -55
- package/src/agent/local/services/AgentAssemblyService.ts +39 -3
- package/src/agent/local/services/AgentSessions.ts +47 -0
- package/src/agent/remote/RemoteAgent.ts +1 -54
- package/src/agent/remote/RemoteSession.ts +105 -163
- package/src/agent/remote/RemoteTransport.ts +23 -34
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +33 -90
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +22 -39
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
- package/src/executor/types/SessionHistoryPaths.ts +1 -1
- package/src/index.ts +20 -12
- package/src/rpc/Client.ts +48 -106
- package/src/session/Session.ts +91 -34
- package/src/session/browse/Browse.ts +51 -22
- package/src/session/recorder/JsonlSessionMessageStore.ts +170 -86
- package/src/session/recorder/SessionMessageCodec.ts +6 -1
- package/src/session/recorder/SessionRecorder.ts +183 -124
- package/src/session/recorder/SessionRecorderCompaction.ts +1 -0
- package/src/session/recorder/SessionRecorderHistoryStore.ts +5 -5
- package/src/session/runtime/SessionEventHub.ts +25 -12
- package/src/session/runtime/SessionPromptRuntime.ts +37 -19
- package/src/session/services/SessionStateService.ts +8 -6
- package/src/session/services/SessionTurnService.ts +3 -14
- package/src/session/storage/Paths.ts +2 -2
- package/src/session/storage/RuntimeSessionPort.ts +8 -11
- package/src/types/agent/SessionActor.ts +23 -20
- package/src/types/rpc/RpcProtocol.ts +34 -71
- package/src/types/runtime/agent/AgentContext.ts +7 -11
- package/src/types/session/SessionApproval.ts +108 -0
- package/src/types/session/SessionMessage.ts +10 -4
- package/src/types/session/SessionMutation.ts +141 -0
- package/src/types/session/SessionOptions.ts +25 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/session/recorder/SessionMessageReducer.d.ts +0 -12
- package/bin/session/recorder/SessionMessageReducer.d.ts.map +0 -1
- package/bin/session/recorder/SessionMessageReducer.js +0 -109
- package/bin/session/recorder/SessionMessageReducer.js.map +0 -1
- package/bin/session/runtime/SessionMessageEventHub.d.ts +0 -15
- package/bin/session/runtime/SessionMessageEventHub.d.ts.map +0 -1
- package/bin/session/runtime/SessionMessageEventHub.js +0 -28
- package/bin/session/runtime/SessionMessageEventHub.js.map +0 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts +0 -303
- package/bin/types/sdk/AgentSessionEvent.d.ts.map +0 -1
- package/bin/types/sdk/AgentSessionEvent.js +0 -9
- package/bin/types/sdk/AgentSessionEvent.js.map +0 -1
- package/bin/types/session/SessionMessageMutation.d.ts +0 -92
- package/bin/types/session/SessionMessageMutation.d.ts.map +0 -1
- package/bin/types/session/SessionMessageMutation.js +0 -21
- package/bin/types/session/SessionMessageMutation.js.map +0 -1
- package/src/session/recorder/SessionMessageReducer.ts +0 -155
- package/src/session/runtime/SessionMessageEventHub.ts +0 -35
- package/src/types/sdk/AgentSessionEvent.ts +0 -372
- package/src/types/session/SessionMessageMutation.ts +0 -121
package/src/session/Session.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
} from "@/types/agent/SessionTypes.js";
|
|
24
24
|
import type { AgentSession } from "@/types/agent/SessionActor.js";
|
|
25
25
|
import {
|
|
26
|
+
getSdkAgentSessionAssistantMessagePath,
|
|
26
27
|
getSdkAgentSessionDirPath,
|
|
27
28
|
} from "@/session/storage/Paths.js";
|
|
28
29
|
import { resolveSystemTimezone } from "@/session/storage/Metadata.js";
|
|
@@ -30,15 +31,18 @@ import { createRuntimeSessionPort } from "@/session/storage/RuntimeSessionPort.j
|
|
|
30
31
|
import { SessionSystemBuilder } from "@/session/SessionSystemBuilder.js";
|
|
31
32
|
import type { SessionPort } from "@/types/runtime/agent/AgentContext.js";
|
|
32
33
|
import type {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
SessionMutation,
|
|
35
|
+
SessionMutationSubscriber,
|
|
36
|
+
SessionMutationUnsubscribe,
|
|
37
|
+
} from "@/types/session/SessionMutation.js";
|
|
36
38
|
import type {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
ResolveSessionApprovalInput,
|
|
40
|
+
SessionApproval,
|
|
41
|
+
SessionApprovalModeSnapshot,
|
|
42
|
+
SessionApprovalResult,
|
|
43
|
+
SetSessionApprovalModeInput,
|
|
44
|
+
SessionApprovalRuntimeEvent,
|
|
45
|
+
} from "@/types/session/SessionApproval.js";
|
|
42
46
|
import type {
|
|
43
47
|
ListSessionMessagesInput,
|
|
44
48
|
SessionMessagePage,
|
|
@@ -47,7 +51,6 @@ import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js"
|
|
|
47
51
|
import type { AgentSessionStopResult } from "@/types/sdk/AgentSessionStop.js";
|
|
48
52
|
import type { AgentSessionTurnHandle } from "@/types/sdk/AgentSessionTurn.js";
|
|
49
53
|
import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
|
|
50
|
-
import { SessionMessageEventHub } from "@/session/runtime/SessionMessageEventHub.js";
|
|
51
54
|
import { SessionStateService } from "@/session/services/SessionStateService.js";
|
|
52
55
|
import { SessionTurnService } from "@/session/services/SessionTurnService.js";
|
|
53
56
|
import { SessionViewService } from "@/session/services/SessionViewService.js";
|
|
@@ -83,8 +86,11 @@ export class Session implements AgentSession {
|
|
|
83
86
|
private readonly recorder: SessionRecorder;
|
|
84
87
|
private readonly historyComposer: SessionHistoryComposer;
|
|
85
88
|
private readonly executor: Executor;
|
|
86
|
-
private readonly eventHub
|
|
87
|
-
private readonly
|
|
89
|
+
private readonly eventHub: SessionEventHub;
|
|
90
|
+
private readonly list_approvals: SessionOptions["list_approvals"];
|
|
91
|
+
private readonly get_approval_mode: SessionOptions["get_approval_mode"];
|
|
92
|
+
private readonly set_approval_mode_hook: SessionOptions["set_approval_mode"];
|
|
93
|
+
private readonly resolve_approval_hook: SessionOptions["resolve_approval"];
|
|
88
94
|
private readonly localState: SessionLocalState;
|
|
89
95
|
private readonly stateService: SessionStateService;
|
|
90
96
|
private readonly turnService: SessionTurnService;
|
|
@@ -102,6 +108,10 @@ export class Session implements AgentSession {
|
|
|
102
108
|
this.getPluginSystemBlocks = options.getPluginSystemBlocks;
|
|
103
109
|
this.ensureConfiguredHook = options.ensureConfigured;
|
|
104
110
|
this.composers = options.composers;
|
|
111
|
+
this.list_approvals = options.list_approvals;
|
|
112
|
+
this.get_approval_mode = options.get_approval_mode;
|
|
113
|
+
this.set_approval_mode_hook = options.set_approval_mode;
|
|
114
|
+
this.resolve_approval_hook = options.resolve_approval;
|
|
105
115
|
if (!this.id) {
|
|
106
116
|
throw new Error("Session requires a non-empty sessionId");
|
|
107
117
|
}
|
|
@@ -112,12 +122,20 @@ export class Session implements AgentSession {
|
|
|
112
122
|
throw new Error("Session requires a non-empty projectRoot");
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
this.eventHub = new SessionEventHub({
|
|
126
|
+
create_reply: (mutation) => ({
|
|
127
|
+
approval: async ({ decision }) => {
|
|
128
|
+
const approval_id = require_approval_id(mutation);
|
|
129
|
+
return await this.resolve_approval({ approval_id, decision });
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
115
133
|
this.messageStore = this.create_message_store();
|
|
116
134
|
this.recorder = new SessionRecorder({
|
|
117
135
|
session_id: this.id,
|
|
118
136
|
store: this.messageStore,
|
|
119
137
|
publish: (mutation) => {
|
|
120
|
-
this.
|
|
138
|
+
this.eventHub.publish(mutation);
|
|
121
139
|
},
|
|
122
140
|
});
|
|
123
141
|
this.historyStore = this.create_history_store();
|
|
@@ -229,19 +247,44 @@ export class Session implements AgentSession {
|
|
|
229
247
|
* 订阅当前 Session 的未来事件。
|
|
230
248
|
*/
|
|
231
249
|
subscribe(
|
|
232
|
-
subscriber:
|
|
233
|
-
):
|
|
234
|
-
return this.
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
subscriber: SessionMutationSubscriber,
|
|
251
|
+
): SessionMutationUnsubscribe {
|
|
252
|
+
return this.eventHub.subscribe(subscriber);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 列出当前 Session 的 pending 工具审批。 */
|
|
256
|
+
async approvals(): Promise<SessionApproval[]> {
|
|
257
|
+
return await this.list_approvals(this.id);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 读取当前 Session 的工具审批模式。 */
|
|
261
|
+
async approval_mode(): Promise<SessionApprovalModeSnapshot> {
|
|
262
|
+
return await this.get_approval_mode(this.id);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 更新当前 Session 的工具审批模式。 */
|
|
266
|
+
async set_approval_mode(input: SetSessionApprovalModeInput): Promise<SessionApprovalModeSnapshot> {
|
|
267
|
+
return await this.set_approval_mode_hook(this.id, input);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 处理当前 Session 的 pending 工具审批。 */
|
|
271
|
+
async resolve_approval(input: ResolveSessionApprovalInput): Promise<SessionApprovalResult> {
|
|
272
|
+
return await this.resolve_approval_hook(this.id, input);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async apply_runtime_event(event: SessionApprovalRuntimeEvent): Promise<void> {
|
|
276
|
+
if (event.type === "tool-approval-request") {
|
|
277
|
+
await this.recorder.require_tool_approval({
|
|
278
|
+
tool_call_id: event.tool_call_id,
|
|
279
|
+
approval_id: event.approval_id,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await this.recorder.resolve_tool_approval({
|
|
284
|
+
tool_call_id: event.tool_call_id,
|
|
285
|
+
approval_id: event.approval_id,
|
|
286
|
+
decision: event.decision,
|
|
287
|
+
});
|
|
245
288
|
}
|
|
246
289
|
|
|
247
290
|
/**
|
|
@@ -280,13 +323,6 @@ export class Session implements AgentSession {
|
|
|
280
323
|
return await this.recorder.list_messages(input);
|
|
281
324
|
}
|
|
282
325
|
|
|
283
|
-
/** 读取当前 Session 的增量 Message Mutation。 */
|
|
284
|
-
async message_changes(
|
|
285
|
-
input: ListSessionMessageChangesInput,
|
|
286
|
-
): Promise<SessionMessageMutationPage> {
|
|
287
|
-
return await this.recorder.list_message_changes(input);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
326
|
/**
|
|
291
327
|
* 读取当前 session 生效的 system 快照。
|
|
292
328
|
*/
|
|
@@ -319,8 +355,8 @@ export class Session implements AgentSession {
|
|
|
319
355
|
prompt: async (input) => await this.prompt(input),
|
|
320
356
|
stop: async () => await this.stop(),
|
|
321
357
|
subscribe: (subscriber) => this.subscribe(subscriber),
|
|
322
|
-
publishEvent: (event) => {
|
|
323
|
-
this.
|
|
358
|
+
publishEvent: async (event) => {
|
|
359
|
+
await this.apply_runtime_event(event);
|
|
324
360
|
},
|
|
325
361
|
append_user_message: async (message_params) => {
|
|
326
362
|
await this.stateService.append_user_message(message_params);
|
|
@@ -356,6 +392,10 @@ export class Session implements AgentSession {
|
|
|
356
392
|
getPluginSystemBlocks: this.getPluginSystemBlocks,
|
|
357
393
|
ensureConfigured: this.ensureConfiguredHook,
|
|
358
394
|
composers: this.composers,
|
|
395
|
+
list_approvals: this.list_approvals,
|
|
396
|
+
get_approval_mode: this.get_approval_mode,
|
|
397
|
+
set_approval_mode: this.set_approval_mode_hook,
|
|
398
|
+
resolve_approval: this.resolve_approval_hook,
|
|
359
399
|
});
|
|
360
400
|
}
|
|
361
401
|
|
|
@@ -383,6 +423,11 @@ export class Session implements AgentSession {
|
|
|
383
423
|
return new JsonlSessionMessageStore({
|
|
384
424
|
session_id: this.id,
|
|
385
425
|
file_path: `${messages_dir_path}/messages.jsonl`,
|
|
426
|
+
assistant_message_file_path: getSdkAgentSessionAssistantMessagePath(
|
|
427
|
+
this.projectRoot,
|
|
428
|
+
this.agentId,
|
|
429
|
+
this.id,
|
|
430
|
+
),
|
|
386
431
|
});
|
|
387
432
|
}
|
|
388
433
|
|
|
@@ -505,3 +550,15 @@ export class Session implements AgentSession {
|
|
|
505
550
|
return input;
|
|
506
551
|
}
|
|
507
552
|
}
|
|
553
|
+
|
|
554
|
+
function require_approval_id(mutation: SessionMutation): string {
|
|
555
|
+
if (
|
|
556
|
+
mutation.variant === "part" &&
|
|
557
|
+
mutation.type === "tool" &&
|
|
558
|
+
mutation.part.state === "approval-required" &&
|
|
559
|
+
mutation.part.approval_id
|
|
560
|
+
) {
|
|
561
|
+
return mutation.part.approval_id;
|
|
562
|
+
}
|
|
563
|
+
throw new Error("Current Session Mutation is not an approval-required Tool Part");
|
|
564
|
+
}
|
|
@@ -45,6 +45,8 @@ import { getSdkAgentArchivedSessionsDirPath } from "@/session/storage/Paths.js";
|
|
|
45
45
|
import { getSdkAgentArchivedSessionMessagesPath } from "@/session/storage/Paths.js";
|
|
46
46
|
import { getSdkAgentArchivedSessionMetaPath } from "@/session/storage/Paths.js";
|
|
47
47
|
import { readSessionMetadataFromPath } from "@/session/storage/Metadata.js";
|
|
48
|
+
import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
|
|
49
|
+
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
48
50
|
|
|
49
51
|
type AnyUiPart = UIMessagePart<Record<string, never>, Record<string, never>>;
|
|
50
52
|
|
|
@@ -324,46 +326,73 @@ export function toSessionTimelineEvents(
|
|
|
324
326
|
export async function loadSessionMessagesFromPath(
|
|
325
327
|
filePath: string,
|
|
326
328
|
): Promise<SessionRecordV1[]> {
|
|
327
|
-
const
|
|
329
|
+
const messages_by_id = new Map<string, SessionMessage>();
|
|
328
330
|
if (await fs.pathExists(filePath)) {
|
|
329
331
|
const raw = await fs.readFile(filePath, "utf-8");
|
|
330
332
|
const lines = raw.split("\n").filter(Boolean);
|
|
331
333
|
for (const line of lines) {
|
|
332
334
|
try {
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
candidate.role !== "user" &&
|
|
339
|
-
candidate.role !== "assistant"
|
|
340
|
-
) {
|
|
341
|
-
continue;
|
|
335
|
+
const message = JSON.parse(line) as SessionMessage;
|
|
336
|
+
if (!is_canonical_session_message(message)) continue;
|
|
337
|
+
const previous = messages_by_id.get(message.message_id);
|
|
338
|
+
if (!previous || message.revision > previous.revision) {
|
|
339
|
+
messages_by_id.set(message.message_id, message);
|
|
342
340
|
}
|
|
343
|
-
messages.push(parsed);
|
|
344
341
|
} catch {
|
|
345
342
|
// 关键点(中文):单行损坏不影响整个 session 的可读性。
|
|
346
343
|
}
|
|
347
344
|
}
|
|
348
345
|
}
|
|
349
346
|
|
|
350
|
-
const inflight_path = filePath.replace(/messages\.jsonl$/, "
|
|
347
|
+
const inflight_path = filePath.replace(/messages\.jsonl$/, "assistant_message.json");
|
|
351
348
|
if (await fs.pathExists(inflight_path)) {
|
|
352
349
|
try {
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
parsed.role === "assistant" &&
|
|
357
|
-
Array.isArray(parsed.parts)
|
|
358
|
-
) {
|
|
359
|
-
messages.push(parsed);
|
|
350
|
+
const message = (await fs.readJson(inflight_path)) as SessionMessage;
|
|
351
|
+
if (is_canonical_session_message(message) && message.type === "assistant") {
|
|
352
|
+
messages_by_id.set(message.message_id, message);
|
|
360
353
|
}
|
|
361
354
|
} catch {
|
|
362
|
-
//
|
|
355
|
+
// 运行中快照损坏时仍返回已经完成的历史。
|
|
363
356
|
}
|
|
364
357
|
}
|
|
365
358
|
|
|
366
|
-
return
|
|
359
|
+
return [...messages_by_id.values()]
|
|
360
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
361
|
+
.flatMap(project_canonical_message_record);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function is_canonical_session_message(input: unknown): input is SessionMessage {
|
|
365
|
+
if (!input || typeof input !== "object") return false;
|
|
366
|
+
const candidate = input as Partial<SessionMessage>;
|
|
367
|
+
return (
|
|
368
|
+
typeof candidate.message_id === "string" &&
|
|
369
|
+
typeof candidate.session_id === "string" &&
|
|
370
|
+
typeof candidate.sequence === "number" &&
|
|
371
|
+
typeof candidate.revision === "number" &&
|
|
372
|
+
(candidate.type === "user" ||
|
|
373
|
+
candidate.type === "assistant" ||
|
|
374
|
+
candidate.type === "action" ||
|
|
375
|
+
candidate.type === "error")
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function project_canonical_message_record(message: SessionMessage): SessionRecordV1[] {
|
|
380
|
+
const projected = to_executor_ui_message(message);
|
|
381
|
+
if (projected) return [projected];
|
|
382
|
+
if (message.type !== "action") return [];
|
|
383
|
+
return [{
|
|
384
|
+
type: "action",
|
|
385
|
+
id: message.message_id,
|
|
386
|
+
title: message.title,
|
|
387
|
+
...(message.description ? { description: message.description } : {}),
|
|
388
|
+
state: message.status,
|
|
389
|
+
metadata: {
|
|
390
|
+
v: 1,
|
|
391
|
+
ts: message.updated_at,
|
|
392
|
+
sessionId: message.session_id,
|
|
393
|
+
...(message.turn_id ? { turnId: message.turn_id } : {}),
|
|
394
|
+
},
|
|
395
|
+
}];
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
/**
|
|
@@ -481,7 +510,7 @@ async function resolve_session_summary_metadata(input: {
|
|
|
481
510
|
const history_bytes = await fs.stat(input.messagesPath)
|
|
482
511
|
.then((file_stat) => file_stat.size)
|
|
483
512
|
.catch(() => 0);
|
|
484
|
-
const inflight_path = `${input.messagesPath.slice(0, -"messages.jsonl".length)}
|
|
513
|
+
const inflight_path = `${input.messagesPath.slice(0, -"messages.jsonl".length)}assistant_message.json`;
|
|
485
514
|
const has_inflight = await fs.pathExists(inflight_path);
|
|
486
515
|
if (
|
|
487
516
|
!input.refresh &&
|
|
@@ -1,121 +1,217 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SessionMessage 的 JSONL 快照存储。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* messages.jsonl 只保存完整 Message 快照;运行中的唯一 Assistant 使用
|
|
5
|
+
* assistant_message.json 原子覆盖,避免把每个 delta 写入历史文件。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import fs from "fs-extra";
|
|
8
9
|
import { open, readFile, stat } from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { generateId } from "@/utils/Id.js";
|
|
11
|
-
import {
|
|
12
|
-
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
13
|
-
import type { SessionMessageMutation } from "@/types/session/SessionMessageMutation.js";
|
|
12
|
+
import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
|
|
14
13
|
|
|
15
|
-
/** JSONL
|
|
14
|
+
/** JSONL 快照存储构造参数。 */
|
|
16
15
|
export interface JsonlSessionMessageStoreOptions {
|
|
17
|
-
/** 当前
|
|
16
|
+
/** 当前 Session 标识。 */
|
|
18
17
|
session_id: string;
|
|
19
|
-
/**
|
|
18
|
+
/** messages.jsonl 的绝对或相对路径。 */
|
|
20
19
|
file_path: string;
|
|
20
|
+
/** Assistant 运行中快照路径,默认与 messages.jsonl 同目录。 */
|
|
21
|
+
assistant_message_file_path?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/** 在写锁内计算新 Message 所需的状态。 */
|
|
24
25
|
export interface SessionMessageCommitState {
|
|
25
|
-
/**
|
|
26
|
-
commit_sequence: number;
|
|
27
|
-
/** 下一条新消息应使用的线性顺序。 */
|
|
26
|
+
/** 下一条新 Message 的线性顺序。 */
|
|
28
27
|
message_sequence: number;
|
|
29
|
-
/**
|
|
28
|
+
/** 当前已持久化的完整 Message 快照。 */
|
|
30
29
|
messages: SessionMessage[];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
/** 单一 JSONL
|
|
32
|
+
/** 单一 JSONL 快照存储。 */
|
|
34
33
|
export class JsonlSessionMessageStore {
|
|
35
34
|
readonly session_id: string;
|
|
36
|
-
|
|
35
|
+
readonly messages_file_path: string;
|
|
36
|
+
readonly assistant_message_file_path: string;
|
|
37
37
|
private readonly lock_file_path: string;
|
|
38
38
|
|
|
39
39
|
constructor(options: JsonlSessionMessageStoreOptions) {
|
|
40
40
|
this.session_id = String(options.session_id || "").trim();
|
|
41
41
|
this.messages_file_path = path.resolve(options.file_path);
|
|
42
|
+
this.assistant_message_file_path = path.resolve(
|
|
43
|
+
options.assistant_message_file_path ||
|
|
44
|
+
path.join(path.dirname(this.messages_file_path), "assistant_message.json"),
|
|
45
|
+
);
|
|
42
46
|
this.lock_file_path = `${this.messages_file_path}.lock`;
|
|
43
|
-
if (!this.session_id)
|
|
44
|
-
throw new Error("JsonlSessionMessageStore requires session_id");
|
|
45
|
-
}
|
|
47
|
+
if (!this.session_id) throw new Error("JsonlSessionMessageStore requires session_id");
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
/**
|
|
50
|
+
/** 创建目录和文件,并验证已有快照。 */
|
|
49
51
|
async initialize(): Promise<void> {
|
|
50
52
|
await this.ensure_layout();
|
|
51
|
-
|
|
53
|
+
const messages = await this.read_messages_unsafe();
|
|
54
|
+
const draft = await this.read_assistant_message();
|
|
55
|
+
if (draft && draft.session_id !== this.session_id) {
|
|
56
|
+
throw new Error("assistant_message.json session_id mismatch");
|
|
57
|
+
}
|
|
58
|
+
const finalized = draft && messages.find((message) => message.message_id === draft.message_id);
|
|
59
|
+
if (draft && finalized && finalized.revision >= draft.revision && finalized.type === "assistant" && finalized.status !== "streaming") {
|
|
60
|
+
await fs.remove(this.assistant_message_file_path);
|
|
61
|
+
}
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
/**
|
|
55
|
-
async
|
|
64
|
+
/** 读取 JSONL 中每一行完整快照,并按 Message revision 折叠。 */
|
|
65
|
+
async list_messages(): Promise<SessionMessage[]> {
|
|
56
66
|
await this.ensure_layout();
|
|
57
|
-
|
|
67
|
+
const by_id = new Map<string, SessionMessage>();
|
|
68
|
+
const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
|
|
69
|
+
for (const [index, line] of raw.split("\n").entries()) {
|
|
70
|
+
const value = line.trim();
|
|
71
|
+
if (!value) continue;
|
|
72
|
+
try {
|
|
73
|
+
const message = JSON.parse(value) as SessionMessage;
|
|
74
|
+
this.validate_message(message);
|
|
75
|
+
const previous = by_id.get(message.message_id);
|
|
76
|
+
if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new Error(`Invalid session message at line ${String(index + 1)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const draft = await this.read_assistant_message();
|
|
82
|
+
if (draft) {
|
|
83
|
+
const previous = by_id.get(draft.message_id);
|
|
84
|
+
if (!previous || draft.revision > previous.revision) by_id.set(draft.message_id, draft);
|
|
85
|
+
}
|
|
86
|
+
return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
|
|
58
87
|
}
|
|
59
88
|
|
|
60
|
-
/**
|
|
61
|
-
async
|
|
62
|
-
|
|
89
|
+
/** 读取当前运行中的 Assistant 草稿。 */
|
|
90
|
+
async read_assistant_message(): Promise<SessionAssistantMessage | null> {
|
|
91
|
+
try {
|
|
92
|
+
const value = await fs.readJson(this.assistant_message_file_path) as SessionAssistantMessage;
|
|
93
|
+
this.validate_message(value);
|
|
94
|
+
if (value.type !== "assistant" || value.status !== "streaming") return null;
|
|
95
|
+
return value;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
98
|
+
if (error instanceof SyntaxError) throw error;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 原子覆盖运行中的 Assistant 草稿。 */
|
|
104
|
+
async write_assistant_message(message: SessionAssistantMessage): Promise<void> {
|
|
105
|
+
this.validate_message(message);
|
|
106
|
+
if (message.status !== "streaming") throw new Error("Assistant draft must be streaming");
|
|
107
|
+
await this.with_write_lock(async () => {
|
|
108
|
+
const current = await this.read_assistant_message();
|
|
109
|
+
if (!current || current.message_id !== message.message_id) {
|
|
110
|
+
throw new Error(`Assistant draft does not exist: ${message.message_id}`);
|
|
111
|
+
}
|
|
112
|
+
if (message.revision !== current.revision + 1) {
|
|
113
|
+
throw new Error(`Invalid Assistant draft revision: ${message.message_id}`);
|
|
114
|
+
}
|
|
115
|
+
await this.write_assistant_message_unsafe(message);
|
|
116
|
+
});
|
|
63
117
|
}
|
|
64
118
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
async commit(
|
|
70
|
-
build_mutation: (state: SessionMessageCommitState) => SessionMessageMutation,
|
|
71
|
-
): Promise<SessionMessageMutation> {
|
|
119
|
+
/** 在写锁内分配 Message sequence 并创建唯一 Assistant 草稿。 */
|
|
120
|
+
async create_assistant_message(
|
|
121
|
+
build_message: (state: SessionMessageCommitState) => SessionAssistantMessage,
|
|
122
|
+
): Promise<SessionAssistantMessage> {
|
|
72
123
|
return await this.with_write_lock(async () => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
message_sequence:
|
|
79
|
-
messages.reduce(
|
|
80
|
-
(value, message) => Math.max(value, message.sequence + 1),
|
|
81
|
-
1,
|
|
82
|
-
),
|
|
124
|
+
const current = await this.read_assistant_message();
|
|
125
|
+
if (current) throw new Error(`Assistant draft already exists: ${current.message_id}`);
|
|
126
|
+
const messages = await this.read_messages_unsafe();
|
|
127
|
+
const message = build_message({
|
|
128
|
+
message_sequence: messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
|
|
83
129
|
messages,
|
|
84
130
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
return
|
|
131
|
+
this.validate_message(message);
|
|
132
|
+
if (message.status !== "streaming" || message.revision !== 1) {
|
|
133
|
+
throw new Error("New Assistant draft must be streaming at revision 1");
|
|
134
|
+
}
|
|
135
|
+
await this.write_assistant_message_unsafe(message);
|
|
136
|
+
return message;
|
|
91
137
|
});
|
|
92
138
|
}
|
|
93
139
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
140
|
+
/** 创建新 Message 或追加已有 Message revision 快照。 */
|
|
141
|
+
async append_message(
|
|
142
|
+
build_message: (state: SessionMessageCommitState) => SessionMessage,
|
|
143
|
+
): Promise<SessionMessage> {
|
|
144
|
+
return await this.with_write_lock(async () => {
|
|
145
|
+
const messages = await this.read_messages_unsafe();
|
|
146
|
+
const draft = await this.read_assistant_message();
|
|
147
|
+
const current_messages = draft ? [...messages, draft] : messages;
|
|
148
|
+
const message = build_message({
|
|
149
|
+
message_sequence: current_messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
|
|
150
|
+
messages: current_messages,
|
|
151
|
+
});
|
|
152
|
+
this.validate_message(message);
|
|
153
|
+
await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
|
|
154
|
+
return message;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 将 Assistant 草稿最终追加为历史快照,并删除草稿文件。 */
|
|
159
|
+
async finalize_assistant_message(message: SessionAssistantMessage): Promise<void> {
|
|
160
|
+
this.validate_message(message);
|
|
161
|
+
if (message.status === "streaming") throw new Error("Final Assistant message cannot be streaming");
|
|
162
|
+
await this.with_write_lock(async () => {
|
|
163
|
+
const current = await this.read_assistant_message();
|
|
164
|
+
if (!current || current.message_id !== message.message_id) {
|
|
165
|
+
throw new Error(`Assistant draft does not exist: ${message.message_id}`);
|
|
166
|
+
}
|
|
167
|
+
if (message.revision !== current.revision + 1) {
|
|
168
|
+
throw new Error(`Invalid final Assistant revision: ${message.message_id}`);
|
|
169
|
+
}
|
|
170
|
+
await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
|
|
171
|
+
await fs.remove(this.assistant_message_file_path);
|
|
172
|
+
});
|
|
97
173
|
}
|
|
98
174
|
|
|
99
|
-
private async
|
|
175
|
+
private async read_messages_unsafe(): Promise<SessionMessage[]> {
|
|
100
176
|
const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
177
|
+
const by_id = new Map<string, SessionMessage>();
|
|
178
|
+
for (const line of raw.split("\n")) {
|
|
179
|
+
if (!line.trim()) continue;
|
|
180
|
+
const message = JSON.parse(line) as SessionMessage;
|
|
181
|
+
this.validate_message(message);
|
|
182
|
+
const previous = by_id.get(message.message_id);
|
|
183
|
+
if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
|
|
184
|
+
}
|
|
185
|
+
return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async write_assistant_message_unsafe(message: SessionAssistantMessage): Promise<void> {
|
|
189
|
+
const temporary_path = `${this.assistant_message_file_path}.${process.pid}.${Date.now()}.tmp`;
|
|
190
|
+
await fs.ensureDir(path.dirname(this.assistant_message_file_path));
|
|
191
|
+
await fs.writeJson(temporary_path, message, { spaces: 2 });
|
|
192
|
+
await fs.move(temporary_path, this.assistant_message_file_path, { overwrite: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private validate_message(message: SessionMessage): void {
|
|
196
|
+
if (!message || typeof message !== "object") throw new Error("message must be an object");
|
|
197
|
+
if (message.session_id !== this.session_id) throw new Error("session_id mismatch");
|
|
198
|
+
if (!message.message_id || !Number.isInteger(message.sequence) || !Number.isInteger(message.revision)) {
|
|
199
|
+
throw new Error("message identity, sequence and revision are required");
|
|
200
|
+
}
|
|
201
|
+
if (message.type === "assistant") {
|
|
202
|
+
const sequences = new Set<number>();
|
|
203
|
+
for (const part of message.parts) {
|
|
204
|
+
if (!Number.isInteger(part.sequence) || sequences.has(part.sequence)) {
|
|
205
|
+
throw new Error(`invalid Assistant part sequence: ${part.part_id}`);
|
|
110
206
|
}
|
|
111
|
-
|
|
112
|
-
} catch (error) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Invalid session mutation at line ${String(index + 1)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
-
);
|
|
207
|
+
sequences.add(part.sequence);
|
|
116
208
|
}
|
|
117
209
|
}
|
|
118
|
-
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async ensure_layout(): Promise<void> {
|
|
213
|
+
await fs.ensureDir(path.dirname(this.messages_file_path));
|
|
214
|
+
await fs.ensureFile(this.messages_file_path);
|
|
119
215
|
}
|
|
120
216
|
|
|
121
217
|
private async with_write_lock<T>(callback: () => Promise<T>): Promise<T> {
|
|
@@ -130,20 +226,12 @@ export class JsonlSessionMessageStore {
|
|
|
130
226
|
await file.close();
|
|
131
227
|
break;
|
|
132
228
|
} catch (error) {
|
|
133
|
-
|
|
134
|
-
if (code !== "EEXIST") throw error;
|
|
229
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
135
230
|
try {
|
|
136
231
|
const lock_stat = await stat(this.lock_file_path);
|
|
137
|
-
if (Date.now() - lock_stat.mtimeMs > stale_ms)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (Date.now() - started_at > stale_ms * 2) {
|
|
145
|
-
throw new Error(`Session message lock timeout: ${this.lock_file_path}`);
|
|
146
|
-
}
|
|
232
|
+
if (Date.now() - lock_stat.mtimeMs > stale_ms) await fs.remove(this.lock_file_path);
|
|
233
|
+
} catch { /* 锁文件可能刚被释放。 */ }
|
|
234
|
+
if (Date.now() - started_at > stale_ms * 2) throw new Error(`Session message lock timeout: ${this.lock_file_path}`);
|
|
147
235
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
148
236
|
}
|
|
149
237
|
}
|
|
@@ -151,12 +239,8 @@ export class JsonlSessionMessageStore {
|
|
|
151
239
|
return await callback();
|
|
152
240
|
} finally {
|
|
153
241
|
try {
|
|
154
|
-
if ((await readFile(this.lock_file_path, "utf8")).trim() === token)
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// 锁文件已被清理时无需额外处理。
|
|
159
|
-
}
|
|
242
|
+
if ((await readFile(this.lock_file_path, "utf8")).trim() === token) await fs.remove(this.lock_file_path);
|
|
243
|
+
} catch { /* 锁已被清理。 */ }
|
|
160
244
|
}
|
|
161
245
|
}
|
|
162
246
|
}
|