@downcity/agent 1.1.226 → 1.1.232
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/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +0 -7
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/executor/Executor.d.ts +25 -0
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +36 -0
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +4 -0
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +15 -11
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts +11 -0
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +12 -8
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +3 -13
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -1
- 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/index.d.ts +2 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- 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/PluginRegistry.d.ts +22 -1
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +46 -4
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/session/Session.d.ts +11 -1
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +76 -11
- package/bin/session/Session.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 +30 -2
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +120 -49
- 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 +50 -62
- 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 +17 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +67 -8
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +15 -1
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +60 -39
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +5 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +17 -3
- 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/SessionTypes.d.ts +2 -24
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +21 -0
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +25 -0
- package/bin/types/plugin/PluginRuntime.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/SessionConfigMutation.d.ts +68 -0
- package/bin/types/session/SessionConfigMutation.d.ts.map +1 -0
- package/bin/types/session/SessionConfigMutation.js +10 -0
- package/bin/types/session/SessionConfigMutation.js.map +1 -0
- 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/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/package.json +3 -3
- package/scripts/city-model-tool-loop.test.mjs +32 -0
- package/scripts/session-action-message.test.mjs +59 -13
- package/scripts/session-compaction-context-window.test.mjs +95 -0
- package/scripts/session-config-turn-boundary.test.mjs +224 -0
- package/scripts/session-list-title.test.mjs +21 -3
- package/scripts/session-prompt-runtime.test.mjs +85 -0
- package/scripts/session-recorder.test.mjs +379 -11
- package/src/agent/local/Agent.ts +34 -3
- package/src/agent/local/services/AgentSessions.ts +61 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +0 -5
- package/src/executor/Executor.ts +62 -0
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +16 -14
- package/src/executor/core-engine/CoreEngineRunner.ts +24 -8
- package/src/executor/store/history/SessionHistoryStore.ts +3 -14
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -1
- package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
- package/src/index.ts +8 -0
- package/src/model/CityModelAdapter.ts +15 -0
- package/src/plugin/core/PluginRegistry.ts +72 -4
- package/src/session/Session.ts +89 -11
- package/src/session/browse/Browse.ts +73 -69
- package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
- package/src/session/recorder/SessionRecorder.ts +151 -52
- package/src/session/recorder/SessionRecorderCompaction.ts +55 -80
- package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
- package/src/session/runtime/SessionPromptRuntime.ts +100 -9
- package/src/session/services/SessionStateService.ts +79 -44
- package/src/session/services/SessionTurnService.ts +19 -3
- package/src/session/services/SessionViewService.ts +4 -50
- package/src/session/storage/Paths.ts +5 -38
- package/src/types/agent/SessionTypes.ts +2 -24
- package/src/types/executor/SessionRunContext.ts +25 -0
- package/src/types/plugin/PluginRuntime.ts +27 -0
- package/src/types/runtime/agent/AgentContext.ts +32 -7
- package/src/types/session/SessionConfigMutation.ts +78 -0
- 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/SessionSegment.ts +62 -0
- 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,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
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)),
|
|
321
350
|
};
|
|
322
351
|
}
|
|
323
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。 */
|
|
@@ -558,6 +615,7 @@ export class SessionAssistantMessageWriter {
|
|
|
558
615
|
readonly message_id: string;
|
|
559
616
|
private readonly recorder: SessionRecorder;
|
|
560
617
|
private readonly pending_text_parts = new Map<string, "text" | "reasoning">();
|
|
618
|
+
private readonly active_text_part_ids = new Map<string, string>();
|
|
561
619
|
private closed = false;
|
|
562
620
|
|
|
563
621
|
constructor(recorder: SessionRecorder, message_id: string) {
|
|
@@ -573,7 +631,7 @@ export class SessionAssistantMessageWriter {
|
|
|
573
631
|
case "text-start":
|
|
574
632
|
case "reasoning-start": {
|
|
575
633
|
const type = chunk.type === "text-start" ? "text" : "reasoning";
|
|
576
|
-
const part_id =
|
|
634
|
+
const part_id = this.resolve_text_part_id(type, chunk.id);
|
|
577
635
|
if (!current.parts.some((part) => part.part_id === part_id)) {
|
|
578
636
|
this.pending_text_parts.set(part_id, type);
|
|
579
637
|
}
|
|
@@ -582,7 +640,7 @@ export class SessionAssistantMessageWriter {
|
|
|
582
640
|
case "text-delta":
|
|
583
641
|
case "reasoning-delta": {
|
|
584
642
|
const type = chunk.type === "text-delta" ? "text" : "reasoning";
|
|
585
|
-
const part_id =
|
|
643
|
+
const part_id = this.resolve_text_part_id(type, chunk.id);
|
|
586
644
|
await this.ensure_text_part(part_id, type);
|
|
587
645
|
await this.recorder.append_assistant_delta(
|
|
588
646
|
this.message_id,
|
|
@@ -594,13 +652,17 @@ export class SessionAssistantMessageWriter {
|
|
|
594
652
|
}
|
|
595
653
|
case "text-end":
|
|
596
654
|
case "reasoning-end": {
|
|
597
|
-
const
|
|
655
|
+
const type = chunk.type === "text-end" ? "text" : "reasoning";
|
|
656
|
+
const source_part_id = this.source_text_part_id(type, chunk.id);
|
|
657
|
+
const part_id = this.active_text_part_ids.get(source_part_id);
|
|
658
|
+
if (!part_id) return;
|
|
598
659
|
const part = current.parts.find((item) => item.part_id === part_id);
|
|
599
660
|
if (part?.type === "text" || part?.type === "reasoning") {
|
|
600
661
|
await this.upsert_part({ ...part, state: "done" });
|
|
601
662
|
} else {
|
|
602
663
|
this.pending_text_parts.delete(part_id);
|
|
603
664
|
}
|
|
665
|
+
this.active_text_part_ids.delete(source_part_id);
|
|
604
666
|
return;
|
|
605
667
|
}
|
|
606
668
|
case "tool-input-start":
|
|
@@ -666,10 +728,7 @@ export class SessionAssistantMessageWriter {
|
|
|
666
728
|
return;
|
|
667
729
|
}
|
|
668
730
|
case "file":
|
|
669
|
-
await this.
|
|
670
|
-
part_id: `file:${generateId()}`,
|
|
671
|
-
sequence: this.next_part_sequence(),
|
|
672
|
-
type: "file",
|
|
731
|
+
await this.append_file_part({
|
|
673
732
|
media_type: chunk.mediaType,
|
|
674
733
|
url: chunk.url,
|
|
675
734
|
});
|
|
@@ -684,6 +743,36 @@ export class SessionAssistantMessageWriter {
|
|
|
684
743
|
await this.recorder.update_assistant_part(this.message_id, part);
|
|
685
744
|
}
|
|
686
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
|
+
|
|
687
776
|
/** 当前实现逐 chunk 等待落盘,因此 flush 在返回时天然完成。 */
|
|
688
777
|
async flush(): Promise<void> {}
|
|
689
778
|
|
|
@@ -724,6 +813,32 @@ export class SessionAssistantMessageWriter {
|
|
|
724
813
|
);
|
|
725
814
|
}
|
|
726
815
|
|
|
816
|
+
/**
|
|
817
|
+
* 把 AI SDK 当前 stream 内的临时 chunk ID 映射为 Message 内唯一 Part ID。
|
|
818
|
+
*
|
|
819
|
+
* AI SDK 会在不同 `streamText()` 调用中重复使用 `txt-0`、`reasoning-0`
|
|
820
|
+
* 等 ID,因此这些 ID 只能用于关联当前尚未结束的文本片段。
|
|
821
|
+
*/
|
|
822
|
+
private resolve_text_part_id(
|
|
823
|
+
type: "text" | "reasoning",
|
|
824
|
+
chunk_id: string,
|
|
825
|
+
): string {
|
|
826
|
+
const source_part_id = this.source_text_part_id(type, chunk_id);
|
|
827
|
+
const active_part_id = this.active_text_part_ids.get(source_part_id);
|
|
828
|
+
if (active_part_id) return active_part_id;
|
|
829
|
+
const part_id = `${type}:${generateId()}`;
|
|
830
|
+
this.active_text_part_ids.set(source_part_id, part_id);
|
|
831
|
+
return part_id;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** 构造当前流片段使用的临时关联键。 */
|
|
835
|
+
private source_text_part_id(
|
|
836
|
+
type: "text" | "reasoning",
|
|
837
|
+
chunk_id: string,
|
|
838
|
+
): string {
|
|
839
|
+
return `${type}:${chunk_id}`;
|
|
840
|
+
}
|
|
841
|
+
|
|
727
842
|
/** 在首个有效 Delta 到达时才固定文本 Part 的真实顺序。 */
|
|
728
843
|
private async ensure_text_part(
|
|
729
844
|
part_id: string,
|
|
@@ -771,6 +886,7 @@ export class SessionAssistantMessageWriter {
|
|
|
771
886
|
private async close(status: "completed" | "stopped" | "failed"): Promise<void> {
|
|
772
887
|
if (this.closed) return;
|
|
773
888
|
this.pending_text_parts.clear();
|
|
889
|
+
this.active_text_part_ids.clear();
|
|
774
890
|
await this.recorder.complete_assistant_message(this.message_id, status);
|
|
775
891
|
this.closed = true;
|
|
776
892
|
}
|
|
@@ -875,28 +991,6 @@ function to_json_value(input: unknown): JsonValue {
|
|
|
875
991
|
}
|
|
876
992
|
}
|
|
877
993
|
|
|
878
|
-
function normalize_limit(value: number | undefined, fallback: number, maximum: number): number {
|
|
879
|
-
if (!Number.isFinite(value)) return fallback;
|
|
880
|
-
return Math.min(maximum, Math.max(1, Math.floor(Number(value))));
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function encode_cursor(offset: number): string {
|
|
884
|
-
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function decode_cursor(cursor: string | undefined): number {
|
|
888
|
-
const value = String(cursor || "").trim();
|
|
889
|
-
if (!value) return 0;
|
|
890
|
-
try {
|
|
891
|
-
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as { offset?: unknown };
|
|
892
|
-
return typeof parsed.offset === "number" && parsed.offset >= 0
|
|
893
|
-
? Math.floor(parsed.offset)
|
|
894
|
-
: 0;
|
|
895
|
-
} catch {
|
|
896
|
-
throw new Error("Invalid Session Message cursor");
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
994
|
function resolve_import_id(map: Map<string, string>, source_id: string, prefix: string): string {
|
|
901
995
|
const existing = map.get(source_id);
|
|
902
996
|
if (existing) return existing;
|
|
@@ -904,3 +998,8 @@ function resolve_import_id(map: Map<string, string>, source_id: string, prefix:
|
|
|
904
998
|
map.set(source_id, created);
|
|
905
999
|
return created;
|
|
906
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";
|
|
@@ -15,20 +15,19 @@ import {
|
|
|
15
15
|
} from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
|
|
16
16
|
import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
|
|
17
17
|
import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
|
|
18
|
-
import type {
|
|
18
|
+
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/** 必要时把 Active 前缀关闭为带累计 Summary 的 Segment。 */
|
|
21
21
|
export async function compact_session_recorder_messages(input: {
|
|
22
22
|
/** 当前 Session Recorder。 */
|
|
23
23
|
recorder: SessionRecorder;
|
|
24
24
|
/** 压缩策略和模型。 */
|
|
25
25
|
compact_input: SessionHistoryCompactInput;
|
|
26
26
|
}): Promise<{ compacted: boolean; reason?: string }> {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const context_messages = project_current_context(page.items);
|
|
27
|
+
const snapshot = await input.recorder.context_snapshot();
|
|
28
|
+
const context_messages = snapshot.messages.filter(
|
|
29
|
+
(message) => message.type === "user" || message.type === "assistant",
|
|
30
|
+
);
|
|
32
31
|
if (context_messages.length < 4) {
|
|
33
32
|
return { compacted: false, reason: "small_messages" };
|
|
34
33
|
}
|
|
@@ -36,21 +35,24 @@ export async function compact_session_recorder_messages(input: {
|
|
|
36
35
|
.map((message) => String(message.content || ""))
|
|
37
36
|
.join("\n\n");
|
|
38
37
|
const estimated_tokens = Math.ceil(
|
|
39
|
-
`${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
|
|
38
|
+
`${system_text}\n${snapshot.summary?.text || ""}\n${JSON.stringify(context_messages)}`.length / 3,
|
|
40
39
|
);
|
|
41
40
|
if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
|
|
42
41
|
return { compacted: false, reason: "under_budget" };
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const keep_last_messages = Math.max(1, input.compact_input.keepLastMessages);
|
|
46
|
+
const max_compact_count = context_messages.length - keep_last_messages;
|
|
47
|
+
if (max_compact_count < 2) {
|
|
48
|
+
return { compacted: false, reason: "keep_last_messages" };
|
|
49
|
+
}
|
|
50
|
+
const compact_count = Math.min(
|
|
51
|
+
max_compact_count,
|
|
52
|
+
Math.max(2, Math.floor(context_messages.length * ratio)),
|
|
49
53
|
);
|
|
50
54
|
const older = context_messages.slice(0, compact_count);
|
|
51
|
-
const boundary =
|
|
52
|
-
.reverse()
|
|
53
|
-
.find((message) => message.type !== "assistant" || message.kind !== "summary");
|
|
55
|
+
const boundary = older.at(-1);
|
|
54
56
|
if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
|
|
55
57
|
|
|
56
58
|
const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
|
|
@@ -61,19 +63,11 @@ export async function compact_session_recorder_messages(input: {
|
|
|
61
63
|
state: "running",
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
const previous_summary = older.find(
|
|
65
|
-
(message): message is SessionAssistantMessage =>
|
|
66
|
-
message.type === "assistant" && message.kind === "summary",
|
|
67
|
-
);
|
|
68
66
|
const conversation_text = older
|
|
69
|
-
.filter((message) => message !== previous_summary)
|
|
70
67
|
.map((message) => message_to_compaction_text(message))
|
|
71
68
|
.filter(Boolean)
|
|
72
|
-
.join("\n")
|
|
73
|
-
|
|
74
|
-
const previous_summary_text = previous_summary
|
|
75
|
-
? assistant_text(previous_summary)
|
|
76
|
-
: "";
|
|
69
|
+
.join("\n");
|
|
70
|
+
const previous_summary_text = String(snapshot.summary?.text || "").trim();
|
|
77
71
|
const prompt = previous_summary_text
|
|
78
72
|
? build_update_session_compaction_prompt({
|
|
79
73
|
previous_summary: previous_summary_text,
|
|
@@ -83,7 +77,7 @@ export async function compact_session_recorder_messages(input: {
|
|
|
83
77
|
conversation_text: conversation_text || "(none)",
|
|
84
78
|
});
|
|
85
79
|
|
|
86
|
-
let summary
|
|
80
|
+
let summary: string;
|
|
87
81
|
try {
|
|
88
82
|
const result = await generateText({
|
|
89
83
|
model: input.compact_input.model,
|
|
@@ -91,74 +85,55 @@ export async function compact_session_recorder_messages(input: {
|
|
|
91
85
|
prompt,
|
|
92
86
|
});
|
|
93
87
|
summary = String(result.text || "").trim();
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
88
|
+
} catch (error) {
|
|
89
|
+
await publish_action(input.compact_input.onAction, {
|
|
90
|
+
id: action_id,
|
|
91
|
+
session_id: input.recorder.session_id,
|
|
92
|
+
title: "Session message compaction failed",
|
|
93
|
+
description: error instanceof Error ? error.message : String(error),
|
|
94
|
+
state: "failed",
|
|
95
|
+
});
|
|
96
|
+
return { compacted: false, reason: "summary_failed" };
|
|
97
|
+
}
|
|
98
|
+
if (!summary) {
|
|
99
|
+
await publish_action(input.compact_input.onAction, {
|
|
100
|
+
id: action_id,
|
|
101
|
+
session_id: input.recorder.session_id,
|
|
102
|
+
title: "Session message compaction failed",
|
|
103
|
+
description: "The compaction model returned an empty Summary.",
|
|
104
|
+
state: "failed",
|
|
105
|
+
});
|
|
106
|
+
return { compacted: false, reason: "summary_failed" };
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
await input.recorder.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sequence: 1,
|
|
106
|
-
type: "text",
|
|
109
|
+
await input.recorder.compact_active({
|
|
110
|
+
through_sequence: boundary.sequence,
|
|
111
|
+
summary: {
|
|
112
|
+
record_type: "summary",
|
|
113
|
+
session_id: input.recorder.session_id,
|
|
114
|
+
summary_id: `summary:${input.recorder.session_id}:${generateId()}`,
|
|
115
|
+
through_sequence: boundary.sequence,
|
|
107
116
|
text: summary,
|
|
108
|
-
|
|
109
|
-
}
|
|
117
|
+
created_at: Date.now(),
|
|
118
|
+
},
|
|
110
119
|
});
|
|
111
120
|
await publish_action(input.compact_input.onAction, {
|
|
112
121
|
id: action_id,
|
|
113
122
|
session_id: input.recorder.session_id,
|
|
114
123
|
title: "Session messages compacted",
|
|
115
|
-
description: `
|
|
124
|
+
description: `Closed Active through Message ${boundary.message_id}.`,
|
|
116
125
|
state: "completed",
|
|
117
126
|
});
|
|
118
127
|
return { compacted: true };
|
|
119
128
|
}
|
|
120
129
|
|
|
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
130
|
function message_to_compaction_text(message: SessionMessage): string {
|
|
148
131
|
const projected = to_executor_ui_message(message);
|
|
149
132
|
if (!projected) return "";
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
return text ? `${message.type}: ${text}` : "";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function assistant_text(message: SessionAssistantMessage): string {
|
|
158
|
-
return message.parts
|
|
159
|
-
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
160
|
-
.join("\n")
|
|
161
|
-
.trim();
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
role: projected.role,
|
|
135
|
+
parts: projected.parts,
|
|
136
|
+
});
|
|
162
137
|
}
|
|
163
138
|
|
|
164
139
|
async function publish_action(
|
|
@@ -168,7 +143,7 @@ async function publish_action(
|
|
|
168
143
|
session_id: string;
|
|
169
144
|
title: string;
|
|
170
145
|
description?: string;
|
|
171
|
-
state: "running" | "completed";
|
|
146
|
+
state: "running" | "completed" | "failed";
|
|
172
147
|
},
|
|
173
148
|
): Promise<void> {
|
|
174
149
|
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
|
}
|