pi-context 1.1.1 → 1.1.3
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/dist/index.js +370 -0
- package/package.json +1 -1
- package/src/context.ts +5 -0
- package/src/index.ts +13 -10
- package/src/utils.ts +2 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { formatTokens } from "./utils.js";
|
|
3
|
+
const InternalTools = ["context_tag", "context_log", "context_checkout"];
|
|
4
|
+
let CommandCtx = null;
|
|
5
|
+
let CheckoutParams = null;
|
|
6
|
+
const isInternal = (name) => InternalTools.includes(name);
|
|
7
|
+
const resolveTargetId = (sm, target) => {
|
|
8
|
+
if (target.toLowerCase() === "root") {
|
|
9
|
+
const tree = sm.getTree();
|
|
10
|
+
return tree.length > 0 ? tree[0].entry.id : target;
|
|
11
|
+
}
|
|
12
|
+
// If it already looks like an ID, keep it.
|
|
13
|
+
if (/^[0-9a-f]{8,}$/i.test(target))
|
|
14
|
+
return target;
|
|
15
|
+
// Iterative DFS to avoid call stack overflows on deep histories.
|
|
16
|
+
const stack = [...sm.getTree()];
|
|
17
|
+
while (stack.length > 0) {
|
|
18
|
+
const n = stack.pop();
|
|
19
|
+
if (sm.getLabel(n.entry.id) === target)
|
|
20
|
+
return n.entry.id;
|
|
21
|
+
if (n.children?.length)
|
|
22
|
+
stack.push(...n.children);
|
|
23
|
+
}
|
|
24
|
+
// Fallback: let SessionManager deal with invalid targets downstream.
|
|
25
|
+
return target;
|
|
26
|
+
};
|
|
27
|
+
const ContextLogParams = Type.Object({
|
|
28
|
+
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
29
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show ALL messages. If false (default), collapses intermediate AI steps and only shows 'milestones': User messages, Tags, Branch Points, and Summaries." })),
|
|
30
|
+
});
|
|
31
|
+
const ContextCheckoutParams = Type.Object({
|
|
32
|
+
target: Type.String({ description: "Where to jump/squash to. Can be a tag name (e.g., 'task-start'), a commit ID, or 'root'. This is the base for your new branch." }),
|
|
33
|
+
message: Type.String({ description: "The 'Carryover Message' for the new branch. A summary of your *current* progress/lessons that you want to bring with you to the new state. This ensures you don't lose key information when switching contexts. Good summary message: '[Status] + [Reason] + [Important Changes] + [Carryover Data]'" }),
|
|
34
|
+
backupTag: Type.Optional(Type.String({ description: "Optional tag name to apply to the CURRENT state before checking out. Use this to create an automatic backup of the history you are about to leave/squash." })),
|
|
35
|
+
});
|
|
36
|
+
const ContextTagParams = Type.Object({
|
|
37
|
+
name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
|
|
38
|
+
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
39
|
+
});
|
|
40
|
+
export default function (pi) {
|
|
41
|
+
pi.registerCommand("acm", {
|
|
42
|
+
description: "Enable agentic context management for the current session",
|
|
43
|
+
handler: async (args, ctx) => {
|
|
44
|
+
CommandCtx = ctx;
|
|
45
|
+
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
46
|
+
pi.sendMessage({
|
|
47
|
+
customType: "pi-context",
|
|
48
|
+
content: "use context-management skill",
|
|
49
|
+
display: false,
|
|
50
|
+
}, {
|
|
51
|
+
deliverAs: "followUp"
|
|
52
|
+
});
|
|
53
|
+
if (args) {
|
|
54
|
+
pi.sendUserMessage(args);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Helper: Check if a tag name already exists in the tree
|
|
59
|
+
const findTagInTree = (sm, nodes, tagName) => {
|
|
60
|
+
for (const n of nodes) {
|
|
61
|
+
if (sm.getLabel(n.entry.id) === tagName)
|
|
62
|
+
return n.entry.id;
|
|
63
|
+
const r = findTagInTree(sm, n.children, tagName);
|
|
64
|
+
if (r)
|
|
65
|
+
return r;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
pi.registerTool({
|
|
70
|
+
name: "context_tag",
|
|
71
|
+
label: "Context Tag",
|
|
72
|
+
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
73
|
+
parameters: ContextTagParams,
|
|
74
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
75
|
+
const sm = ctx.sessionManager;
|
|
76
|
+
// Deduplication check: ensure tag name is unique
|
|
77
|
+
const existingTagId = findTagInTree(sm, sm.getTree(), params.name);
|
|
78
|
+
if (existingTagId) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `Error: Tag '${params.name}' already exists at ${existingTagId}. Tag names must be unique. Use a different name or delete the existing tag first.`
|
|
83
|
+
}],
|
|
84
|
+
details: {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
88
|
+
if (!id) {
|
|
89
|
+
// Auto-resolve: Find the last "interesting" node to tag.
|
|
90
|
+
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
91
|
+
const branch = sm.getBranch();
|
|
92
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
93
|
+
const entry = branch[i];
|
|
94
|
+
// 1. Check ToolResults
|
|
95
|
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
96
|
+
const tr = entry.message;
|
|
97
|
+
if (isInternal(tr.toolName))
|
|
98
|
+
continue;
|
|
99
|
+
// Public tool result is a valid target
|
|
100
|
+
id = entry.id;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
// 2. Check Assistant messages for visibility
|
|
104
|
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
105
|
+
const m = entry.message;
|
|
106
|
+
const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
|
|
107
|
+
if (!hasInternalTool) {
|
|
108
|
+
id = entry.id;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
id = entry.id;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// Fallback to leaf if search failed
|
|
116
|
+
if (!id)
|
|
117
|
+
id = sm.getLeafId() ?? "";
|
|
118
|
+
}
|
|
119
|
+
pi.setLabel(id, params.name);
|
|
120
|
+
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
pi.registerTool({
|
|
124
|
+
name: "context_log",
|
|
125
|
+
label: "Context Log",
|
|
126
|
+
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
127
|
+
parameters: ContextLogParams,
|
|
128
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
129
|
+
const sm = ctx.sessionManager;
|
|
130
|
+
const branch = sm.getBranch();
|
|
131
|
+
const currentLeafId = sm.getLeafId();
|
|
132
|
+
const verbose = params.verbose ?? false;
|
|
133
|
+
const limit = params.limit ?? 50;
|
|
134
|
+
const backboneIds = new Set(branch.map((e) => e.id));
|
|
135
|
+
const sequence = [];
|
|
136
|
+
branch.forEach((entry) => {
|
|
137
|
+
sequence.push(entry);
|
|
138
|
+
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
139
|
+
const children = sm.getChildren(entry.id);
|
|
140
|
+
children.forEach((child) => {
|
|
141
|
+
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
142
|
+
sequence.push(child);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
const getMsgContent = (entry) => {
|
|
147
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
148
|
+
const e = entry;
|
|
149
|
+
return e.summary || "[No summary provided]";
|
|
150
|
+
}
|
|
151
|
+
if (entry.type === "label") {
|
|
152
|
+
return `tag: ${entry.label}`;
|
|
153
|
+
}
|
|
154
|
+
if (entry.type === "message") {
|
|
155
|
+
const msg = entry.message;
|
|
156
|
+
if (msg.role === "toolResult") {
|
|
157
|
+
const tr = msg;
|
|
158
|
+
if (!verbose && isInternal(tr.toolName))
|
|
159
|
+
return "";
|
|
160
|
+
const extractText = (content) => {
|
|
161
|
+
return content
|
|
162
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
163
|
+
.join(" ")
|
|
164
|
+
.trim();
|
|
165
|
+
};
|
|
166
|
+
let resText = extractText(tr.content);
|
|
167
|
+
const details = tr.details;
|
|
168
|
+
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
169
|
+
resText = `${details.path}: ${resText}`;
|
|
170
|
+
}
|
|
171
|
+
return `(${tr.toolName}) ${resText}`;
|
|
172
|
+
}
|
|
173
|
+
if (msg.role === "bashExecution") {
|
|
174
|
+
return `[Bash] ${msg.command}`;
|
|
175
|
+
}
|
|
176
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
177
|
+
let text = "";
|
|
178
|
+
if (typeof msg.content === "string") {
|
|
179
|
+
text = msg.content;
|
|
180
|
+
}
|
|
181
|
+
else if (Array.isArray(msg.content)) {
|
|
182
|
+
text = msg.content
|
|
183
|
+
.map((p) => {
|
|
184
|
+
if (typeof p === "object" && p !== null && "text" in p)
|
|
185
|
+
return p.text;
|
|
186
|
+
return "";
|
|
187
|
+
})
|
|
188
|
+
.join(" ")
|
|
189
|
+
.trim();
|
|
190
|
+
}
|
|
191
|
+
let toolCallsText = "";
|
|
192
|
+
if (msg.role === "assistant") {
|
|
193
|
+
const toolCalls = msg.content.filter((c) => c.type === "toolCall");
|
|
194
|
+
toolCallsText = toolCalls
|
|
195
|
+
.filter((tc) => verbose || !isInternal(tc.name))
|
|
196
|
+
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
197
|
+
.join("; ");
|
|
198
|
+
}
|
|
199
|
+
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
};
|
|
204
|
+
const isInteresting = (entry) => {
|
|
205
|
+
// 1. HEAD and Root
|
|
206
|
+
if (entry.id === currentLeafId)
|
|
207
|
+
return true;
|
|
208
|
+
if (branch.length > 0 && entry.id === branch[0].id)
|
|
209
|
+
return true;
|
|
210
|
+
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
211
|
+
if (sm.getLabel(entry.id))
|
|
212
|
+
return true;
|
|
213
|
+
if (entry.type === 'label')
|
|
214
|
+
return false; // Hide label nodes, they are redundant
|
|
215
|
+
// 3. Structural Milestones (Summaries)
|
|
216
|
+
if (entry.type === 'branch_summary' || entry.type === 'compaction')
|
|
217
|
+
return true;
|
|
218
|
+
// 4. Branch Points (Forks)
|
|
219
|
+
if (sm.getChildren(entry.id).length > 1)
|
|
220
|
+
return true;
|
|
221
|
+
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
222
|
+
if (entry.type === 'message' && entry.message.role === 'user')
|
|
223
|
+
return true;
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
const visibleSequenceIds = new Set();
|
|
227
|
+
sequence.forEach(e => {
|
|
228
|
+
if (verbose || isInteresting(e)) {
|
|
229
|
+
visibleSequenceIds.add(e.id);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
233
|
+
if (visibleEntries.length > limit) {
|
|
234
|
+
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
235
|
+
visibleSequenceIds.clear();
|
|
236
|
+
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
237
|
+
}
|
|
238
|
+
const lines = [];
|
|
239
|
+
let hiddenCount = 0;
|
|
240
|
+
sequence.forEach((entry) => {
|
|
241
|
+
if (!visibleSequenceIds.has(entry.id)) {
|
|
242
|
+
hiddenCount++;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (hiddenCount > 0) {
|
|
246
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
247
|
+
hiddenCount = 0;
|
|
248
|
+
}
|
|
249
|
+
const isHead = entry.id === currentLeafId;
|
|
250
|
+
const label = sm.getLabel(entry.id);
|
|
251
|
+
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
252
|
+
let role = entry.type.toUpperCase();
|
|
253
|
+
if (entry.type === "message") {
|
|
254
|
+
const m = entry.message;
|
|
255
|
+
role =
|
|
256
|
+
m.role === "assistant"
|
|
257
|
+
? "AI"
|
|
258
|
+
: m.role === "user"
|
|
259
|
+
? "USER"
|
|
260
|
+
: m.role === "bashExecution"
|
|
261
|
+
? "BASH"
|
|
262
|
+
: "TOOL";
|
|
263
|
+
}
|
|
264
|
+
else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
265
|
+
role = "SUMMARY";
|
|
266
|
+
}
|
|
267
|
+
// hide custom messages
|
|
268
|
+
if (role === "CUSTOM_MESSAGE") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const id = entry.id;
|
|
272
|
+
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
273
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
274
|
+
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
275
|
+
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
276
|
+
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
277
|
+
});
|
|
278
|
+
if (hiddenCount > 0) {
|
|
279
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
280
|
+
}
|
|
281
|
+
// --- Context Dashboard (HUD) ---
|
|
282
|
+
const usage = await ctx.getContextUsage();
|
|
283
|
+
let usageStr = "Unknown";
|
|
284
|
+
if (usage && usage.percent != null && usage.tokens != null && usage.contextWindow != null) {
|
|
285
|
+
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
286
|
+
}
|
|
287
|
+
// Find the distance to the nearest tag
|
|
288
|
+
let stepsSinceTag = 0;
|
|
289
|
+
let nearestTagName = "None";
|
|
290
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
291
|
+
const id = branch[i].id;
|
|
292
|
+
const label = sm.getLabel(id);
|
|
293
|
+
if (label) {
|
|
294
|
+
nearestTagName = label;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
stepsSinceTag++;
|
|
298
|
+
}
|
|
299
|
+
const hud = [
|
|
300
|
+
`[Context Dashboard]`,
|
|
301
|
+
`• Context Usage: ${usageStr}`,
|
|
302
|
+
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
303
|
+
`---------------------------------------------------`
|
|
304
|
+
].join("\n");
|
|
305
|
+
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
pi.registerTool({
|
|
309
|
+
name: "context_checkout",
|
|
310
|
+
label: "Context Checkout",
|
|
311
|
+
description: "Navigate to ANY point in the conversation history. This checkout only resets *conversation history*, NOT disk files. ALWAYS provide a detailed 'message' to bridge context.",
|
|
312
|
+
parameters: ContextCheckoutParams,
|
|
313
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
314
|
+
if (!CommandCtx) {
|
|
315
|
+
ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`);
|
|
316
|
+
return {
|
|
317
|
+
content: [{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: "Agentic context management is not enabled. Ask the user to run `/acm` in the pi to enable it, then retry."
|
|
320
|
+
}],
|
|
321
|
+
details: {}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const sm = ctx.sessionManager;
|
|
325
|
+
const tid = resolveTargetId(sm, params.target);
|
|
326
|
+
const currentLeaf = sm.getLeafId();
|
|
327
|
+
if (currentLeaf === tid) {
|
|
328
|
+
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
329
|
+
}
|
|
330
|
+
if (params.backupTag && currentLeaf) {
|
|
331
|
+
pi.setLabel(currentLeaf, params.backupTag);
|
|
332
|
+
}
|
|
333
|
+
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
334
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
335
|
+
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
336
|
+
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
337
|
+
CheckoutParams = params;
|
|
338
|
+
CheckoutParams.nid = nid;
|
|
339
|
+
CheckoutParams.tid = tid;
|
|
340
|
+
CheckoutParams.enrichedMessage = enrichedMessage;
|
|
341
|
+
return { content: [{ type: "text", text: "checkout start" }], details: {} };
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
345
|
+
if (!CheckoutParams) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
ctx.abort();
|
|
349
|
+
});
|
|
350
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
351
|
+
if (!CheckoutParams) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!CommandCtx) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
await CommandCtx.navigateTree(CheckoutParams.nid, {
|
|
358
|
+
summarize: false,
|
|
359
|
+
});
|
|
360
|
+
ctx.ui.notify(`Checked out ${CheckoutParams.target}${CheckoutParams.target === CheckoutParams.tid ? "" : `(${CheckoutParams.tid})`}\nBackup tag created: ${CheckoutParams.backupTag || "none"}\nmessage: ${CheckoutParams.enrichedMessage}`, "info");
|
|
361
|
+
CheckoutParams = null;
|
|
362
|
+
pi.sendMessage({
|
|
363
|
+
customType: "pi-context",
|
|
364
|
+
content: "context_checkout complete. A summary of your previous branch was injected above. Read it to understand your new state. Execute the 'Next Step' from the summary",
|
|
365
|
+
display: false,
|
|
366
|
+
}, {
|
|
367
|
+
triggerTurn: true,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -62,6 +62,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
62
|
const totalActual = usage.tokens;
|
|
63
63
|
const limit = usage.contextWindow;
|
|
64
64
|
|
|
65
|
+
if (totalActual == null || limit == null || usage.percent == null) {
|
|
66
|
+
ctx.ui.notify("Context usage info not available.", "warning");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
66
71
|
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
67
72
|
|
package/src/index.ts
CHANGED
|
@@ -30,17 +30,20 @@ const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
|
30
30
|
const tree = sm.getTree();
|
|
31
31
|
return tree.length > 0 ? tree[0].entry.id : target;
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
// If it already looks like an ID, keep it.
|
|
33
35
|
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
|
|
37
|
+
// Iterative DFS to avoid call stack overflows on deep histories.
|
|
38
|
+
const stack: SessionTreeNode[] = [...(sm.getTree() as unknown as SessionTreeNode[])];
|
|
39
|
+
while (stack.length > 0) {
|
|
40
|
+
const n = stack.pop()!;
|
|
41
|
+
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
42
|
+
if (n.children?.length) stack.push(...n.children);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback: let SessionManager deal with invalid targets downstream.
|
|
46
|
+
return target;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
const ContextLogParams = Type.Object({
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const formatTokens = (n: number) => {
|
|
1
|
+
export const formatTokens = (n: number | null | undefined) => {
|
|
2
|
+
if (n == null) return "N/A";
|
|
2
3
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
3
4
|
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
4
5
|
return n.toString();
|