pi-sessions 0.5.1 → 0.7.0
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/README.md +22 -1
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +82 -134
- package/extensions/session-auto-title/command.ts +1 -1
- package/extensions/session-auto-title/controller.ts +3 -3
- package/extensions/session-auto-title/generate.ts +2 -2
- package/extensions/session-auto-title/model.ts +4 -12
- package/extensions/session-auto-title/retitle.ts +5 -5
- package/extensions/session-auto-title/state.ts +1 -1
- package/extensions/session-auto-title/wizard.ts +4 -4
- package/extensions/session-auto-title.ts +8 -8
- package/extensions/session-handoff/extract.ts +18 -5
- package/extensions/session-handoff/metadata.ts +4 -1
- package/extensions/session-handoff/picker.ts +52 -6
- package/extensions/session-handoff/query.ts +78 -62
- package/extensions/session-handoff/refs.ts +14 -20
- package/extensions/session-handoff/spawn.ts +1 -1
- package/extensions/session-handoff.ts +54 -35
- package/extensions/session-hooks.ts +3 -3
- package/extensions/session-index.ts +4 -4
- package/extensions/session-messaging/broker/process.ts +302 -0
- package/extensions/session-messaging/broker/spawn.ts +164 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +114 -0
- package/extensions/session-messaging/pi/message-contracts.ts +36 -0
- package/extensions/session-messaging/pi/message-view.ts +131 -0
- package/extensions/session-messaging/pi/renderer.ts +16 -0
- package/extensions/session-messaging/pi/service.ts +370 -0
- package/extensions/session-messaging/pi/tools.ts +92 -0
- package/extensions/session-messaging.ts +30 -0
- package/extensions/session-search/extract.ts +90 -440
- package/extensions/session-search/hooks.ts +20 -28
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +3 -2
- package/extensions/session-search.ts +161 -132
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/shared/session-broker/client.ts +253 -0
- package/extensions/shared/session-broker/framing.ts +72 -0
- package/extensions/shared/session-broker/protocol.ts +95 -0
- package/extensions/shared/session-broker/socket-path.ts +30 -0
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +46 -51
- package/extensions/shared/session-index/index.ts +6 -5
- package/extensions/shared/session-index/lineage.ts +19 -2
- package/extensions/shared/session-index/query/ast.ts +40 -0
- package/extensions/shared/session-index/query/compiler.ts +146 -0
- package/extensions/shared/session-index/query/lexer.ts +140 -0
- package/extensions/shared/session-index/query/parser.ts +178 -0
- package/extensions/shared/session-index/schema.ts +25 -9
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +555 -281
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +14 -23
- package/extensions/shared/settings.ts +62 -5
- package/extensions/shared/text.ts +50 -0
- package/package.json +4 -3
|
@@ -10,32 +10,32 @@ import {
|
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
13
|
-
import type { AssistantMessage, ToolCall
|
|
13
|
+
import type { AssistantMessage, ToolCall } from "@earendil-works/pi-ai";
|
|
14
14
|
import {
|
|
15
15
|
type CustomEntry,
|
|
16
16
|
parseSessionEntries,
|
|
17
17
|
type SessionEntry,
|
|
18
18
|
type SessionHeader,
|
|
19
|
-
type SessionMessageEntry,
|
|
20
19
|
} from "@earendil-works/pi-coding-agent";
|
|
21
20
|
import { Type } from "typebox";
|
|
22
21
|
import {
|
|
23
22
|
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
24
23
|
type HandoffSessionMetadata,
|
|
25
24
|
parseHandoffSessionMetadata,
|
|
26
|
-
} from "../session-handoff/metadata.
|
|
27
|
-
import type { SessionOrigin } from "../shared/session-index/index.
|
|
28
|
-
import {
|
|
25
|
+
} from "../session-handoff/metadata.ts";
|
|
26
|
+
import type { SessionOrigin } from "../shared/session-index/index.ts";
|
|
27
|
+
import { contentToText, isRecord } from "../shared/text.ts";
|
|
28
|
+
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
29
29
|
import {
|
|
30
30
|
deriveSessionRepoRoots,
|
|
31
31
|
type FileTouchOp,
|
|
32
32
|
type FileTouchSource,
|
|
33
33
|
normalizePathRecord,
|
|
34
34
|
type PathScope,
|
|
35
|
-
} from "./normalize.
|
|
35
|
+
} from "./normalize.ts";
|
|
36
36
|
|
|
37
37
|
export interface SearchTextChunk {
|
|
38
|
-
entryId
|
|
38
|
+
entryId: string;
|
|
39
39
|
entryType: string;
|
|
40
40
|
role?: string | undefined;
|
|
41
41
|
ts: string;
|
|
@@ -44,7 +44,7 @@ export interface SearchTextChunk {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface DurableHandoffMetadataRecord {
|
|
47
|
-
entryId
|
|
47
|
+
entryId: string;
|
|
48
48
|
ts: string;
|
|
49
49
|
metadata: HandoffSessionMetadata;
|
|
50
50
|
}
|
|
@@ -94,6 +94,8 @@ export interface SessionEntryScan {
|
|
|
94
94
|
maxEntryTs?: string | undefined;
|
|
95
95
|
firstUserPrompt?: string | undefined;
|
|
96
96
|
sessionName?: string | undefined;
|
|
97
|
+
sessionNameEntryId?: string | undefined;
|
|
98
|
+
sessionNameTs?: string | undefined;
|
|
97
99
|
handoffMetadata?: DurableHandoffMetadataRecord | undefined;
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -119,6 +121,7 @@ export interface ParsedSessionFile {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
const TOOL_RESULT_TEXT_LIMIT = 500;
|
|
124
|
+
const TOOL_CALL_TEXT_LIMIT = 2_000;
|
|
122
125
|
const BASH_OUTPUT_TEXT_LIMIT = 500;
|
|
123
126
|
const FILE_ANCHOR_BYTES = 64;
|
|
124
127
|
const NEWLINE_BYTE = 0x0a;
|
|
@@ -165,8 +168,14 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
|
|
|
165
168
|
const scan = scanSessionEntries(parsed.entries, fallbackTs, parsed.header.cwd);
|
|
166
169
|
|
|
167
170
|
const chunks: SearchTextChunk[] = [];
|
|
168
|
-
if (parsed.sessionName) {
|
|
169
|
-
chunks.push(
|
|
171
|
+
if (parsed.sessionName && scan.sessionNameEntryId) {
|
|
172
|
+
chunks.push(
|
|
173
|
+
createSessionNameChunk(
|
|
174
|
+
parsed.sessionName,
|
|
175
|
+
scan.sessionNameTs ?? fallbackTs,
|
|
176
|
+
scan.sessionNameEntryId,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
170
179
|
}
|
|
171
180
|
chunks.push(...scan.chunks);
|
|
172
181
|
appendDurableHandoffMetadataChunks(chunks, scan.handoffMetadata);
|
|
@@ -247,8 +256,13 @@ export function extractSessionTail(
|
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
export function createSessionNameChunk(
|
|
259
|
+
export function createSessionNameChunk(
|
|
260
|
+
sessionName: string,
|
|
261
|
+
ts: string,
|
|
262
|
+
entryId: string,
|
|
263
|
+
): SearchTextChunk {
|
|
251
264
|
return {
|
|
265
|
+
entryId,
|
|
252
266
|
entryType: "session_info",
|
|
253
267
|
ts,
|
|
254
268
|
sourceKind: "session_name",
|
|
@@ -267,6 +281,8 @@ export function scanSessionEntries(
|
|
|
267
281
|
let maxEntryTs: string | undefined;
|
|
268
282
|
let firstUserPrompt: string | undefined;
|
|
269
283
|
let sessionName: string | undefined;
|
|
284
|
+
let sessionNameEntryId: string | undefined;
|
|
285
|
+
let sessionNameTs: string | undefined;
|
|
270
286
|
let handoffMetadata: DurableHandoffMetadataRecord | undefined;
|
|
271
287
|
|
|
272
288
|
for (const entry of entries) {
|
|
@@ -290,6 +306,8 @@ export function scanSessionEntries(
|
|
|
290
306
|
case "session_info": {
|
|
291
307
|
if (typeof entry.name === "string") {
|
|
292
308
|
sessionName = entry.name.trim();
|
|
309
|
+
sessionNameEntryId = entry.id;
|
|
310
|
+
sessionNameTs = entryTs;
|
|
293
311
|
}
|
|
294
312
|
continue;
|
|
295
313
|
}
|
|
@@ -348,6 +366,8 @@ export function scanSessionEntries(
|
|
|
348
366
|
maxEntryTs,
|
|
349
367
|
firstUserPrompt,
|
|
350
368
|
sessionName,
|
|
369
|
+
sessionNameEntryId,
|
|
370
|
+
sessionNameTs,
|
|
351
371
|
handoffMetadata,
|
|
352
372
|
};
|
|
353
373
|
}
|
|
@@ -438,26 +458,6 @@ function normalizeParentSessionPath(parentSession: string | undefined): string |
|
|
|
438
458
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
439
459
|
}
|
|
440
460
|
|
|
441
|
-
function findDurableHandoffMetadata(
|
|
442
|
-
entries: SessionEntry[],
|
|
443
|
-
fallbackTs: string,
|
|
444
|
-
): DurableHandoffMetadataRecord | undefined {
|
|
445
|
-
let durableHandoffMetadata: DurableHandoffMetadataRecord | undefined;
|
|
446
|
-
|
|
447
|
-
for (const entry of entries) {
|
|
448
|
-
if (entry.type !== "custom") {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const parsed = extractHandoffMetadata(entry, getEntryTimestamp(entry, fallbackTs));
|
|
453
|
-
if (parsed && !durableHandoffMetadata) {
|
|
454
|
-
durableHandoffMetadata = parsed;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return durableHandoffMetadata;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
461
|
function readSessionIdFromPath(sessionPath: string): string | undefined {
|
|
462
462
|
try {
|
|
463
463
|
const parsed = parseSessionFile(sessionPath);
|
|
@@ -514,7 +514,7 @@ function appendDurableHandoffMetadataChunks(
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
function createMetadataChunk(
|
|
517
|
-
entryId: string
|
|
517
|
+
entryId: string,
|
|
518
518
|
ts: string,
|
|
519
519
|
sourceKind: string,
|
|
520
520
|
text: string,
|
|
@@ -634,6 +634,28 @@ function createFileTouch(
|
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
function getAssistantToolCalls(content: unknown): ToolCallBlock[] {
|
|
638
|
+
if (!Array.isArray(content)) {
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return content.filter(isToolCallBlock);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function isToolCallBlock(part: unknown): part is ToolCallBlock {
|
|
646
|
+
return (
|
|
647
|
+
isRecord(part) &&
|
|
648
|
+
part.type === "toolCall" &&
|
|
649
|
+
typeof part.id === "string" &&
|
|
650
|
+
typeof part.name === "string" &&
|
|
651
|
+
isRecord(part.arguments)
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function stringValue(value: unknown): string | undefined {
|
|
656
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
637
659
|
function getToolCallFileTouchOp(toolName: string): FileTouchOp | undefined {
|
|
638
660
|
switch (toolName) {
|
|
639
661
|
case "read":
|
|
@@ -650,182 +672,15 @@ function trimmedText(value: unknown): string {
|
|
|
650
672
|
return typeof value === "string" ? value.trim() : "";
|
|
651
673
|
}
|
|
652
674
|
|
|
653
|
-
function
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
for (const entry of entries) {
|
|
657
|
-
const parentId = entry.parentId ?? null;
|
|
658
|
-
const bucket = byParent.get(parentId);
|
|
659
|
-
if (bucket) {
|
|
660
|
-
bucket.push(entry);
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
byParent.set(parentId, [entry]);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
for (const childEntries of byParent.values()) {
|
|
668
|
-
childEntries.sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return byParent;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function buildSessionTreeHeader(parsed: ParsedSessionFile, sessionPath: string): string[] {
|
|
675
|
-
const durableHandoffMetadata = findDurableHandoffMetadata(
|
|
676
|
-
parsed.entries,
|
|
677
|
-
parsed.header.timestamp,
|
|
678
|
-
);
|
|
679
|
-
const lines = [
|
|
680
|
-
`# Session ${parsed.sessionName || parsed.header.id}`,
|
|
681
|
-
"",
|
|
682
|
-
`- session_id: ${parsed.header.id}`,
|
|
683
|
-
`- session_path: ${sessionPath}`,
|
|
684
|
-
`- cwd: ${parsed.header.cwd}`,
|
|
685
|
-
`- started_at: ${parsed.header.timestamp}`,
|
|
686
|
-
];
|
|
687
|
-
|
|
688
|
-
if (parsed.header.parentSession) {
|
|
689
|
-
lines.push(`- parent_session: ${parsed.header.parentSession}`);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (durableHandoffMetadata) {
|
|
693
|
-
lines.push(
|
|
694
|
-
`- session_origin: ${durableHandoffMetadata.metadata.origin}`,
|
|
695
|
-
`- handoff_goal: ${durableHandoffMetadata.metadata.goal}`,
|
|
696
|
-
`- handoff_next_task: ${durableHandoffMetadata.metadata.nextTask}`,
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
lines.push("", "## Session Tree", "");
|
|
701
|
-
return lines;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
export interface RenderedSessionTree {
|
|
705
|
-
markdown: string;
|
|
706
|
-
sessionId: string;
|
|
707
|
-
sessionName: string;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
export function renderSessionTreeMarkdown(
|
|
711
|
-
sessionPath: string,
|
|
712
|
-
options?: { maxChars?: number },
|
|
713
|
-
): RenderedSessionTree {
|
|
714
|
-
const parsed = parseSessionFile(sessionPath);
|
|
715
|
-
if (!parsed) {
|
|
716
|
-
throw new Error(`Unable to parse session file: ${sessionPath}`);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const byParent = groupEntriesByParent(parsed.entries);
|
|
720
|
-
const lines = buildSessionTreeHeader(parsed, sessionPath);
|
|
721
|
-
|
|
722
|
-
const roots = byParent.get(null) ?? [];
|
|
723
|
-
for (const root of roots) {
|
|
724
|
-
renderTreeSegment(root, byParent, lines);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const markdown = lines.join("\n");
|
|
728
|
-
const maxChars = options?.maxChars;
|
|
729
|
-
if (maxChars === undefined || markdown.length <= maxChars) {
|
|
730
|
-
return { markdown, sessionId: parsed.header.id, sessionName: parsed.sessionName };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
markdown: `${markdown.slice(0, maxChars)}\n\n[session tree truncated to ${maxChars} characters]`,
|
|
735
|
-
sessionId: parsed.header.id,
|
|
736
|
-
sessionName: parsed.sessionName,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function renderTreeSegment(
|
|
741
|
-
start: SessionEntry,
|
|
742
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
743
|
-
lines: string[],
|
|
744
|
-
depth = 0,
|
|
745
|
-
): void {
|
|
746
|
-
let current: SessionEntry | undefined = start;
|
|
747
|
-
|
|
748
|
-
while (current) {
|
|
749
|
-
appendEntryLines(lines, describeEntry(current, byParent), depth);
|
|
750
|
-
|
|
751
|
-
const children = getRenderableChildren(current, byParent);
|
|
752
|
-
if (children.length === 0) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (children.length === 1) {
|
|
757
|
-
current = children[0];
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
lines.push(`${" ".repeat(depth)} branches:`);
|
|
762
|
-
for (const child of children) {
|
|
763
|
-
renderTreeSegment(child, byParent, lines, depth + 1);
|
|
764
|
-
}
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function appendEntryLines(lines: string[], entryLines: string[], depth: number): void {
|
|
770
|
-
if (entryLines.length === 0) return;
|
|
771
|
-
|
|
772
|
-
const indent = " ".repeat(depth);
|
|
773
|
-
lines.push(`${indent}- ${entryLines[0]}`);
|
|
774
|
-
for (let i = 1; i < entryLines.length; i++) {
|
|
775
|
-
lines.push(`${indent} ${entryLines[i]}`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function describeEntry(
|
|
780
|
-
entry: SessionEntry,
|
|
781
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
782
|
-
): string[] {
|
|
783
|
-
switch (entry.type) {
|
|
784
|
-
case "message":
|
|
785
|
-
return describeMessageEntry(entry, byParent);
|
|
786
|
-
case "branch_summary":
|
|
787
|
-
return [describeLabeledText("Branch summary", entry.summary, 220)];
|
|
788
|
-
case "compaction":
|
|
789
|
-
return [describeLabeledText("Compaction summary", entry.summary, 220)];
|
|
790
|
-
case "session_info":
|
|
791
|
-
return [describeLabeledText("Session name", entry.name, 180)];
|
|
792
|
-
case "model_change":
|
|
793
|
-
return [`Model: ${entry.provider}/${entry.modelId}`];
|
|
794
|
-
case "thinking_level_change":
|
|
795
|
-
return [`Thinking: ${entry.thinkingLevel}`];
|
|
796
|
-
default:
|
|
797
|
-
return [entry.type];
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function describeMessageEntry(
|
|
802
|
-
entry: MessageEntry,
|
|
803
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
804
|
-
): string[] {
|
|
805
|
-
const { message } = entry;
|
|
806
|
-
|
|
807
|
-
switch (message.role) {
|
|
808
|
-
case "user":
|
|
809
|
-
return [`User: ${fullText(contentToText(message.content))}`];
|
|
810
|
-
case "assistant":
|
|
811
|
-
return describeAssistantMessage(entry, byParent, message);
|
|
812
|
-
case "toolResult":
|
|
813
|
-
return [];
|
|
814
|
-
case "bashExecution":
|
|
815
|
-
return [`Bash ${previewText(message.command, 120)}: ${previewText(message.output, 220)}`];
|
|
816
|
-
case "custom":
|
|
817
|
-
return [`Custom: ${fullText(contentToText(message.content))}`];
|
|
818
|
-
default:
|
|
819
|
-
return [describeFallbackMessage(message)];
|
|
675
|
+
function truncateText(text: string, limit: number): string {
|
|
676
|
+
if (text.length <= limit) {
|
|
677
|
+
return text;
|
|
820
678
|
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function describeLabeledText(label: string, value: unknown, limit: number): string {
|
|
824
|
-
return `${label}: ${previewText(trimmedText(value), limit)}`;
|
|
679
|
+
return `${text.slice(0, limit)}…`;
|
|
825
680
|
}
|
|
826
681
|
|
|
827
682
|
function extractMessageChunks(
|
|
828
|
-
entryId: string
|
|
683
|
+
entryId: string,
|
|
829
684
|
fallbackTs: string,
|
|
830
685
|
message: AgentMessage,
|
|
831
686
|
): SearchTextChunk[] {
|
|
@@ -842,13 +697,16 @@ function extractMessageChunks(
|
|
|
842
697
|
);
|
|
843
698
|
}
|
|
844
699
|
case "assistant": {
|
|
845
|
-
return
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
700
|
+
return [
|
|
701
|
+
...buildOptionalMessageChunk(
|
|
702
|
+
entryId,
|
|
703
|
+
"assistant",
|
|
704
|
+
ts,
|
|
705
|
+
"assistant_text",
|
|
706
|
+
contentToText(message.content),
|
|
707
|
+
),
|
|
708
|
+
...extractToolCallChunks(entryId, ts, message),
|
|
709
|
+
];
|
|
852
710
|
}
|
|
853
711
|
case "toolResult": {
|
|
854
712
|
return buildOptionalMessageChunk(
|
|
@@ -889,8 +747,27 @@ function extractMessageChunks(
|
|
|
889
747
|
}
|
|
890
748
|
}
|
|
891
749
|
|
|
750
|
+
function extractToolCallChunks(
|
|
751
|
+
entryId: string,
|
|
752
|
+
ts: string,
|
|
753
|
+
message: AssistantMessage,
|
|
754
|
+
): SearchTextChunk[] {
|
|
755
|
+
return getAssistantToolCalls(message.content).map((toolCall) =>
|
|
756
|
+
createMessageChunk(
|
|
757
|
+
entryId,
|
|
758
|
+
"assistant",
|
|
759
|
+
ts,
|
|
760
|
+
"tool_call",
|
|
761
|
+
truncateText(
|
|
762
|
+
`${toolCall.name} ${JSON.stringify(toolCall.arguments ?? {})}`,
|
|
763
|
+
TOOL_CALL_TEXT_LIMIT,
|
|
764
|
+
),
|
|
765
|
+
),
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
892
769
|
function createMessageChunk(
|
|
893
|
-
entryId: string
|
|
770
|
+
entryId: string,
|
|
894
771
|
role: string,
|
|
895
772
|
ts: string,
|
|
896
773
|
sourceKind: string,
|
|
@@ -907,7 +784,7 @@ function createMessageChunk(
|
|
|
907
784
|
}
|
|
908
785
|
|
|
909
786
|
function buildOptionalMessageChunk(
|
|
910
|
-
entryId: string
|
|
787
|
+
entryId: string,
|
|
911
788
|
role: string,
|
|
912
789
|
ts: string,
|
|
913
790
|
sourceKind: string,
|
|
@@ -920,232 +797,15 @@ function buildOptionalMessageChunk(
|
|
|
920
797
|
return [createMessageChunk(entryId, role, ts, sourceKind, text)];
|
|
921
798
|
}
|
|
922
799
|
|
|
923
|
-
function contentToText(content: unknown): string {
|
|
924
|
-
if (typeof content === "string") {
|
|
925
|
-
return content.trim();
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (!Array.isArray(content)) {
|
|
929
|
-
return "";
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
return content
|
|
933
|
-
.filter(isTextBlock)
|
|
934
|
-
.map((part) => part.text)
|
|
935
|
-
.join("\n")
|
|
936
|
-
.trim();
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function isTextBlock(part: unknown): part is TextBlock {
|
|
940
|
-
return isRecord(part) && part.type === "text" && typeof part.text === "string";
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
944
|
-
return typeof value === "object" && value !== null;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function describeAssistantMessage(
|
|
948
|
-
entry: MessageEntry,
|
|
949
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
950
|
-
assistantMessage: AssistantMessage,
|
|
951
|
-
): string[] {
|
|
952
|
-
const blocks: string[] = [];
|
|
953
|
-
const text = contentToText(assistantMessage.content);
|
|
954
|
-
if (text) {
|
|
955
|
-
blocks.push(`Assistant: ${fullText(text)}`);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const toolCalls = getAssistantToolCalls(assistantMessage.content);
|
|
959
|
-
const toolResults = getToolResults(entry, byParent);
|
|
960
|
-
const resultsByCallId = new Map<string, ToolResultEntry[]>();
|
|
961
|
-
for (const result of toolResults) {
|
|
962
|
-
const toolCallId = result.message.toolCallId;
|
|
963
|
-
if (!toolCallId) continue;
|
|
964
|
-
const existing = resultsByCallId.get(toolCallId) ?? [];
|
|
965
|
-
existing.push(result);
|
|
966
|
-
resultsByCallId.set(toolCallId, existing);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
for (const toolCall of toolCalls) {
|
|
970
|
-
const toolResultGroup = resultsByCallId.get(toolCall.id) ?? [];
|
|
971
|
-
blocks.push(...describeToolOperation(toolCall, toolResultGroup));
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
return blocks.length > 0 ? blocks : ["Assistant"];
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function getAssistantToolCalls(content: unknown): ToolCallBlock[] {
|
|
978
|
-
if (!Array.isArray(content)) {
|
|
979
|
-
return [];
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return content.filter(isToolCallBlock);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function isToolCallBlock(part: unknown): part is ToolCallBlock {
|
|
986
|
-
return (
|
|
987
|
-
isRecord(part) &&
|
|
988
|
-
part.type === "toolCall" &&
|
|
989
|
-
typeof part.id === "string" &&
|
|
990
|
-
typeof part.name === "string" &&
|
|
991
|
-
isRecord(part.arguments)
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function getChildEntries(
|
|
996
|
-
entryId: string | undefined,
|
|
997
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
998
|
-
): SessionEntry[] {
|
|
999
|
-
return byParent.get(entryId ?? null) ?? [];
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function getDescendantChildren(
|
|
1003
|
-
entries: SessionEntry[],
|
|
1004
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
1005
|
-
): SessionEntry[] {
|
|
1006
|
-
const descendants: SessionEntry[] = [];
|
|
1007
|
-
|
|
1008
|
-
for (const entry of entries) {
|
|
1009
|
-
descendants.push(...getChildEntries(entry.id, byParent));
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
return descendants;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function isToolResultEntry(entry: SessionEntry): entry is ToolResultEntry {
|
|
1016
|
-
return entry.type === "message" && entry.message.role === "toolResult";
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function getToolResults(
|
|
1020
|
-
entry: MessageEntry,
|
|
1021
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
1022
|
-
): ToolResultEntry[] {
|
|
1023
|
-
const results: ToolResultEntry[] = [];
|
|
1024
|
-
let currentChildren = getChildEntries(entry.id, byParent);
|
|
1025
|
-
|
|
1026
|
-
while (currentChildren.length > 0) {
|
|
1027
|
-
const toolResults = currentChildren.filter(isToolResultEntry);
|
|
1028
|
-
|
|
1029
|
-
if (toolResults.length === 0) {
|
|
1030
|
-
break;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
results.push(...toolResults);
|
|
1034
|
-
currentChildren = getDescendantChildren(toolResults, byParent);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
return results;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function getRenderableChildren(
|
|
1041
|
-
entry: SessionEntry,
|
|
1042
|
-
byParent: Map<string | null, SessionEntry[]>,
|
|
1043
|
-
): SessionEntry[] {
|
|
1044
|
-
let currentChildren = getChildEntries(entry.id, byParent);
|
|
1045
|
-
|
|
1046
|
-
while (currentChildren.length > 0) {
|
|
1047
|
-
const nonToolResultChildren = currentChildren.filter((child) => !isToolResultEntry(child));
|
|
1048
|
-
if (nonToolResultChildren.length > 0) {
|
|
1049
|
-
return nonToolResultChildren;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
currentChildren = getDescendantChildren(currentChildren, byParent);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
return [];
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function describeToolOperation(toolCall: ToolCallBlock, toolResults: ToolResultEntry[]): string[] {
|
|
1059
|
-
const args = toolCall.arguments ?? {};
|
|
1060
|
-
const resultLines = summarizeToolResults(toolCall.name, toolResults);
|
|
1061
|
-
|
|
1062
|
-
switch (toolCall.name) {
|
|
1063
|
-
case "read":
|
|
1064
|
-
return formatToolOperation(`Read ${stringArg(args.path, "(unknown path)")}`, resultLines);
|
|
1065
|
-
case "bash":
|
|
1066
|
-
return formatToolOperation(
|
|
1067
|
-
`Bash ${fullText(stringArg(args.command, "(no command)"))}`,
|
|
1068
|
-
resultLines,
|
|
1069
|
-
);
|
|
1070
|
-
case "search_web":
|
|
1071
|
-
return formatToolOperation(`Search web ${fullText(JSON.stringify(args))}`, resultLines);
|
|
1072
|
-
case "fetch_web":
|
|
1073
|
-
return formatToolOperation(`Fetch web ${fullText(JSON.stringify(args))}`, resultLines);
|
|
1074
|
-
case "edit":
|
|
1075
|
-
return formatToolOperation(`Edit ${stringArg(args.path, "(unknown path)")}`, resultLines);
|
|
1076
|
-
case "write":
|
|
1077
|
-
return formatToolOperation(`Write ${stringArg(args.path, "(unknown path)")}`, resultLines);
|
|
1078
|
-
default:
|
|
1079
|
-
return formatToolOperation(toolCall.name, resultLines);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function stringArg(value: unknown, fallback: string): string {
|
|
1084
|
-
return typeof value === "string" ? value : fallback;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function stringValue(value: unknown): string | undefined {
|
|
1088
|
-
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function summarizeToolResults(toolName: string, toolResults: ToolResultEntry[]): string[] {
|
|
1092
|
-
if (toolResults.length === 0) {
|
|
1093
|
-
return ["(pending result)"];
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const shouldTruncate = toolName !== "write";
|
|
1097
|
-
const parts = toolResults
|
|
1098
|
-
.map((toolResult) => {
|
|
1099
|
-
const text = contentToText(toolResult.message.content);
|
|
1100
|
-
return shouldTruncate ? truncateText(text, TOOL_RESULT_TEXT_LIMIT) : text;
|
|
1101
|
-
})
|
|
1102
|
-
.filter((text) => text.trim().length > 0);
|
|
1103
|
-
|
|
1104
|
-
if (parts.length === 0) {
|
|
1105
|
-
return ["(no text output)"];
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
return parts;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function formatToolOperation(label: string, resultLines: string[]): string[] {
|
|
1112
|
-
return [label, "```", ...resultLines, "```"];
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function truncateText(text: string, limit: number): string {
|
|
1116
|
-
if (text.length <= limit) return text;
|
|
1117
|
-
return `${text.slice(0, limit)}…`;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function fullText(text: string): string {
|
|
1121
|
-
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
1122
|
-
return cleaned || "(no text)";
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function previewText(text: string, limit: number): string {
|
|
1126
|
-
return truncateText(fullText(text), limit);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
type MessageEntry = SessionMessageEntry;
|
|
1130
|
-
|
|
1131
800
|
interface SummaryDetails {
|
|
1132
801
|
readFiles?: unknown;
|
|
1133
802
|
modifiedFiles?: unknown;
|
|
1134
803
|
}
|
|
1135
804
|
|
|
1136
|
-
interface TextBlock {
|
|
1137
|
-
type: "text";
|
|
1138
|
-
text: string;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
805
|
type ToolCallBlock = ToolCall & {
|
|
1142
806
|
arguments: Record<string, unknown>;
|
|
1143
807
|
};
|
|
1144
808
|
|
|
1145
|
-
type ToolResultEntry = MessageEntry & {
|
|
1146
|
-
message: ToolResultMessage;
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
809
|
function isSessionEntry(entry: SessionHeader | SessionEntry): entry is SessionEntry {
|
|
1150
810
|
return entry.type !== "session";
|
|
1151
811
|
}
|
|
@@ -1153,13 +813,3 @@ function isSessionEntry(entry: SessionHeader | SessionEntry): entry is SessionEn
|
|
|
1153
813
|
function getSummaryDetails(details: unknown): SummaryDetails | undefined {
|
|
1154
814
|
return safeParseTypeBoxValue(SUMMARY_DETAILS_SCHEMA, details);
|
|
1155
815
|
}
|
|
1156
|
-
|
|
1157
|
-
function describeFallbackMessage(message: AgentMessage): string {
|
|
1158
|
-
return hasMessageContent(message)
|
|
1159
|
-
? `${message.role}: ${fullText(contentToText(message.content))}`
|
|
1160
|
-
: message.role;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function hasMessageContent(message: AgentMessage): message is AgentMessage & { content: unknown } {
|
|
1164
|
-
return "content" in message;
|
|
1165
|
-
}
|