agentel 0.2.0 → 0.2.2

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.
@@ -5,6 +5,14 @@ const { toIso } = require("../archive");
5
5
  const PROVIDER = "gemini_cli";
6
6
 
7
7
  function parseGeminiCliJsonl(text, options = {}) {
8
+ return parseGeminiCliEvents(geminiJsonlEvents(text), options);
9
+ }
10
+
11
+ function parseGeminiCliJsonlSessions(text, options = {}) {
12
+ return parseGeminiCliEventSessions(geminiJsonlEvents(text), options);
13
+ }
14
+
15
+ function geminiJsonlEvents(text) {
8
16
  const events = [];
9
17
  for (const line of String(text || "").split(/\r?\n/)) {
10
18
  if (!line.trim()) continue;
@@ -14,13 +22,29 @@ function parseGeminiCliJsonl(text, options = {}) {
14
22
  // Ignore malformed partial lines in append-only history files.
15
23
  }
16
24
  }
17
- return parseGeminiCliEvents(events, options);
25
+ return events;
18
26
  }
19
27
 
20
28
  function parseGeminiCliJson(data, options = {}) {
21
29
  return parseGeminiCliEvents(geminiEventList(data), { ...options, root: data });
22
30
  }
23
31
 
32
+ function parseGeminiCliJsonSessions(data, options = {}) {
33
+ return parseGeminiCliEventSessions(geminiEventList(data), { ...options, root: data });
34
+ }
35
+
36
+ function parseGeminiCliEventSessions(events, options = {}) {
37
+ const groups = geminiSessionEventGroups(events, options);
38
+ return groups
39
+ .map((group) =>
40
+ parseGeminiCliEvents(group.events, {
41
+ ...options,
42
+ sessionId: group.sessionId || options.sessionId
43
+ })
44
+ )
45
+ .filter((session) => session.messages.length);
46
+ }
47
+
24
48
  function parseGeminiCliEvents(events, options = {}) {
25
49
  const root = options.root && typeof options.root === "object" ? options.root : {};
26
50
  const normalizedEvents = coalesceGeminiIncrementalEvents(events);
@@ -31,19 +55,27 @@ function parseGeminiCliEvents(events, options = {}) {
31
55
  };
32
56
  let sessionId = firstString(options.sessionId, geminiSessionId(root));
33
57
  let title = firstString(options.title, geminiTitle(root));
58
+ let topicTitle = "";
34
59
  let cwd = firstString(options.cwd, geminiCwd(root));
35
60
  const messages = [];
61
+ const sessionSummaries = [];
36
62
 
37
63
  for (let index = 0; index < normalizedEvents.length; index++) {
38
64
  const event = normalizedEvents[index];
39
65
  if (!event || typeof event !== "object") continue;
40
66
  context.model = firstString(geminiModel(event), context.model);
41
- sessionId ||= geminiSessionId(event);
67
+ sessionId ||= geminiEventSessionId(event);
42
68
  title ||= geminiTitle(event);
43
69
  cwd ||= geminiCwd(event);
44
70
  const timestamp = eventTimestamp(event, fallbackTime, index);
45
71
  context.lastTimestamp = timestamp;
46
72
  const usage = geminiUsage(event);
73
+ const sessionSummary = geminiSessionSummary(event, timestamp);
74
+ if (sessionSummary) {
75
+ sessionSummaries.push(sessionSummary);
76
+ sessionId ||= sessionSummary.sessionId;
77
+ continue;
78
+ }
47
79
 
48
80
  const checkpoint = geminiCheckpointMessage(event, timestamp);
49
81
  if (checkpoint) {
@@ -52,6 +84,7 @@ function parseGeminiCliEvents(events, options = {}) {
52
84
  }
53
85
 
54
86
  const extracted = geminiMessagesFromEvent(event, { ...context, timestamp, usage });
87
+ topicTitle = firstString(geminiTopicTitleFromMessages(extracted), topicTitle);
55
88
  if (extracted.length) {
56
89
  messages.push(...extracted);
57
90
  continue;
@@ -70,12 +103,13 @@ function parseGeminiCliEvents(events, options = {}) {
70
103
 
71
104
  return {
72
105
  sessionId,
73
- title,
106
+ title: firstString(topicTitle, title),
74
107
  cwd,
75
108
  model: context.model,
76
109
  messages: sorted,
77
110
  startedAt: sorted[0]?.timestamp || "",
78
- endedAt: sorted[sorted.length - 1]?.timestamp || ""
111
+ endedAt: sorted[sorted.length - 1]?.timestamp || "",
112
+ sessionSummary: mergeGeminiSessionSummaries(sessionSummaries)
79
113
  };
80
114
  }
81
115
 
@@ -101,6 +135,27 @@ function geminiEventList(data) {
101
135
  return [data];
102
136
  }
103
137
 
138
+ function geminiSessionEventGroups(events, options = {}) {
139
+ const rootSessionId = firstString(options.sessionId, geminiSessionId(options.root));
140
+ const groups = new Map();
141
+ const explicitSessionIds = new Set();
142
+ let currentSessionId = rootSessionId;
143
+ for (const event of events || []) {
144
+ const explicitSessionId = geminiEventSessionId(event) || geminiSessionSummaryId(event);
145
+ if (explicitSessionId) {
146
+ explicitSessionIds.add(explicitSessionId);
147
+ currentSessionId = explicitSessionId;
148
+ }
149
+ const key = currentSessionId || "";
150
+ if (!groups.has(key)) groups.set(key, { sessionId: key, events: [] });
151
+ groups.get(key).events.push(event);
152
+ }
153
+ if (explicitSessionIds.size <= 1) {
154
+ return [{ sessionId: rootSessionId || [...explicitSessionIds][0] || "", events: events || [] }];
155
+ }
156
+ return [...groups.values()];
157
+ }
158
+
104
159
  function coalesceGeminiIncrementalEvents(events) {
105
160
  const output = [];
106
161
  const indexes = new Map();
@@ -109,7 +164,7 @@ function coalesceGeminiIncrementalEvents(events) {
109
164
  output.push(event);
110
165
  continue;
111
166
  }
112
- const id = geminiEventId(event);
167
+ const id = geminiIncrementalEventKey(event);
113
168
  if (!id) {
114
169
  output.push(event);
115
170
  continue;
@@ -125,6 +180,13 @@ function coalesceGeminiIncrementalEvents(events) {
125
180
  return output;
126
181
  }
127
182
 
183
+ function geminiIncrementalEventKey(event) {
184
+ const id = geminiEventId(event);
185
+ if (!id) return "";
186
+ const sessionId = geminiEventSessionId(event);
187
+ return sessionId ? `${sessionId}:${id}` : id;
188
+ }
189
+
128
190
  function mergeGeminiEvent(previous, next) {
129
191
  const merged = { ...(previous && typeof previous === "object" ? previous : {}) };
130
192
  for (const [key, value] of Object.entries(next || {})) {
@@ -571,6 +633,270 @@ function applyUsageToLastAssistant(messages, usage) {
571
633
  target.metadata = { ...(target.metadata || {}), usage };
572
634
  }
573
635
 
636
+ function geminiTopicTitleFromMessages(messages) {
637
+ let title = "";
638
+ for (const message of messages || []) {
639
+ for (const call of asArray(message?.metadata?.toolCalls)) {
640
+ if (!isGeminiTopicContextCall(call)) continue;
641
+ title = firstString(geminiTopicTitleFromArgs(call.arguments), geminiTopicTitleFromArgs(call.rawInputSummary), title);
642
+ }
643
+ }
644
+ return title;
645
+ }
646
+
647
+ function isGeminiTopicContextCall(call) {
648
+ return [call?.name, call?.displayName, call?.display_name, call?.title]
649
+ .filter((value) => typeof value === "string" && value.trim())
650
+ .some((name) => {
651
+ const normalized = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
652
+ return normalized === "updatetopic" || normalized === "updatetopiccontext" || /update.*topic.*context/i.test(name);
653
+ });
654
+ }
655
+
656
+ function geminiTopicTitleFromArgs(args) {
657
+ if (!args) return "";
658
+ if (typeof args === "string") {
659
+ const parsed = parseToolArgsValue(args);
660
+ if (parsed && typeof parsed === "object") return geminiTopicTitleFromArgs(parsed);
661
+ return cleanGeminiTopicTitle(
662
+ firstString(
663
+ matchGeminiTopicTitle(args, /\btitle\s*:\s*([^,\n]+)/i),
664
+ matchGeminiTopicTitle(args, /\btitle\s+(.+?)(?:,\s*(?:strategic[_\s-]*intent|summary)\b|\s+and\s+title\s*:|$)/i)
665
+ )
666
+ );
667
+ }
668
+ if (typeof args !== "object") return "";
669
+ return cleanGeminiTopicTitle(
670
+ firstString(
671
+ args.title,
672
+ args.topicTitle,
673
+ args.topic_title,
674
+ args.topic,
675
+ args.topic?.title,
676
+ args.context?.title,
677
+ args.topicContext?.title,
678
+ args.topic_context?.title
679
+ )
680
+ );
681
+ }
682
+
683
+ function matchGeminiTopicTitle(text, pattern) {
684
+ const match = String(text || "").match(pattern);
685
+ return match ? match[1] : "";
686
+ }
687
+
688
+ function cleanGeminiTopicTitle(value) {
689
+ const title = String(value || "")
690
+ .replace(/\s+/g, " ")
691
+ .trim()
692
+ .replace(/^["'`]+|["'`]+$/g, "")
693
+ .trim();
694
+ if (!title || /^update topic context$/i.test(title)) return "";
695
+ return title;
696
+ }
697
+
698
+ function geminiSessionSummary(event, timestamp) {
699
+ const text = geminiSessionSummaryText(event);
700
+ const parsed = parseGeminiSessionSummaryText(text, timestamp);
701
+ if (parsed) return parsed;
702
+ const value = event?.sessionSummary || event?.session_summary || event?.summary;
703
+ if (!value || typeof value !== "object") return null;
704
+ const structured = compactMetadata({
705
+ sessionId: firstString(value.sessionId, value.session_id, value.id),
706
+ resumeCommand: firstString(value.resumeCommand, value.resume_command),
707
+ toolCalls: value.toolCalls || value.tool_calls,
708
+ successRatePercent: numberValue(value.successRatePercent ?? value.success_rate_percent ?? value.successRate ?? value.success_rate),
709
+ userAgreement: value.userAgreement || value.user_agreement,
710
+ performance: value.performance,
711
+ modelUsage: Array.isArray(value.modelUsage) ? value.modelUsage : Array.isArray(value.model_usage) ? value.model_usage : undefined,
712
+ occurredAt: timestamp
713
+ });
714
+ const usage = geminiSessionSummaryUsage(structured);
715
+ if (usage) structured.usage = usage;
716
+ return Object.keys(structured).length ? structured : null;
717
+ }
718
+
719
+ function geminiSessionSummaryText(event) {
720
+ const candidates = [
721
+ event?.sessionSummary,
722
+ event?.session_summary,
723
+ event?.summary,
724
+ event?.content,
725
+ event?.message,
726
+ event?.text,
727
+ event?.output,
728
+ event?.payload?.sessionSummary,
729
+ event?.payload?.session_summary,
730
+ event?.payload?.summary,
731
+ event?.payload?.content,
732
+ event?.payload?.message,
733
+ event?.payload?.text,
734
+ event?.data?.summary,
735
+ event?.data?.content,
736
+ event?.data?.message,
737
+ event?.data?.text
738
+ ];
739
+ for (const candidate of candidates) {
740
+ if (typeof candidate === "string" && looksLikeGeminiSessionSummary(candidate)) return candidate;
741
+ }
742
+ return "";
743
+ }
744
+
745
+ function geminiSessionSummaryId(event) {
746
+ const text = geminiSessionSummaryText(event);
747
+ if (!text) return "";
748
+ return parseGeminiSessionSummaryText(text)?.sessionId || "";
749
+ }
750
+
751
+ function looksLikeGeminiSessionSummary(text) {
752
+ const normalized = String(text || "");
753
+ return /Interaction Summary/i.test(normalized) && /(?:Session ID:|To resume this session:)/i.test(normalized);
754
+ }
755
+
756
+ function parseGeminiSessionSummaryText(text, timestamp = "") {
757
+ if (!looksLikeGeminiSessionSummary(text)) return null;
758
+ const lines = cleanGeminiSessionSummaryLines(text);
759
+ const summary = { occurredAt: timestamp || undefined };
760
+ const modelUsage = [];
761
+ let inModelTable = false;
762
+ let lastModel = null;
763
+ for (const line of lines) {
764
+ const sessionId = line.match(/^Session ID:\s*(.+)$/i);
765
+ if (sessionId) {
766
+ summary.sessionId = sessionId[1].trim();
767
+ continue;
768
+ }
769
+ const toolCalls = line.match(/^Tool Calls:\s*([\d,]+)(?:\s*\(\s*(?:[\u2713\u2714]\s*)?([\d,]+)?\s*(?:x|\u00d7)\s*([\d,]+)\s*\))?/i);
770
+ if (toolCalls) {
771
+ const total = numberValue(toolCalls[1]);
772
+ const succeeded = numberValue(toolCalls[2]);
773
+ const failed = numberValue(toolCalls[3]);
774
+ summary.toolCalls = compactMetadata({
775
+ total,
776
+ succeeded: succeeded || (Number.isFinite(total) && Number.isFinite(failed) ? total - failed : undefined),
777
+ failed
778
+ });
779
+ continue;
780
+ }
781
+ const success = line.match(/^Success Rate:\s*([\d.]+)%/i);
782
+ if (success) {
783
+ summary.successRatePercent = numberValue(success[1]);
784
+ continue;
785
+ }
786
+ const agreement = line.match(/^User Agreement:\s*([\d.]+)%(?:\s*\(([\d,]+)\s+reviewed\))?/i);
787
+ if (agreement) {
788
+ summary.userAgreement = compactMetadata({
789
+ ratePercent: numberValue(agreement[1]),
790
+ reviewed: numberValue(agreement[2])
791
+ });
792
+ continue;
793
+ }
794
+ const wall = line.match(/^Wall Time:\s*(.+)$/i);
795
+ if (wall) {
796
+ summary.performance = { ...(summary.performance || {}), wallTimeMs: parseGeminiDurationMs(wall[1]) };
797
+ continue;
798
+ }
799
+ const active = line.match(/^Agent Active:\s*(.+)$/i);
800
+ if (active) {
801
+ summary.performance = { ...(summary.performance || {}), agentActiveMs: parseGeminiDurationMs(active[1]) };
802
+ continue;
803
+ }
804
+ const api = line.match(/^(?:\u00bb\s*)?API Time:\s*([^(]+?)(?:\s*\(([\d.]+)%\))?$/i);
805
+ if (api) {
806
+ summary.performance = { ...(summary.performance || {}), apiTimeMs: parseGeminiDurationMs(api[1]), apiTimePercent: numberValue(api[2]) };
807
+ continue;
808
+ }
809
+ const tool = line.match(/^(?:\u00bb\s*)?Tool Time:\s*([^(]+?)(?:\s*\(([\d.]+)%\))?$/i);
810
+ if (tool) {
811
+ summary.performance = { ...(summary.performance || {}), toolTimeMs: parseGeminiDurationMs(tool[1]), toolTimePercent: numberValue(tool[2]) };
812
+ continue;
813
+ }
814
+ const resume = line.match(/^To resume this session:\s*(.+)$/i);
815
+ if (resume) {
816
+ summary.resumeCommand = resume[1].trim();
817
+ continue;
818
+ }
819
+ if (/^Model\s+Reqs\s+Input Tokens\s+Cache Reads\s+Output Tokens$/i.test(line)) {
820
+ inModelTable = true;
821
+ continue;
822
+ }
823
+ if (!inModelTable) continue;
824
+ if (/^Use \/model\b/i.test(line) || /^Model Usage$/i.test(line)) continue;
825
+ const model = parseGeminiModelUsageLine(line);
826
+ if (!model) continue;
827
+ if (model.role && lastModel) {
828
+ lastModel.roles = (lastModel.roles || []).concat(model);
829
+ continue;
830
+ }
831
+ modelUsage.push(model);
832
+ lastModel = model;
833
+ }
834
+ if (modelUsage.length) summary.modelUsage = modelUsage;
835
+ const usage = geminiSessionSummaryUsage(summary);
836
+ if (usage) summary.usage = usage;
837
+ const compact = compactMetadata(summary);
838
+ if (compact.performance) compact.performance = compactMetadata(compact.performance);
839
+ if (compact.toolCalls) compact.toolCalls = compactMetadata(compact.toolCalls);
840
+ if (compact.userAgreement) compact.userAgreement = compactMetadata(compact.userAgreement);
841
+ return Object.keys(compact).length ? compact : null;
842
+ }
843
+
844
+ function cleanGeminiSessionSummaryLines(text) {
845
+ return String(text || "")
846
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
847
+ .split(/\r?\n/)
848
+ .map((line) =>
849
+ line
850
+ .replace(/^[\s\u2502\u2503\u2551]+|[\s\u2502\u2503\u2551]+$/g, "")
851
+ .replace(/^[\u00bb]\s*/, "\u00bb ")
852
+ .trim()
853
+ )
854
+ .filter((line) => line && /[A-Za-z0-9/]/.test(line) && !/^[\u2500-\u257f\s-]+$/.test(line));
855
+ }
856
+
857
+ function parseGeminiModelUsageLine(line) {
858
+ const role = /^\u21b3\s*/.test(line);
859
+ const normalized = line.replace(/^\u21b3\s*/, "");
860
+ const match = normalized.match(/^(.+?)\s+([\d,]+)\s+([\d,]+)\s+([\d,]+)\s+([\d,]+)$/);
861
+ if (!match) return null;
862
+ const row = {
863
+ requests: numberValue(match[2]),
864
+ inputTokens: numberValue(match[3]),
865
+ cacheReadTokens: numberValue(match[4]),
866
+ outputTokens: numberValue(match[5])
867
+ };
868
+ if (role) return { role: match[1].trim(), ...row };
869
+ return { model: match[1].trim(), ...row };
870
+ }
871
+
872
+ function geminiSessionSummaryUsage(summary) {
873
+ const models = Array.isArray(summary?.modelUsage) ? summary.modelUsage : [];
874
+ let inputTokens = 0;
875
+ let outputTokens = 0;
876
+ let cacheInputTokens = 0;
877
+ for (const model of models) {
878
+ inputTokens += numberValue(model.inputTokens) || 0;
879
+ outputTokens += numberValue(model.outputTokens) || 0;
880
+ cacheInputTokens += numberValue(model.cacheReadTokens) || 0;
881
+ }
882
+ if (!inputTokens && !outputTokens && !cacheInputTokens) return null;
883
+ return { inputTokens, outputTokens, cacheInputTokens, totalTokens: inputTokens + outputTokens };
884
+ }
885
+
886
+ function mergeGeminiSessionSummaries(summaries) {
887
+ const values = summaries.filter(Boolean);
888
+ if (!values.length) return null;
889
+ return values.reduce((merged, summary) => ({
890
+ ...merged,
891
+ ...summary,
892
+ toolCalls: { ...(merged.toolCalls || {}), ...(summary.toolCalls || {}) },
893
+ userAgreement: { ...(merged.userAgreement || {}), ...(summary.userAgreement || {}) },
894
+ performance: { ...(merged.performance || {}), ...(summary.performance || {}) },
895
+ modelUsage: summary.modelUsage || merged.modelUsage,
896
+ usage: summary.usage || merged.usage
897
+ }), {});
898
+ }
899
+
574
900
  function geminiSessionId(value) {
575
901
  return firstString(
576
902
  value?.sessionId,
@@ -586,6 +912,20 @@ function geminiSessionId(value) {
586
912
  );
587
913
  }
588
914
 
915
+ function geminiEventSessionId(value) {
916
+ return firstString(
917
+ value?.sessionId,
918
+ value?.session_id,
919
+ value?.conversationId,
920
+ value?.conversation_id,
921
+ value?.chatId,
922
+ value?.chat_id,
923
+ value?.session?.id,
924
+ value?.metadata?.sessionId,
925
+ value?.metadata?.session_id
926
+ );
927
+ }
928
+
589
929
  function geminiTitle(value) {
590
930
  return firstString(value?.title, value?.name, value?.summary, value?.metadata?.title, value?.session?.title);
591
931
  }
@@ -758,10 +1098,29 @@ function firstString(...values) {
758
1098
  }
759
1099
 
760
1100
  function numberValue(value) {
761
- const number = Number(value);
1101
+ if (value === undefined || value === null || value === "") return undefined;
1102
+ const number = Number(String(value ?? "").replace(/,/g, ""));
762
1103
  return Number.isFinite(number) ? number : undefined;
763
1104
  }
764
1105
 
1106
+ function parseGeminiDurationMs(value) {
1107
+ const text = String(value || "").trim();
1108
+ if (!text) return undefined;
1109
+ let total = 0;
1110
+ const re = /([\d.]+)\s*(ms|milliseconds?|h|hr|hrs|hours?|m|min|mins|minutes?|s|sec|secs|seconds?)/gi;
1111
+ let match;
1112
+ while ((match = re.exec(text))) {
1113
+ const amount = Number(match[1]);
1114
+ if (!Number.isFinite(amount)) continue;
1115
+ const unit = match[2].toLowerCase();
1116
+ if (unit.startsWith("ms") || unit.startsWith("millisecond")) total += amount;
1117
+ else if (unit === "h" || unit.startsWith("hr") || unit.startsWith("hour")) total += amount * 60 * 60 * 1000;
1118
+ else if (unit === "m" || unit.startsWith("min") || unit.startsWith("minute")) total += amount * 60 * 1000;
1119
+ else total += amount * 1000;
1120
+ }
1121
+ return total ? Math.round(total) : undefined;
1122
+ }
1123
+
765
1124
  function asArray(value) {
766
1125
  if (Array.isArray(value)) return value;
767
1126
  return value == null ? [] : [value];
@@ -791,5 +1150,8 @@ function uniqueObjects(values) {
791
1150
  module.exports = {
792
1151
  parseGeminiCliEvents,
793
1152
  parseGeminiCliJson,
794
- parseGeminiCliJsonl
1153
+ parseGeminiCliJsonSessions,
1154
+ parseGeminiCliJsonl,
1155
+ parseGeminiCliJsonlSessions,
1156
+ parseGeminiSessionSummaryText
795
1157
  };