pi-sessions 0.6.0 → 0.7.1

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.
Files changed (43) hide show
  1. package/README.md +22 -11
  2. package/extensions/session-ask/agent.ts +400 -0
  3. package/extensions/session-ask/navigate.ts +880 -0
  4. package/extensions/session-ask.ts +79 -131
  5. package/extensions/session-auto-title/model.ts +3 -11
  6. package/extensions/session-handoff/picker.ts +50 -4
  7. package/extensions/session-handoff/query.ts +74 -56
  8. package/extensions/session-handoff/refs.ts +12 -18
  9. package/extensions/session-handoff.ts +36 -27
  10. package/extensions/session-messaging/broker/process.ts +23 -26
  11. package/extensions/session-messaging/broker/spawn.ts +24 -3
  12. package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
  13. package/extensions/session-messaging/pi/message-contracts.ts +9 -10
  14. package/extensions/session-messaging/pi/message-view.ts +12 -1
  15. package/extensions/session-messaging/pi/service.ts +117 -62
  16. package/extensions/session-messaging/pi/tools.ts +4 -56
  17. package/extensions/session-search/extract.ts +86 -436
  18. package/extensions/session-search/hooks.ts +17 -25
  19. package/extensions/session-search/normalize.ts +1 -1
  20. package/extensions/session-search/reindex.ts +1 -0
  21. package/extensions/session-search.ts +157 -128
  22. package/extensions/shared/model.ts +18 -0
  23. package/extensions/shared/session-broker/active.ts +26 -0
  24. package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
  25. package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
  26. package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
  27. package/extensions/shared/session-index/access.ts +128 -0
  28. package/extensions/shared/session-index/common.ts +43 -48
  29. package/extensions/shared/session-index/index.ts +1 -0
  30. package/extensions/shared/session-index/lineage.ts +10 -0
  31. package/extensions/shared/session-index/query/ast.ts +40 -0
  32. package/extensions/shared/session-index/query/compiler.ts +146 -0
  33. package/extensions/shared/session-index/query/lexer.ts +140 -0
  34. package/extensions/shared/session-index/query/parser.ts +178 -0
  35. package/extensions/shared/session-index/schema.ts +22 -6
  36. package/extensions/shared/session-index/scoring.ts +80 -0
  37. package/extensions/shared/session-index/search.ts +552 -278
  38. package/extensions/shared/session-index/sqlite.ts +16 -9
  39. package/extensions/shared/session-index/store.ts +12 -21
  40. package/extensions/shared/settings.ts +61 -4
  41. package/extensions/shared/text.ts +50 -0
  42. package/package.json +5 -4
  43. /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
@@ -10,13 +10,12 @@ 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, ToolResultMessage } from "@earendil-works/pi-ai";
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 {
@@ -25,6 +24,7 @@ import {
25
24
  parseHandoffSessionMetadata,
26
25
  } from "../session-handoff/metadata.ts";
27
26
  import type { SessionOrigin } from "../shared/session-index/index.ts";
27
+ import { contentToText, isRecord } from "../shared/text.ts";
28
28
  import { safeParseTypeBoxValue } from "../shared/typebox.ts";
29
29
  import {
30
30
  deriveSessionRepoRoots,
@@ -35,7 +35,7 @@ import {
35
35
  } from "./normalize.ts";
36
36
 
37
37
  export interface SearchTextChunk {
38
- entryId?: string | undefined;
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?: string | undefined;
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(createSessionNameChunk(parsed.sessionName, fallbackTs));
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(sessionName: string, ts: string): SearchTextChunk {
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 | undefined,
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 groupEntriesByParent(entries: SessionEntry[]): Map<string | null, SessionEntry[]> {
654
- const byParent = new Map<string | null, SessionEntry[]>();
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 | undefined,
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 buildOptionalMessageChunk(
846
- entryId,
847
- "assistant",
848
- ts,
849
- "assistant_text",
850
- contentToText(message.content),
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 | undefined,
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 | undefined,
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
- }