@downcity/agent 1.1.228 → 1.1.236
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 +6 -1
- package/bin/agent/local/Agent.d.ts.map +1 -1
- package/bin/agent/local/Agent.js +30 -3
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +23 -0
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +44 -0
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +2 -0
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +4 -0
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +2 -0
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -7
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/Executor.d.ts +43 -0
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +127 -7
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +10 -0
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -10
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -34
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
- package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts +15 -0
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +150 -35
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +7 -23
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -4
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/executor/types/SessionRun.d.ts +8 -0
- package/bin/executor/types/SessionRun.d.ts.map +1 -1
- package/bin/index.d.ts +3 -2
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +2 -2
- package/bin/index.js.map +1 -1
- package/bin/model/CityModelAdapter.d.ts +6 -0
- package/bin/model/CityModelAdapter.d.ts.map +1 -1
- package/bin/model/CityModelAdapter.js +13 -0
- package/bin/model/CityModelAdapter.js.map +1 -1
- package/bin/plugin/core/PluginLocalExecution.js +3 -3
- package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
- package/bin/plugin/core/PluginRegistry.d.ts +41 -2
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +145 -6
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/rpc/Client.d.ts +4 -0
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +11 -0
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +15 -1
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +83 -11
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
- package/bin/session/SessionSystemBuilder.js +3 -1
- package/bin/session/SessionSystemBuilder.js.map +1 -1
- package/bin/session/browse/Browse.d.ts +0 -8
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +72 -57
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
- package/bin/session/recorder/SessionRecorder.d.ts +20 -2
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +91 -46
- package/bin/session/recorder/SessionRecorder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +118 -88
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +21 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +65 -8
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +19 -1
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +70 -40
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +15 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +104 -7
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts +1 -5
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +4 -34
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/bin/session/storage/Paths.d.ts +3 -15
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +5 -21
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +8 -0
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +2 -24
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +29 -0
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +59 -2
- package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
- package/bin/types/plugin/PluginState.d.ts +10 -0
- package/bin/types/plugin/PluginState.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +10 -0
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js +29 -6
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/session/SessionLocalState.d.ts +8 -0
- package/bin/types/session/SessionLocalState.d.ts.map +1 -1
- package/bin/types/session/SessionMessage.d.ts +16 -12
- package/bin/types/session/SessionMessage.d.ts.map +1 -1
- package/bin/types/session/SessionMutation.d.ts +1 -1
- package/bin/types/session/SessionMutation.js +1 -1
- package/bin/types/session/SessionOptions.d.ts +16 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/SessionQueueCommand.d.ts +68 -0
- package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
- package/bin/types/session/SessionQueueCommand.js +10 -0
- package/bin/types/session/SessionQueueCommand.js.map +1 -0
- package/bin/types/session/SessionSegment.d.ts +57 -0
- package/bin/types/session/SessionSegment.d.ts.map +1 -0
- package/bin/types/session/SessionSegment.js +8 -0
- package/bin/types/session/SessionSegment.js.map +1 -0
- package/bin/utils/logger/Logger.d.ts +7 -9
- package/bin/utils/logger/Logger.d.ts.map +1 -1
- package/bin/utils/logger/Logger.js +11 -9
- package/bin/utils/logger/Logger.js.map +1 -1
- package/package.json +3 -3
- package/scripts/city-model-tool-loop.test.mjs +32 -0
- package/scripts/core-engine-context-compaction.test.mjs +411 -0
- package/scripts/logger-isolation.test.mjs +55 -0
- package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
- package/scripts/plugin-tool-bridge.test.mjs +52 -0
- package/scripts/remote-session-reconnect.test.mjs +18 -0
- package/scripts/session-action-message.test.mjs +59 -13
- package/scripts/session-compaction-context-window.test.mjs +96 -0
- package/scripts/session-config-turn-boundary.test.mjs +278 -0
- package/scripts/session-list-title.test.mjs +21 -3
- package/scripts/session-prompt-runtime.test.mjs +93 -0
- package/scripts/session-recorder.test.mjs +335 -15
- package/src/agent/local/Agent.ts +34 -3
- package/src/agent/local/services/AgentSessions.ts +61 -0
- package/src/agent/remote/RemoteSession.ts +5 -0
- package/src/agent/remote/RemoteTransport.ts +2 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -5
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
- package/src/executor/Executor.ts +161 -7
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +12 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -55
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
- package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
- package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
- package/src/executor/core-engine/CoreEngineRunner.ts +211 -43
- package/src/executor/store/history/SessionHistoryStore.ts +7 -26
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -4
- package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
- package/src/executor/types/SessionRun.ts +9 -0
- package/src/index.ts +9 -1
- package/src/model/CityModelAdapter.ts +15 -0
- package/src/plugin/core/PluginLocalExecution.ts +3 -3
- package/src/plugin/core/PluginRegistry.ts +187 -6
- package/src/rpc/Client.ts +12 -0
- package/src/session/Session.ts +97 -11
- package/src/session/SessionSystemBuilder.ts +4 -1
- package/src/session/browse/Browse.ts +73 -69
- package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
- package/src/session/recorder/SessionRecorder.ts +116 -49
- package/src/session/recorder/SessionRecorderCompaction.ts +131 -108
- package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
- package/src/session/runtime/SessionPromptRuntime.ts +86 -10
- package/src/session/services/SessionStateService.ts +89 -45
- package/src/session/services/SessionTurnService.ts +112 -7
- package/src/session/services/SessionViewService.ts +4 -50
- package/src/session/storage/Paths.ts +5 -38
- package/src/types/agent/SessionActor.ts +9 -0
- package/src/types/agent/SessionTypes.ts +2 -24
- package/src/types/executor/SessionRunContext.ts +34 -0
- package/src/types/plugin/PluginRuntime.ts +63 -2
- package/src/types/plugin/PluginState.ts +10 -0
- package/src/types/rpc/RpcProtocol.ts +11 -0
- package/src/types/runtime/agent/AgentContext.ts +32 -7
- package/src/types/session/SessionLocalState.ts +9 -0
- package/src/types/session/SessionMessage.ts +16 -12
- package/src/types/session/SessionMutation.ts +1 -1
- package/src/types/session/SessionOptions.ts +21 -0
- package/src/types/session/SessionQueueCommand.ts +78 -0
- package/src/types/session/SessionSegment.ts +62 -0
- package/src/utils/logger/Logger.ts +10 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,6 +11,7 @@ import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
|
11
11
|
import type {
|
|
12
12
|
ListSessionMessagesInput,
|
|
13
13
|
SessionActionMessage,
|
|
14
|
+
SessionAssistantFilePart,
|
|
14
15
|
SessionAssistantMessage,
|
|
15
16
|
SessionAssistantMessagePart,
|
|
16
17
|
SessionAssistantToolPart,
|
|
@@ -24,6 +25,11 @@ import type {
|
|
|
24
25
|
SessionMutation,
|
|
25
26
|
SessionMessageMutation as SessionMessageSnapshotMutation,
|
|
26
27
|
} from "@/types/session/SessionMutation.js";
|
|
28
|
+
import type {
|
|
29
|
+
SessionContextSnapshot,
|
|
30
|
+
SessionMessageStorageStats,
|
|
31
|
+
SessionSegmentSummary,
|
|
32
|
+
} from "@/types/session/SessionSegment.js";
|
|
27
33
|
|
|
28
34
|
/** SessionRecorder 构造参数。 */
|
|
29
35
|
export interface SessionRecorderOptions {
|
|
@@ -289,38 +295,88 @@ export class SessionRecorder {
|
|
|
289
295
|
}))) as SessionErrorMessage;
|
|
290
296
|
}
|
|
291
297
|
|
|
292
|
-
/**
|
|
298
|
+
/** 读取 Active 或指定边界之前最近的完整 Segment。 */
|
|
293
299
|
async list_messages(
|
|
294
300
|
input?: ListSessionMessagesInput,
|
|
295
301
|
): Promise<SessionMessagePage> {
|
|
296
302
|
await this.ensure_initialized();
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
303
|
+
const requests_segment = input?.before_sequence !== undefined;
|
|
304
|
+
const before_sequence = input?.before_sequence;
|
|
305
|
+
if (
|
|
306
|
+
requests_segment &&
|
|
307
|
+
(!Number.isInteger(before_sequence) || Number(before_sequence) <= 0)
|
|
308
|
+
) {
|
|
309
|
+
throw new Error("before_sequence must be a positive integer");
|
|
310
|
+
}
|
|
311
|
+
const segment = requests_segment
|
|
312
|
+
? await this.store.read_segment_before(Number(before_sequence))
|
|
313
|
+
: null;
|
|
314
|
+
const source = requests_segment ? "segment" as const : "active" as const;
|
|
315
|
+
const messages = requests_segment
|
|
316
|
+
? segment?.messages || []
|
|
317
|
+
: [...this.messages_by_id.values()].sort(compare_message_sequence);
|
|
318
|
+
const start_sequence = messages[0]?.sequence;
|
|
319
|
+
const end_sequence = messages.at(-1)?.sequence;
|
|
320
|
+
const history_boundary = source === "segment"
|
|
321
|
+
? segment?.range.start_sequence
|
|
322
|
+
: await this.store.active_before_sequence(messages);
|
|
323
|
+
const has_more = history_boundary !== undefined &&
|
|
324
|
+
await this.store.has_segment_before(history_boundary);
|
|
325
|
+
const stats = await this.store.stats();
|
|
326
|
+
const items = messages
|
|
327
|
+
.filter((message) => input?.include_internal === true || message.visibility === "visible")
|
|
328
|
+
.map((message) => structuredClone(message));
|
|
316
329
|
return {
|
|
317
330
|
items,
|
|
318
|
-
total:
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
total: stats.message_count,
|
|
332
|
+
source,
|
|
333
|
+
...(start_sequence !== undefined ? { start_sequence } : {}),
|
|
334
|
+
...(end_sequence !== undefined ? { end_sequence } : {}),
|
|
335
|
+
...(has_more && history_boundary !== undefined
|
|
336
|
+
? { next_before_sequence: history_boundary }
|
|
337
|
+
: {}),
|
|
338
|
+
has_more,
|
|
321
339
|
};
|
|
322
340
|
}
|
|
323
341
|
|
|
342
|
+
/** 读取最新累计 Summary 与全部 Active Message,供模型上下文使用。 */
|
|
343
|
+
async context_snapshot(): Promise<SessionContextSnapshot> {
|
|
344
|
+
await this.ensure_initialized();
|
|
345
|
+
return {
|
|
346
|
+
summary: await this.store.read_latest_summary(),
|
|
347
|
+
messages: [...this.messages_by_id.values()]
|
|
348
|
+
.sort(compare_message_sequence)
|
|
349
|
+
.map((message) => structuredClone(message)),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 读取全部真实历史,供 Fork 等明确的全量复制操作使用。 */
|
|
354
|
+
async list_history_messages(): Promise<SessionMessage[]> {
|
|
355
|
+
await this.ensure_initialized();
|
|
356
|
+
return await this.store.list_history_messages();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 读取当前 Session 的存储统计。 */
|
|
360
|
+
async storage_stats(): Promise<SessionMessageStorageStats> {
|
|
361
|
+
await this.ensure_initialized();
|
|
362
|
+
return await this.store.stats();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** 把 Active 前缀和累计 Summary 提交为不可变 Segment。 */
|
|
366
|
+
async compact_active(input: {
|
|
367
|
+
/** 移入 Segment 的最后一条真实 Message sequence。 */
|
|
368
|
+
through_sequence: number;
|
|
369
|
+
/** 写入 Segment footer 的累计 Summary。 */
|
|
370
|
+
summary: SessionSegmentSummary;
|
|
371
|
+
}): Promise<void> {
|
|
372
|
+
await this.ensure_initialized();
|
|
373
|
+
const result = await this.store.compact_active(input);
|
|
374
|
+
this.messages_by_id.clear();
|
|
375
|
+
for (const message of result.active_messages) {
|
|
376
|
+
this.messages_by_id.set(message.message_id, structuredClone(message));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
324
380
|
/** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
|
|
325
381
|
async import_messages(messages: SessionMessage[]): Promise<void> {
|
|
326
382
|
const turn_ids = new Map<string, string>();
|
|
@@ -551,6 +607,7 @@ export class SessionRecorder {
|
|
|
551
607
|
private async ensure_initialized(): Promise<void> {
|
|
552
608
|
if (!this.initialized) await this.initialize();
|
|
553
609
|
}
|
|
610
|
+
|
|
554
611
|
}
|
|
555
612
|
|
|
556
613
|
/** 单个 Assistant segment 的流式 writer。 */
|
|
@@ -671,10 +728,7 @@ export class SessionAssistantMessageWriter {
|
|
|
671
728
|
return;
|
|
672
729
|
}
|
|
673
730
|
case "file":
|
|
674
|
-
await this.
|
|
675
|
-
part_id: `file:${generateId()}`,
|
|
676
|
-
sequence: this.next_part_sequence(),
|
|
677
|
-
type: "file",
|
|
731
|
+
await this.append_file_part({
|
|
678
732
|
media_type: chunk.mediaType,
|
|
679
733
|
url: chunk.url,
|
|
680
734
|
});
|
|
@@ -689,6 +743,36 @@ export class SessionAssistantMessageWriter {
|
|
|
689
743
|
await this.recorder.update_assistant_part(this.message_id, part);
|
|
690
744
|
}
|
|
691
745
|
|
|
746
|
+
/**
|
|
747
|
+
* 把最终结果中的文件补入当前 Assistant,并对流式已写入文件去重。
|
|
748
|
+
*/
|
|
749
|
+
async append_file_part(
|
|
750
|
+
input: Pick<SessionAssistantFilePart, "filename" | "media_type" | "url">,
|
|
751
|
+
): Promise<void> {
|
|
752
|
+
const filename = String(input.filename || "").trim();
|
|
753
|
+
const current = this.current_message();
|
|
754
|
+
const existing = current.parts.find(
|
|
755
|
+
(part) =>
|
|
756
|
+
part.type === "file" &&
|
|
757
|
+
part.url === input.url &&
|
|
758
|
+
part.media_type === input.media_type,
|
|
759
|
+
);
|
|
760
|
+
if (existing?.type === "file") {
|
|
761
|
+
if (filename && String(existing.filename || "").trim() !== filename) {
|
|
762
|
+
await this.upsert_part({ ...existing, filename });
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await this.upsert_part({
|
|
767
|
+
part_id: `file:${generateId()}`,
|
|
768
|
+
sequence: this.next_part_sequence(),
|
|
769
|
+
type: "file",
|
|
770
|
+
media_type: input.media_type,
|
|
771
|
+
url: input.url,
|
|
772
|
+
...(filename ? { filename } : {}),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
692
776
|
/** 当前实现逐 chunk 等待落盘,因此 flush 在返回时天然完成。 */
|
|
693
777
|
async flush(): Promise<void> {}
|
|
694
778
|
|
|
@@ -907,28 +991,6 @@ function to_json_value(input: unknown): JsonValue {
|
|
|
907
991
|
}
|
|
908
992
|
}
|
|
909
993
|
|
|
910
|
-
function normalize_limit(value: number | undefined, fallback: number, maximum: number): number {
|
|
911
|
-
if (!Number.isFinite(value)) return fallback;
|
|
912
|
-
return Math.min(maximum, Math.max(1, Math.floor(Number(value))));
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function encode_cursor(offset: number): string {
|
|
916
|
-
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function decode_cursor(cursor: string | undefined): number {
|
|
920
|
-
const value = String(cursor || "").trim();
|
|
921
|
-
if (!value) return 0;
|
|
922
|
-
try {
|
|
923
|
-
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as { offset?: unknown };
|
|
924
|
-
return typeof parsed.offset === "number" && parsed.offset >= 0
|
|
925
|
-
? Math.floor(parsed.offset)
|
|
926
|
-
: 0;
|
|
927
|
-
} catch {
|
|
928
|
-
throw new Error("Invalid Session Message cursor");
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
994
|
function resolve_import_id(map: Map<string, string>, source_id: string, prefix: string): string {
|
|
933
995
|
const existing = map.get(source_id);
|
|
934
996
|
if (existing) return existing;
|
|
@@ -936,3 +998,8 @@ function resolve_import_id(map: Map<string, string>, source_id: string, prefix:
|
|
|
936
998
|
map.set(source_id, created);
|
|
937
999
|
return created;
|
|
938
1000
|
}
|
|
1001
|
+
|
|
1002
|
+
/** 按真实 Message sequence 升序排序。 */
|
|
1003
|
+
function compare_message_sequence(left: SessionMessage, right: SessionMessage): number {
|
|
1004
|
+
return left.sequence - right.sequence;
|
|
1005
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionRecorder
|
|
2
|
+
* SessionRecorder Active/Segment 上下文压缩。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Compact 把 Active 前缀写入不可变 Segment,并在 footer 保存累计 Summary。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { generateText } from "ai";
|
|
@@ -13,44 +13,38 @@ import {
|
|
|
13
13
|
build_update_session_compaction_prompt,
|
|
14
14
|
SESSION_COMPACTION_SYSTEM_PROMPT,
|
|
15
15
|
} from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
|
|
16
|
+
import { fold_compacted_text } from "@executor/core-engine/CoreEngineContextCompaction.js";
|
|
16
17
|
import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
|
|
17
18
|
import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
|
|
18
|
-
import type {
|
|
19
|
+
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/** 单次摘要请求中 conversation 文本的最大字符数。 */
|
|
22
|
+
const SUMMARY_CHUNK_MAX_CHARS = 20_000;
|
|
23
|
+
|
|
24
|
+
/** 单条 canonical Message 投影到摘要输入后的最大字符数。 */
|
|
25
|
+
const SUMMARY_MESSAGE_MAX_CHARS = 12_000;
|
|
26
|
+
|
|
27
|
+
/** 上一轮累计 Summary 进入下一次 reduce 请求时的最大字符数。 */
|
|
28
|
+
const PREVIOUS_SUMMARY_MAX_CHARS = 12_000;
|
|
29
|
+
|
|
30
|
+
/** 摘要模型的最大输出 token,用于避免 Summary 自身无限增长。 */
|
|
31
|
+
const SUMMARY_MAX_OUTPUT_TOKENS = 4_000;
|
|
32
|
+
|
|
33
|
+
/** 必要时把 Active 前缀关闭为带累计 Summary 的 Segment。 */
|
|
21
34
|
export async function compact_session_recorder_messages(input: {
|
|
22
35
|
/** 当前 Session Recorder。 */
|
|
23
36
|
recorder: SessionRecorder;
|
|
24
37
|
/** 压缩策略和模型。 */
|
|
25
38
|
compact_input: SessionHistoryCompactInput;
|
|
26
39
|
}): Promise<{ compacted: boolean; reason?: string }> {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
const context_messages = project_current_context(page.items);
|
|
32
|
-
if (context_messages.length < 4) {
|
|
33
|
-
return { compacted: false, reason: "small_messages" };
|
|
34
|
-
}
|
|
35
|
-
const system_text = input.compact_input.system
|
|
36
|
-
.map((message) => String(message.content || ""))
|
|
37
|
-
.join("\n\n");
|
|
38
|
-
const estimated_tokens = Math.ceil(
|
|
39
|
-
`${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
|
|
40
|
+
const snapshot = await input.recorder.context_snapshot();
|
|
41
|
+
const context_messages = snapshot.messages.filter(
|
|
42
|
+
(message) => message.type === "user" || message.type === "assistant",
|
|
40
43
|
);
|
|
41
|
-
if (
|
|
42
|
-
return { compacted: false, reason: "
|
|
44
|
+
if (!input.compact_input.force) {
|
|
45
|
+
return { compacted: false, reason: "not_requested" };
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
|
|
46
|
-
const compact_count = Math.max(
|
|
47
|
-
2,
|
|
48
|
-
Math.min(context_messages.length - 1, Math.floor(context_messages.length * ratio)),
|
|
49
|
-
);
|
|
50
|
-
const older = context_messages.slice(0, compact_count);
|
|
51
|
-
const boundary = [...older]
|
|
52
|
-
.reverse()
|
|
53
|
-
.find((message) => message.type !== "assistant" || message.kind !== "summary");
|
|
47
|
+
const boundary = context_messages.at(-1);
|
|
54
48
|
if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
|
|
55
49
|
|
|
56
50
|
const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
|
|
@@ -61,104 +55,133 @@ export async function compact_session_recorder_messages(input: {
|
|
|
61
55
|
state: "running",
|
|
62
56
|
});
|
|
63
57
|
|
|
64
|
-
const
|
|
65
|
-
(message): message is SessionAssistantMessage =>
|
|
66
|
-
message.type === "assistant" && message.kind === "summary",
|
|
67
|
-
);
|
|
68
|
-
const conversation_text = older
|
|
69
|
-
.filter((message) => message !== previous_summary)
|
|
58
|
+
const conversation_messages = context_messages
|
|
70
59
|
.map((message) => message_to_compaction_text(message))
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
const chunks = build_summary_chunks(conversation_messages);
|
|
62
|
+
let summary = String(snapshot.summary?.text || "").trim();
|
|
63
|
+
let used_fallback = false;
|
|
64
|
+
for (const chunk of chunks) {
|
|
65
|
+
const prompt = summary
|
|
66
|
+
? build_update_session_compaction_prompt({
|
|
67
|
+
previous_summary: fold_compacted_text(
|
|
68
|
+
summary,
|
|
69
|
+
PREVIOUS_SUMMARY_MAX_CHARS,
|
|
70
|
+
),
|
|
71
|
+
new_conversation_text: chunk || "(none)",
|
|
72
|
+
})
|
|
73
|
+
: build_initial_session_compaction_prompt({
|
|
74
|
+
conversation_text: chunk || "(none)",
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const result = await generateText({
|
|
78
|
+
model: input.compact_input.model,
|
|
79
|
+
system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
|
|
80
|
+
prompt,
|
|
81
|
+
maxOutputTokens: SUMMARY_MAX_OUTPUT_TOKENS,
|
|
84
82
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
summary = "
|
|
83
|
+
const next_summary = String(result.text || "").trim();
|
|
84
|
+
if (!next_summary) throw new Error("Compaction model returned an empty Summary");
|
|
85
|
+
summary = next_summary;
|
|
86
|
+
} catch {
|
|
87
|
+
used_fallback = true;
|
|
88
|
+
summary = build_deterministic_summary(summary, chunk);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!summary) {
|
|
92
|
+
used_fallback = true;
|
|
93
|
+
summary = build_deterministic_summary("", "(no textual context)");
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
await input.recorder.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sequence: 1,
|
|
106
|
-
type: "text",
|
|
96
|
+
await input.recorder.compact_active({
|
|
97
|
+
through_sequence: boundary.sequence,
|
|
98
|
+
summary: {
|
|
99
|
+
record_type: "summary",
|
|
100
|
+
session_id: input.recorder.session_id,
|
|
101
|
+
summary_id: `summary:${input.recorder.session_id}:${generateId()}`,
|
|
102
|
+
through_sequence: boundary.sequence,
|
|
107
103
|
text: summary,
|
|
108
|
-
|
|
109
|
-
}
|
|
104
|
+
created_at: Date.now(),
|
|
105
|
+
},
|
|
110
106
|
});
|
|
111
107
|
await publish_action(input.compact_input.onAction, {
|
|
112
108
|
id: action_id,
|
|
113
109
|
session_id: input.recorder.session_id,
|
|
114
110
|
title: "Session messages compacted",
|
|
115
|
-
description:
|
|
111
|
+
description: used_fallback
|
|
112
|
+
? `Closed Active through Message ${boundary.message_id} with deterministic fallback Summary.`
|
|
113
|
+
: `Closed Active through Message ${boundary.message_id}.`,
|
|
116
114
|
state: "completed",
|
|
117
115
|
});
|
|
118
116
|
return { compacted: true };
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
function project_current_context(messages: SessionMessage[]): SessionMessage[] {
|
|
122
|
-
const latest_summary = [...messages].reverse().find(
|
|
123
|
-
(message): message is SessionAssistantMessage =>
|
|
124
|
-
message.type === "assistant" &&
|
|
125
|
-
message.kind === "summary" &&
|
|
126
|
-
message.visibility === "internal",
|
|
127
|
-
);
|
|
128
|
-
if (!latest_summary?.summary_through_message_id) {
|
|
129
|
-
return messages.filter(
|
|
130
|
-
(message) => message.type === "user" || message.type === "assistant",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
const boundary_index = messages.findIndex(
|
|
134
|
-
(message) => message.message_id === latest_summary.summary_through_message_id,
|
|
135
|
-
);
|
|
136
|
-
return [
|
|
137
|
-
latest_summary,
|
|
138
|
-
...messages.slice(boundary_index + 1).filter(
|
|
139
|
-
(message) =>
|
|
140
|
-
message !== latest_summary &&
|
|
141
|
-
(message.type === "user" ||
|
|
142
|
-
message.type === "assistant" && message.kind !== "summary"),
|
|
143
|
-
),
|
|
144
|
-
];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
119
|
function message_to_compaction_text(message: SessionMessage): string {
|
|
148
120
|
const projected = to_executor_ui_message(message);
|
|
149
121
|
if (!projected) return "";
|
|
150
|
-
const
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
122
|
+
const parts = projected.parts
|
|
123
|
+
.filter((part) => part.type !== "reasoning")
|
|
124
|
+
.map((part) => {
|
|
125
|
+
const serialized = safe_stringify(part);
|
|
126
|
+
return serialized.length <= SUMMARY_MESSAGE_MAX_CHARS
|
|
127
|
+
? part
|
|
128
|
+
: {
|
|
129
|
+
type: part.type,
|
|
130
|
+
compacted_preview: fold_compacted_text(
|
|
131
|
+
serialized,
|
|
132
|
+
SUMMARY_MESSAGE_MAX_CHARS,
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
return fold_compacted_text(safe_stringify({
|
|
137
|
+
role: projected.role,
|
|
138
|
+
parts,
|
|
139
|
+
}), SUMMARY_MESSAGE_MAX_CHARS);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 按 Message 边界把摘要输入拆为固定字符上限的 chunk。 */
|
|
143
|
+
function build_summary_chunks(messages: string[]): string[] {
|
|
144
|
+
const chunks: string[] = [];
|
|
145
|
+
let current = "";
|
|
146
|
+
for (const message of messages) {
|
|
147
|
+
const value = fold_compacted_text(message, SUMMARY_CHUNK_MAX_CHARS);
|
|
148
|
+
const candidate = current ? `${current}\n${value}` : value;
|
|
149
|
+
if (current && candidate.length > SUMMARY_CHUNK_MAX_CHARS) {
|
|
150
|
+
chunks.push(current);
|
|
151
|
+
current = value;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
current = candidate;
|
|
155
|
+
}
|
|
156
|
+
if (current) chunks.push(current);
|
|
157
|
+
return chunks;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
/** 摘要模型不可用时仍可完成归档的确定性 checkpoint。 */
|
|
161
|
+
function build_deterministic_summary(
|
|
162
|
+
previous_summary: string,
|
|
163
|
+
new_context: string,
|
|
164
|
+
): string {
|
|
165
|
+
const body = [
|
|
166
|
+
String(previous_summary || "").trim(),
|
|
167
|
+
String(new_context || "").trim(),
|
|
168
|
+
]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join("\n\n");
|
|
171
|
+
return [
|
|
172
|
+
"## Compacted Context",
|
|
173
|
+
"The summary model was unavailable. The following deterministic checkpoint preserves the beginning and latest context.",
|
|
174
|
+
"",
|
|
175
|
+
fold_compacted_text(body || "(none)", PREVIOUS_SUMMARY_MAX_CHARS),
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safe_stringify(value: unknown): string {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.stringify(value) || String(value || "");
|
|
182
|
+
} catch {
|
|
183
|
+
return String(value || "");
|
|
184
|
+
}
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
async function publish_action(
|
|
@@ -168,7 +191,7 @@ async function publish_action(
|
|
|
168
191
|
session_id: string;
|
|
169
192
|
title: string;
|
|
170
193
|
description?: string;
|
|
171
|
-
state: "running" | "completed";
|
|
194
|
+
state: "running" | "completed" | "failed";
|
|
172
195
|
},
|
|
173
196
|
): Promise<void> {
|
|
174
197
|
if (!callback) return;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - Recorder 的
|
|
5
|
+
* - Recorder 的 active.jsonl、segments 与 assistant_message.json 共同组成持久化事实源。
|
|
6
6
|
* - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
|
|
7
7
|
* - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
|
|
8
8
|
*/
|
|
@@ -111,11 +111,8 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
111
111
|
|
|
112
112
|
/** 从 Recorder 当前快照投影运行中的 Assistant。 */
|
|
113
113
|
async read_inflight(): Promise<SessionRecordV1 | null> {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
include_internal: true,
|
|
117
|
-
});
|
|
118
|
-
const assistant = [...page.items]
|
|
114
|
+
const snapshot = await this.recorder.context_snapshot();
|
|
115
|
+
const assistant = [...snapshot.messages]
|
|
119
116
|
.reverse()
|
|
120
117
|
.find(
|
|
121
118
|
(message) =>
|
|
@@ -132,15 +129,32 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
132
129
|
|
|
133
130
|
/** 返回 Context Composer 使用的 UIMessage 投影。 */
|
|
134
131
|
async list_records(): Promise<SessionRecordV1[]> {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
const snapshot = await this.recorder.context_snapshot();
|
|
133
|
+
const records: SessionRecordV1[] = [];
|
|
134
|
+
if (snapshot.summary) {
|
|
135
|
+
records.push({
|
|
136
|
+
id: snapshot.summary.summary_id,
|
|
137
|
+
role: "assistant",
|
|
138
|
+
metadata: {
|
|
139
|
+
v: 1,
|
|
140
|
+
ts: snapshot.summary.created_at,
|
|
141
|
+
sessionId: this.sessionId,
|
|
142
|
+
source: "compact",
|
|
143
|
+
kind: "summary",
|
|
144
|
+
extra: {
|
|
145
|
+
visibility: "internal",
|
|
146
|
+
summaryThroughSequence: snapshot.summary.through_sequence,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
parts: [{ type: "text", text: snapshot.summary.text }],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
for (const message of snapshot.messages) {
|
|
153
|
+
if (message.type !== "user" && message.type !== "assistant") continue;
|
|
141
154
|
const projected = to_executor_ui_message(message);
|
|
142
|
-
|
|
143
|
-
}
|
|
155
|
+
if (projected) records.push(projected);
|
|
156
|
+
}
|
|
157
|
+
return records;
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
/** 返回 Context Message 区间。 */
|
|
@@ -158,7 +172,7 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
158
172
|
return {};
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
/** 通过 Recorder
|
|
175
|
+
/** 通过 Recorder 把 Active 前缀关闭为带累计 Summary 的 Segment。 */
|
|
162
176
|
async compact(input: SessionHistoryCompactInput): Promise<{
|
|
163
177
|
compacted: boolean;
|
|
164
178
|
reason?: string;
|
|
@@ -237,40 +251,6 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
|
237
251
|
}
|
|
238
252
|
}
|
|
239
253
|
|
|
240
|
-
function project_context_messages(messages: import("@/types/session/SessionMessage.js").SessionMessage[]) {
|
|
241
|
-
const latest_summary = [...messages]
|
|
242
|
-
.reverse()
|
|
243
|
-
.find(
|
|
244
|
-
(message) =>
|
|
245
|
-
message.type === "assistant" &&
|
|
246
|
-
message.kind === "summary" &&
|
|
247
|
-
message.visibility === "internal",
|
|
248
|
-
);
|
|
249
|
-
if (
|
|
250
|
-
!latest_summary ||
|
|
251
|
-
latest_summary.type !== "assistant" ||
|
|
252
|
-
!latest_summary.summary_through_message_id
|
|
253
|
-
) {
|
|
254
|
-
return messages.filter(
|
|
255
|
-
(message) => message.type === "user" || message.type === "assistant",
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
const boundary_index = messages.findIndex(
|
|
259
|
-
(message) => message.message_id === latest_summary.summary_through_message_id,
|
|
260
|
-
);
|
|
261
|
-
return [
|
|
262
|
-
latest_summary,
|
|
263
|
-
...messages
|
|
264
|
-
.slice(boundary_index + 1)
|
|
265
|
-
.filter(
|
|
266
|
-
(message) =>
|
|
267
|
-
message !== latest_summary &&
|
|
268
|
-
message.type === "user" ||
|
|
269
|
-
(message.type === "assistant" && message.kind !== "summary"),
|
|
270
|
-
),
|
|
271
|
-
];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
254
|
function infer_action_type(message_id: string): string {
|
|
275
255
|
return String(message_id || "").split(":")[0] || "action";
|
|
276
256
|
}
|