@downcity/agent 1.1.222 → 1.1.226
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 +23 -11
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +187 -112
- 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 +141 -140
- 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 +228 -134
- 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
|
@@ -109,6 +109,7 @@ export function from_ui_assistant_parts(
|
|
|
109
109
|
if (type === "text" || type === "reasoning") {
|
|
110
110
|
return [{
|
|
111
111
|
part_id: `${type}:${index + 1}`,
|
|
112
|
+
sequence: index + 1,
|
|
112
113
|
type: type as "text" | "reasoning",
|
|
113
114
|
text: String(candidate.text || ""),
|
|
114
115
|
state: "done" as const,
|
|
@@ -117,6 +118,7 @@ export function from_ui_assistant_parts(
|
|
|
117
118
|
if (type === "file") {
|
|
118
119
|
return [{
|
|
119
120
|
part_id: `file:${index + 1}`,
|
|
121
|
+
sequence: index + 1,
|
|
120
122
|
type: "file" as const,
|
|
121
123
|
url: String(candidate.url || ""),
|
|
122
124
|
media_type: String(candidate.mediaType || "application/octet-stream"),
|
|
@@ -127,6 +129,7 @@ export function from_ui_assistant_parts(
|
|
|
127
129
|
const state = String(candidate.state || "");
|
|
128
130
|
return [{
|
|
129
131
|
part_id: String(candidate.toolCallId || `tool:${index + 1}`),
|
|
132
|
+
sequence: index + 1,
|
|
130
133
|
type: "tool" as const,
|
|
131
134
|
tool_call_id: String(candidate.toolCallId || `tool:${index + 1}`),
|
|
132
135
|
tool_name:
|
|
@@ -142,7 +145,9 @@ export function from_ui_assistant_parts(
|
|
|
142
145
|
? "approval-required" as const
|
|
143
146
|
: state === "input-streaming"
|
|
144
147
|
? "input-streaming" as const
|
|
145
|
-
: "
|
|
148
|
+
: state === "input-available"
|
|
149
|
+
? "ready" as const
|
|
150
|
+
: "running" as const,
|
|
146
151
|
...(candidate.input !== undefined
|
|
147
152
|
? { input: to_json_value(candidate.input) }
|
|
148
153
|
: {}),
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SessionRecorder:Session Message 的唯一持久化与发布入口。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 完整 Message 与 Assistant 草稿先持久化,成功后再发布实时 Mutation。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
8
8
|
import { generateId } from "@/utils/Id.js";
|
|
9
9
|
import { JsonlSessionMessageStore } from "@/session/recorder/JsonlSessionMessageStore.js";
|
|
10
|
-
import { reduce_session_message } from "@/session/recorder/SessionMessageReducer.js";
|
|
11
10
|
import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
12
11
|
import type {
|
|
13
12
|
ListSessionMessagesInput,
|
|
@@ -22,23 +21,18 @@ import type {
|
|
|
22
21
|
SessionUserMessagePart,
|
|
23
22
|
} from "@/types/session/SessionMessage.js";
|
|
24
23
|
import type {
|
|
25
|
-
|
|
26
|
-
SessionMessageMutation,
|
|
27
|
-
|
|
28
|
-
SessionMessageMutationSubscriber,
|
|
29
|
-
SessionMessageMutationUnsubscribe,
|
|
30
|
-
SessionMessageSnapshotMutation,
|
|
31
|
-
SessionPartSnapshotMutation,
|
|
32
|
-
} from "@/types/session/SessionMessageMutation.js";
|
|
24
|
+
SessionMutation,
|
|
25
|
+
SessionMessageMutation as SessionMessageSnapshotMutation,
|
|
26
|
+
} from "@/types/session/SessionMutation.js";
|
|
33
27
|
|
|
34
28
|
/** SessionRecorder 构造参数。 */
|
|
35
29
|
export interface SessionRecorderOptions {
|
|
36
30
|
/** 当前 Session 标识。 */
|
|
37
31
|
session_id: string;
|
|
38
|
-
/**
|
|
32
|
+
/** Message 快照 store。 */
|
|
39
33
|
store: JsonlSessionMessageStore;
|
|
40
|
-
/** Mutation
|
|
41
|
-
publish: (mutation:
|
|
34
|
+
/** 持久化成功后的实时 Mutation 发布函数。 */
|
|
35
|
+
publish: (mutation: SessionMutation) => void;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
/** User Message 创建参数。 */
|
|
@@ -120,7 +114,6 @@ export class SessionRecorder {
|
|
|
120
114
|
readonly session_id: string;
|
|
121
115
|
private readonly store: JsonlSessionMessageStore;
|
|
122
116
|
private readonly publish: SessionRecorderOptions["publish"];
|
|
123
|
-
private readonly subscribers = new Set<SessionMessageMutationSubscriber>();
|
|
124
117
|
private readonly messages_by_id = new Map<string, SessionMessage>();
|
|
125
118
|
private initialized = false;
|
|
126
119
|
|
|
@@ -131,7 +124,7 @@ export class SessionRecorder {
|
|
|
131
124
|
if (!this.session_id) throw new Error("SessionRecorder requires session_id");
|
|
132
125
|
}
|
|
133
126
|
|
|
134
|
-
/**
|
|
127
|
+
/** 恢复已有 Message,并收口进程中断遗留的运行状态。 */
|
|
135
128
|
async initialize(): Promise<void> {
|
|
136
129
|
if (this.initialized) return;
|
|
137
130
|
await this.store.initialize();
|
|
@@ -202,7 +195,7 @@ export class SessionRecorder {
|
|
|
202
195
|
...(input.summary_through_message_id
|
|
203
196
|
? { summary_through_message_id: input.summary_through_message_id }
|
|
204
197
|
: {}),
|
|
205
|
-
}))) as SessionAssistantMessage;
|
|
198
|
+
}), true)) as SessionAssistantMessage;
|
|
206
199
|
return new SessionAssistantMessageWriter(this, message.message_id);
|
|
207
200
|
}
|
|
208
201
|
|
|
@@ -256,10 +249,10 @@ export class SessionRecorder {
|
|
|
256
249
|
status: "running" | "completed" | "failed",
|
|
257
250
|
changes?: { title?: string; description?: string; data?: JsonObject },
|
|
258
251
|
): Promise<SessionActionMessage> {
|
|
259
|
-
const
|
|
252
|
+
const message = await this.store.append_message((state) => {
|
|
260
253
|
const current = require_message(state.messages, message_id, "action");
|
|
261
254
|
const created_at = Date.now();
|
|
262
|
-
|
|
255
|
+
return {
|
|
263
256
|
...current,
|
|
264
257
|
status,
|
|
265
258
|
...(changes?.title ? { title: changes.title } : {}),
|
|
@@ -269,11 +262,10 @@ export class SessionRecorder {
|
|
|
269
262
|
...(changes?.data ? { data: structuredClone(changes.data) } : {}),
|
|
270
263
|
revision: current.revision + 1,
|
|
271
264
|
updated_at: created_at,
|
|
272
|
-
};
|
|
273
|
-
return this.build_message_mutation(state.commit_sequence, message);
|
|
265
|
+
} satisfies SessionActionMessage;
|
|
274
266
|
});
|
|
275
|
-
this.
|
|
276
|
-
return
|
|
267
|
+
this.accept_message(message);
|
|
268
|
+
return message as SessionActionMessage;
|
|
277
269
|
}
|
|
278
270
|
|
|
279
271
|
/** 创建用户可见 Error Message。 */
|
|
@@ -302,9 +294,6 @@ export class SessionRecorder {
|
|
|
302
294
|
input?: ListSessionMessagesInput,
|
|
303
295
|
): Promise<SessionMessagePage> {
|
|
304
296
|
await this.ensure_initialized();
|
|
305
|
-
const mutations = await this.store.list_mutations();
|
|
306
|
-
const latest_commit_sequence =
|
|
307
|
-
mutations[mutations.length - 1]?.commit_sequence || 0;
|
|
308
297
|
const include_internal = input?.include_internal === true;
|
|
309
298
|
const through_sequence = Number.isFinite(input?.through_sequence)
|
|
310
299
|
? Number(input?.through_sequence)
|
|
@@ -329,38 +318,9 @@ export class SessionRecorder {
|
|
|
329
318
|
total: all.length,
|
|
330
319
|
...(next_offset < all.length ? { next_cursor: encode_cursor(next_offset) } : {}),
|
|
331
320
|
has_more: next_offset < all.length,
|
|
332
|
-
latest_commit_sequence,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** 从指定 commit_sequence 增量读取 Mutation。 */
|
|
337
|
-
async list_message_changes(
|
|
338
|
-
input: ListSessionMessageChangesInput,
|
|
339
|
-
): Promise<SessionMessageMutationPage> {
|
|
340
|
-
await this.ensure_initialized();
|
|
341
|
-
const after = Math.max(0, Math.floor(input.after_commit_sequence || 0));
|
|
342
|
-
const limit = normalize_limit(input.limit, 200, 1000);
|
|
343
|
-
const all = await this.store.list_mutations();
|
|
344
|
-
const matching = all.filter((item) => item.commit_sequence > after);
|
|
345
|
-
const items = matching.slice(0, limit);
|
|
346
|
-
const latest_commit_sequence = all[all.length - 1]?.commit_sequence || 0;
|
|
347
|
-
return {
|
|
348
|
-
items,
|
|
349
|
-
has_more: matching.length > items.length,
|
|
350
|
-
next_commit_sequence:
|
|
351
|
-
items[items.length - 1]?.commit_sequence || after,
|
|
352
|
-
latest_commit_sequence,
|
|
353
321
|
};
|
|
354
322
|
}
|
|
355
323
|
|
|
356
|
-
/** 订阅已成功持久化的未来 Mutation。 */
|
|
357
|
-
subscribe(
|
|
358
|
-
subscriber: SessionMessageMutationSubscriber,
|
|
359
|
-
): SessionMessageMutationUnsubscribe {
|
|
360
|
-
this.subscribers.add(subscriber);
|
|
361
|
-
return () => this.subscribers.delete(subscriber);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
324
|
/** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
|
|
365
325
|
async import_messages(messages: SessionMessage[]): Promise<void> {
|
|
366
326
|
const turn_ids = new Map<string, string>();
|
|
@@ -400,23 +360,37 @@ export class SessionRecorder {
|
|
|
400
360
|
delta: string,
|
|
401
361
|
): Promise<void> {
|
|
402
362
|
if (!delta) return;
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
363
|
+
const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
|
|
364
|
+
require_streaming_assistant(current);
|
|
365
|
+
const part = current.parts.find((item) => item.part_id === part_id);
|
|
366
|
+
if (!part || (part.type !== "text" && part.type !== "reasoning")) {
|
|
367
|
+
throw new Error(`Delta target Part does not exist: ${part_id}`);
|
|
368
|
+
}
|
|
369
|
+
if (part.type !== type) throw new Error(`Delta type changed for Part: ${part_id}`);
|
|
370
|
+
const created_at = Date.now();
|
|
371
|
+
const message: SessionAssistantMessage = {
|
|
372
|
+
...current,
|
|
373
|
+
revision: current.revision + 1,
|
|
374
|
+
updated_at: created_at,
|
|
375
|
+
parts: current.parts.map((item) =>
|
|
376
|
+
item.part_id === part_id && (item.type === "text" || item.type === "reasoning")
|
|
377
|
+
? { ...item, text: item.text + delta }
|
|
378
|
+
: item,
|
|
379
|
+
),
|
|
380
|
+
};
|
|
381
|
+
await this.store.write_assistant_message(message);
|
|
382
|
+
this.accept_mutation({
|
|
383
|
+
mutation_id: generateId(),
|
|
384
|
+
variant: "delta",
|
|
385
|
+
type,
|
|
386
|
+
message_id,
|
|
387
|
+
revision: message.revision,
|
|
388
|
+
session_id: this.session_id,
|
|
389
|
+
turn_id: message.turn_id,
|
|
390
|
+
created_at,
|
|
391
|
+
part_id,
|
|
392
|
+
delta,
|
|
393
|
+
}, message);
|
|
420
394
|
}
|
|
421
395
|
|
|
422
396
|
/** @internal 写入 Assistant 完整 part。 */
|
|
@@ -424,23 +398,36 @@ export class SessionRecorder {
|
|
|
424
398
|
message_id: string,
|
|
425
399
|
part: SessionAssistantMessagePart,
|
|
426
400
|
): Promise<void> {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
part:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
401
|
+
const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
|
|
402
|
+
require_streaming_assistant(current);
|
|
403
|
+
const existing = current.parts.find((item) => item.part_id === part.part_id);
|
|
404
|
+
if (existing && existing.sequence !== part.sequence) {
|
|
405
|
+
throw new Error(`Assistant Part sequence changed: ${part.part_id}`);
|
|
406
|
+
}
|
|
407
|
+
const created_at = Date.now();
|
|
408
|
+
const next_part = structuredClone(part);
|
|
409
|
+
const message: SessionAssistantMessage = {
|
|
410
|
+
...current,
|
|
411
|
+
revision: current.revision + 1,
|
|
412
|
+
updated_at: created_at,
|
|
413
|
+
parts: (existing
|
|
414
|
+
? current.parts.map((item) => item.part_id === part.part_id ? next_part : item)
|
|
415
|
+
: [...current.parts, next_part]
|
|
416
|
+
).sort((left, right) => left.sequence - right.sequence),
|
|
417
|
+
};
|
|
418
|
+
await this.store.write_assistant_message(message);
|
|
419
|
+
this.accept_mutation({
|
|
420
|
+
mutation_id: generateId(),
|
|
421
|
+
variant: "part",
|
|
422
|
+
type: next_part.type,
|
|
423
|
+
message_id,
|
|
424
|
+
revision: message.revision,
|
|
425
|
+
session_id: this.session_id,
|
|
426
|
+
turn_id: message.turn_id,
|
|
427
|
+
created_at,
|
|
428
|
+
part_id: next_part.part_id,
|
|
429
|
+
part: next_part,
|
|
430
|
+
} as SessionMutation, message);
|
|
444
431
|
}
|
|
445
432
|
|
|
446
433
|
/** @internal 收口 Assistant Message。 */
|
|
@@ -448,45 +435,100 @@ export class SessionRecorder {
|
|
|
448
435
|
message_id: string,
|
|
449
436
|
status: "completed" | "stopped" | "failed",
|
|
450
437
|
): Promise<void> {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
438
|
+
const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
|
|
439
|
+
require_streaming_assistant(current);
|
|
440
|
+
const created_at = Date.now();
|
|
441
|
+
const message: SessionAssistantMessage = {
|
|
442
|
+
...current,
|
|
443
|
+
revision: current.revision + 1,
|
|
444
|
+
status,
|
|
445
|
+
updated_at: created_at,
|
|
446
|
+
parts: current.parts.map((part) =>
|
|
447
|
+
part.type === "text" || part.type === "reasoning"
|
|
448
|
+
? { ...part, state: "done" as const }
|
|
449
|
+
: part,
|
|
450
|
+
),
|
|
451
|
+
};
|
|
452
|
+
await this.store.finalize_assistant_message(message);
|
|
453
|
+
this.accept_message(message);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @internal 将 Shell Runtime 审批请求更新到当前 Tool Part。 */
|
|
457
|
+
async require_tool_approval(input: {
|
|
458
|
+
tool_call_id: string;
|
|
459
|
+
approval_id: string;
|
|
460
|
+
}): Promise<void> {
|
|
461
|
+
const tool = this.find_streaming_tool(input.tool_call_id);
|
|
462
|
+
await this.update_assistant_part(tool.message_id, {
|
|
463
|
+
...tool.part,
|
|
464
|
+
state: "approval-required",
|
|
465
|
+
approval_id: input.approval_id,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** @internal 将审批结果更新到当前 Tool Part。 */
|
|
470
|
+
async resolve_tool_approval(input: {
|
|
471
|
+
tool_call_id: string;
|
|
472
|
+
approval_id: string;
|
|
473
|
+
decision: "approved" | "denied" | "expired";
|
|
474
|
+
}): Promise<void> {
|
|
475
|
+
const tool = this.find_streaming_tool(input.tool_call_id);
|
|
476
|
+
if (tool.part.approval_id && tool.part.approval_id !== input.approval_id) {
|
|
477
|
+
throw new Error(`Tool approval identity mismatch: ${input.approval_id}`);
|
|
478
|
+
}
|
|
479
|
+
await this.update_assistant_part(tool.message_id, {
|
|
480
|
+
...tool.part,
|
|
481
|
+
state: input.decision === "approved" ? "running" : "failed",
|
|
482
|
+
...(input.decision === "approved"
|
|
483
|
+
? {}
|
|
484
|
+
: { error: input.decision === "expired" ? "Approval expired" : "Approval denied" }),
|
|
466
485
|
});
|
|
467
|
-
this.accept_mutation(mutation);
|
|
468
486
|
}
|
|
469
487
|
|
|
470
488
|
private async create_message(
|
|
471
489
|
factory: (sequence: number, created_at: number) => SessionMessage,
|
|
490
|
+
draft = false,
|
|
472
491
|
): Promise<SessionMessage> {
|
|
473
|
-
|
|
474
|
-
const message =
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
492
|
+
if (draft) {
|
|
493
|
+
const message = await this.store.create_assistant_message((state) => {
|
|
494
|
+
const candidate = factory(state.message_sequence, Date.now());
|
|
495
|
+
if (candidate.type !== "assistant" || candidate.status !== "streaming") {
|
|
496
|
+
throw new Error("Draft Message must be a streaming Assistant");
|
|
497
|
+
}
|
|
498
|
+
return candidate;
|
|
499
|
+
});
|
|
500
|
+
this.accept_message(message);
|
|
501
|
+
return message;
|
|
502
|
+
}
|
|
503
|
+
const message = await this.store.append_message((state) =>
|
|
504
|
+
factory(state.message_sequence, Date.now()),
|
|
505
|
+
);
|
|
506
|
+
this.accept_message(message);
|
|
507
|
+
return message;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private find_streaming_tool(tool_call_id: string): {
|
|
511
|
+
message_id: string;
|
|
512
|
+
part: SessionAssistantToolPart;
|
|
513
|
+
} {
|
|
514
|
+
for (const message of this.messages_by_id.values()) {
|
|
515
|
+
if (message.type !== "assistant" || message.status !== "streaming") continue;
|
|
516
|
+
const part = message.parts.find(
|
|
517
|
+
(item): item is SessionAssistantToolPart =>
|
|
518
|
+
item.type === "tool" && item.tool_call_id === tool_call_id,
|
|
519
|
+
);
|
|
520
|
+
if (part) return { message_id: message.message_id, part };
|
|
521
|
+
}
|
|
522
|
+
throw new Error(`Streaming Tool Part not found: ${tool_call_id}`);
|
|
479
523
|
}
|
|
480
524
|
|
|
481
525
|
private build_message_mutation(
|
|
482
|
-
commit_sequence: number,
|
|
483
526
|
message: SessionMessage,
|
|
484
527
|
): SessionMessageSnapshotMutation {
|
|
485
528
|
return {
|
|
486
529
|
mutation_id: generateId(),
|
|
487
530
|
variant: "message",
|
|
488
531
|
type: message.type,
|
|
489
|
-
commit_sequence,
|
|
490
532
|
message_id: message.message_id,
|
|
491
533
|
sequence: message.sequence,
|
|
492
534
|
revision: message.revision,
|
|
@@ -497,18 +539,13 @@ export class SessionRecorder {
|
|
|
497
539
|
} as SessionMessageSnapshotMutation;
|
|
498
540
|
}
|
|
499
541
|
|
|
500
|
-
private
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
542
|
+
private accept_message(message: SessionMessage): void {
|
|
543
|
+
this.accept_mutation(this.build_message_mutation(message), message);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private accept_mutation(mutation: SessionMutation, message: SessionMessage): void {
|
|
547
|
+
this.messages_by_id.set(message.message_id, structuredClone(message));
|
|
504
548
|
this.publish(mutation);
|
|
505
|
-
for (const subscriber of this.subscribers) {
|
|
506
|
-
try {
|
|
507
|
-
subscriber(mutation);
|
|
508
|
-
} catch {
|
|
509
|
-
// 单个订阅者异常不能影响已经完成的提交。
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
549
|
}
|
|
513
550
|
|
|
514
551
|
private async ensure_initialized(): Promise<void> {
|
|
@@ -520,6 +557,7 @@ export class SessionRecorder {
|
|
|
520
557
|
export class SessionAssistantMessageWriter {
|
|
521
558
|
readonly message_id: string;
|
|
522
559
|
private readonly recorder: SessionRecorder;
|
|
560
|
+
private readonly pending_text_parts = new Map<string, "text" | "reasoning">();
|
|
523
561
|
private closed = false;
|
|
524
562
|
|
|
525
563
|
constructor(recorder: SessionRecorder, message_id: string) {
|
|
@@ -533,29 +571,35 @@ export class SessionAssistantMessageWriter {
|
|
|
533
571
|
const current = this.current_message();
|
|
534
572
|
switch (chunk.type) {
|
|
535
573
|
case "text-start":
|
|
536
|
-
case "reasoning-start":
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
});
|
|
574
|
+
case "reasoning-start": {
|
|
575
|
+
const type = chunk.type === "text-start" ? "text" : "reasoning";
|
|
576
|
+
const part_id = `${type}:${chunk.id}`;
|
|
577
|
+
if (!current.parts.some((part) => part.part_id === part_id)) {
|
|
578
|
+
this.pending_text_parts.set(part_id, type);
|
|
579
|
+
}
|
|
543
580
|
return;
|
|
581
|
+
}
|
|
544
582
|
case "text-delta":
|
|
545
|
-
case "reasoning-delta":
|
|
583
|
+
case "reasoning-delta": {
|
|
584
|
+
const type = chunk.type === "text-delta" ? "text" : "reasoning";
|
|
585
|
+
const part_id = `${type}:${chunk.id}`;
|
|
586
|
+
await this.ensure_text_part(part_id, type);
|
|
546
587
|
await this.recorder.append_assistant_delta(
|
|
547
588
|
this.message_id,
|
|
548
|
-
|
|
549
|
-
|
|
589
|
+
part_id,
|
|
590
|
+
type,
|
|
550
591
|
chunk.delta,
|
|
551
592
|
);
|
|
552
593
|
return;
|
|
594
|
+
}
|
|
553
595
|
case "text-end":
|
|
554
596
|
case "reasoning-end": {
|
|
555
597
|
const part_id = `${chunk.type === "text-end" ? "text" : "reasoning"}:${chunk.id}`;
|
|
556
598
|
const part = current.parts.find((item) => item.part_id === part_id);
|
|
557
599
|
if (part?.type === "text" || part?.type === "reasoning") {
|
|
558
600
|
await this.upsert_part({ ...part, state: "done" });
|
|
601
|
+
} else {
|
|
602
|
+
this.pending_text_parts.delete(part_id);
|
|
559
603
|
}
|
|
560
604
|
return;
|
|
561
605
|
}
|
|
@@ -578,7 +622,7 @@ export class SessionAssistantMessageWriter {
|
|
|
578
622
|
case "tool-input-available":
|
|
579
623
|
await this.upsert_tool(chunk.toolCallId, {
|
|
580
624
|
tool_name: chunk.toolName,
|
|
581
|
-
state: "
|
|
625
|
+
state: "ready",
|
|
582
626
|
input: to_json_value(chunk.input),
|
|
583
627
|
});
|
|
584
628
|
return;
|
|
@@ -624,6 +668,7 @@ export class SessionAssistantMessageWriter {
|
|
|
624
668
|
case "file":
|
|
625
669
|
await this.upsert_part({
|
|
626
670
|
part_id: `file:${generateId()}`,
|
|
671
|
+
sequence: this.next_part_sequence(),
|
|
627
672
|
type: "file",
|
|
628
673
|
media_type: chunk.mediaType,
|
|
629
674
|
url: chunk.url,
|
|
@@ -672,6 +717,41 @@ export class SessionAssistantMessageWriter {
|
|
|
672
717
|
);
|
|
673
718
|
}
|
|
674
719
|
|
|
720
|
+
private next_part_sequence(): number {
|
|
721
|
+
return this.current_message().parts.reduce(
|
|
722
|
+
(value, part) => Math.max(value, part.sequence + 1),
|
|
723
|
+
1,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** 在首个有效 Delta 到达时才固定文本 Part 的真实顺序。 */
|
|
728
|
+
private async ensure_text_part(
|
|
729
|
+
part_id: string,
|
|
730
|
+
type: "text" | "reasoning",
|
|
731
|
+
): Promise<void> {
|
|
732
|
+
const existing = this.current_message().parts.find(
|
|
733
|
+
(part) => part.part_id === part_id,
|
|
734
|
+
);
|
|
735
|
+
if (existing) {
|
|
736
|
+
if (existing.type !== type) {
|
|
737
|
+
throw new Error(`Assistant Part type changed: ${part_id}`);
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const pending_type = this.pending_text_parts.get(part_id);
|
|
742
|
+
if (pending_type && pending_type !== type) {
|
|
743
|
+
throw new Error(`Assistant pending Part type changed: ${part_id}`);
|
|
744
|
+
}
|
|
745
|
+
await this.upsert_part({
|
|
746
|
+
part_id,
|
|
747
|
+
sequence: this.next_part_sequence(),
|
|
748
|
+
type,
|
|
749
|
+
text: "",
|
|
750
|
+
state: "streaming",
|
|
751
|
+
});
|
|
752
|
+
this.pending_text_parts.delete(part_id);
|
|
753
|
+
}
|
|
754
|
+
|
|
675
755
|
private async upsert_tool(
|
|
676
756
|
tool_call_id: string,
|
|
677
757
|
changes: Pick<SessionAssistantToolPart, "tool_name" | "state"> &
|
|
@@ -681,6 +761,7 @@ export class SessionAssistantMessageWriter {
|
|
|
681
761
|
await this.upsert_part({
|
|
682
762
|
...(current || {}),
|
|
683
763
|
part_id: `tool:${tool_call_id}`,
|
|
764
|
+
sequence: current?.sequence || this.next_part_sequence(),
|
|
684
765
|
type: "tool",
|
|
685
766
|
tool_call_id,
|
|
686
767
|
...changes,
|
|
@@ -689,6 +770,7 @@ export class SessionAssistantMessageWriter {
|
|
|
689
770
|
|
|
690
771
|
private async close(status: "completed" | "stopped" | "failed"): Promise<void> {
|
|
691
772
|
if (this.closed) return;
|
|
773
|
+
this.pending_text_parts.clear();
|
|
692
774
|
await this.recorder.complete_assistant_message(this.message_id, status);
|
|
693
775
|
this.closed = true;
|
|
694
776
|
}
|
|
@@ -771,6 +853,18 @@ function require_message<TType extends SessionMessage["type"]>(
|
|
|
771
853
|
return message as Extract<SessionMessage, { type: TType }>;
|
|
772
854
|
}
|
|
773
855
|
|
|
856
|
+
function require_streaming_assistant(
|
|
857
|
+
message: SessionMessage,
|
|
858
|
+
): SessionAssistantMessage {
|
|
859
|
+
if (message.type !== "assistant") {
|
|
860
|
+
throw new Error(`Session Message is not assistant: ${message.message_id}`);
|
|
861
|
+
}
|
|
862
|
+
if (message.status !== "streaming") {
|
|
863
|
+
throw new Error(`Assistant Message is already closed: ${message.message_id}`);
|
|
864
|
+
}
|
|
865
|
+
return message;
|
|
866
|
+
}
|
|
867
|
+
|
|
774
868
|
function to_json_value(input: unknown): JsonValue {
|
|
775
869
|
if (input === undefined || input === null) return null;
|
|
776
870
|
if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") return input;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - Recorder messages.jsonl
|
|
5
|
+
* - Recorder 的 messages.jsonl 与 assistant_message.json 共同组成持久化事实源。
|
|
6
6
|
* - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
|
|
7
|
-
* -
|
|
7
|
+
* - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { nanoid } from "nanoid";
|
|
@@ -109,7 +109,7 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** Recorder
|
|
112
|
+
/** 从 Recorder 当前快照投影运行中的 Assistant。 */
|
|
113
113
|
async read_inflight(): Promise<SessionRecordV1 | null> {
|
|
114
114
|
const page = await this.recorder.list_messages({
|
|
115
115
|
limit: 500,
|
|
@@ -124,10 +124,10 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
124
124
|
return assistant ? to_executor_ui_message(assistant) : null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
127
|
+
/** Executor 草稿写入口不持久化;流式内容由 Recorder writer 负责。 */
|
|
128
128
|
async write_inflight(_message: SessionRecordV1): Promise<void> {}
|
|
129
129
|
|
|
130
|
-
/**
|
|
130
|
+
/** Executor 草稿收口入口不持久化;Assistant writer 负责 complete。 */
|
|
131
131
|
async finalize_inflight(_message?: SessionRecordV1 | null): Promise<void> {}
|
|
132
132
|
|
|
133
133
|
/** 返回 Context Composer 使用的 UIMessage 投影。 */
|