mini-coder 0.5.11 → 0.5.13
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/BENCHMARK.md +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- package/src/plugins.ts +0 -183
package/src/session.ts
CHANGED
|
@@ -10,14 +10,29 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Database } from "bun:sqlite";
|
|
13
|
-
import type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
|
|
14
|
+
import {
|
|
15
|
+
getAssistantUsage,
|
|
16
|
+
isUiMessage,
|
|
17
|
+
type PersistedMessage,
|
|
18
|
+
parsePersistedMessage,
|
|
19
|
+
readFirstUserPreview,
|
|
20
|
+
type UiInfoFormat,
|
|
21
|
+
type UiInfoMessage,
|
|
22
|
+
type UiMessage,
|
|
23
|
+
type UiTodoMessage,
|
|
24
|
+
} from "./session-message.ts";
|
|
19
25
|
import type { TodoItem } from "./tools.ts";
|
|
20
26
|
|
|
27
|
+
export type {
|
|
28
|
+
PersistedMessage,
|
|
29
|
+
UiInfoFormat,
|
|
30
|
+
UiInfoMessage,
|
|
31
|
+
UiMessage,
|
|
32
|
+
UiTodoMessage,
|
|
33
|
+
};
|
|
34
|
+
export { getAssistantUsage };
|
|
35
|
+
|
|
21
36
|
// ---------------------------------------------------------------------------
|
|
22
37
|
// Types
|
|
23
38
|
// ---------------------------------------------------------------------------
|
|
@@ -95,41 +110,6 @@ interface AppendPromptHistoryOpts {
|
|
|
95
110
|
sessionId?: string;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
/** Rich-text format hints supported by persisted UI info messages. */
|
|
99
|
-
export type UiInfoFormat = "markdown";
|
|
100
|
-
|
|
101
|
-
/** A persisted UI-only info message shown in the conversation log. */
|
|
102
|
-
export interface UiInfoMessage {
|
|
103
|
-
/** Identifies this as an internal UI message. */
|
|
104
|
-
role: "ui";
|
|
105
|
-
/** UI message category for rendering and future behavior. */
|
|
106
|
-
kind: "info";
|
|
107
|
-
/** Display text shown in the conversation log. */
|
|
108
|
-
content: string;
|
|
109
|
-
/** Optional rich-text format hint for the content. */
|
|
110
|
-
format?: UiInfoFormat;
|
|
111
|
-
/** Unix timestamp in milliseconds. */
|
|
112
|
-
timestamp: number;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** A persisted UI-only todo snapshot shown in the conversation log. */
|
|
116
|
-
export interface UiTodoMessage {
|
|
117
|
-
/** Identifies this as an internal UI message. */
|
|
118
|
-
role: "ui";
|
|
119
|
-
/** UI message category for rendering and future behavior. */
|
|
120
|
-
kind: "todo";
|
|
121
|
-
/** Todo snapshot rendered in the conversation pane. */
|
|
122
|
-
todos: TodoItem[];
|
|
123
|
-
/** Unix timestamp in milliseconds. */
|
|
124
|
-
timestamp: number;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** A persisted UI-only message shown in the conversation log. */
|
|
128
|
-
export type UiMessage = UiInfoMessage | UiTodoMessage;
|
|
129
|
-
|
|
130
|
-
/** Any message persisted in session history. */
|
|
131
|
-
export type PersistedMessage = Message | UiMessage;
|
|
132
|
-
|
|
133
113
|
/** Options for creating a new session. */
|
|
134
114
|
interface CreateSessionOpts {
|
|
135
115
|
/** Working directory to scope the session to. */
|
|
@@ -166,21 +146,6 @@ type MaxTurnRow = { max_turn: number | null };
|
|
|
166
146
|
/** Row shape for `SELECT data` queries. */
|
|
167
147
|
type DataRow = { data: string };
|
|
168
148
|
|
|
169
|
-
const EMPTY_ASSISTANT_USAGE: AssistantMessage["usage"] = {
|
|
170
|
-
input: 0,
|
|
171
|
-
output: 0,
|
|
172
|
-
cacheRead: 0,
|
|
173
|
-
cacheWrite: 0,
|
|
174
|
-
totalTokens: 0,
|
|
175
|
-
cost: {
|
|
176
|
-
input: 0,
|
|
177
|
-
output: 0,
|
|
178
|
-
cacheRead: 0,
|
|
179
|
-
cacheWrite: 0,
|
|
180
|
-
total: 0,
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
|
|
184
149
|
const SQLITE_BUSY_TIMEOUT_MS = 1_000;
|
|
185
150
|
|
|
186
151
|
/** Row shape returned by `SELECT * FROM prompt_history`. */
|
|
@@ -338,240 +303,6 @@ export function getSession(db: Database, id: string): Session | null {
|
|
|
338
303
|
};
|
|
339
304
|
}
|
|
340
305
|
|
|
341
|
-
/** Collapse preview text into a single readable line. */
|
|
342
|
-
function collapsePreviewText(text: string): string | null {
|
|
343
|
-
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
344
|
-
return collapsed.length > 0 ? collapsed : null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function getMultipartUserPreview(
|
|
348
|
-
content: Extract<Message, { role: "user" }>["content"],
|
|
349
|
-
): string | null {
|
|
350
|
-
if (typeof content === "string") {
|
|
351
|
-
return collapsePreviewText(content);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const text = content
|
|
355
|
-
.filter(
|
|
356
|
-
(block): block is Extract<(typeof content)[number], { type: "text" }> => {
|
|
357
|
-
return block.type === "text";
|
|
358
|
-
},
|
|
359
|
-
)
|
|
360
|
-
.map((block) => block.text)
|
|
361
|
-
.join(" ");
|
|
362
|
-
|
|
363
|
-
return collapsePreviewText(text);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function isTextContentBlock(
|
|
367
|
-
value: unknown,
|
|
368
|
-
): value is { type: "text"; text: string } {
|
|
369
|
-
const record = toRecord(value);
|
|
370
|
-
return record?.type === "text" && typeof record.text === "string";
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function isImageContentBlock(
|
|
374
|
-
value: unknown,
|
|
375
|
-
): value is { type: "image"; data: string; mimeType: string } {
|
|
376
|
-
const record = toRecord(value);
|
|
377
|
-
return (
|
|
378
|
-
record?.type === "image" &&
|
|
379
|
-
typeof record.data === "string" &&
|
|
380
|
-
typeof record.mimeType === "string"
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function isThinkingContentBlock(
|
|
385
|
-
value: unknown,
|
|
386
|
-
): value is Extract<AssistantMessage["content"][number], { type: "thinking" }> {
|
|
387
|
-
const record = toRecord(value);
|
|
388
|
-
return record?.type === "thinking" && typeof record.thinking === "string";
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function isToolCallContentBlock(
|
|
392
|
-
value: unknown,
|
|
393
|
-
): value is Extract<AssistantMessage["content"][number], { type: "toolCall" }> {
|
|
394
|
-
const record = toRecord(value);
|
|
395
|
-
return (
|
|
396
|
-
record?.type === "toolCall" &&
|
|
397
|
-
typeof record.id === "string" &&
|
|
398
|
-
typeof record.name === "string" &&
|
|
399
|
-
toRecord(record.arguments) !== null
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function isAssistantUsage(value: unknown): value is AssistantMessage["usage"] {
|
|
404
|
-
const usageRecord = toRecord(value);
|
|
405
|
-
const costRecord = toRecord(usageRecord?.cost);
|
|
406
|
-
return (
|
|
407
|
-
usageRecord !== null &&
|
|
408
|
-
costRecord !== null &&
|
|
409
|
-
readFiniteNumber(usageRecord, "input") !== null &&
|
|
410
|
-
readFiniteNumber(usageRecord, "output") !== null &&
|
|
411
|
-
readFiniteNumber(usageRecord, "cacheRead") !== null &&
|
|
412
|
-
readFiniteNumber(usageRecord, "cacheWrite") !== null &&
|
|
413
|
-
readFiniteNumber(usageRecord, "totalTokens") !== null &&
|
|
414
|
-
readFiniteNumber(costRecord, "input") !== null &&
|
|
415
|
-
readFiniteNumber(costRecord, "output") !== null &&
|
|
416
|
-
readFiniteNumber(costRecord, "cacheRead") !== null &&
|
|
417
|
-
readFiniteNumber(costRecord, "cacheWrite") !== null &&
|
|
418
|
-
readFiniteNumber(costRecord, "total") !== null
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function isStopReason(value: unknown): value is AssistantMessage["stopReason"] {
|
|
423
|
-
return (
|
|
424
|
-
value === "stop" ||
|
|
425
|
-
value === "length" ||
|
|
426
|
-
value === "toolUse" ||
|
|
427
|
-
value === "error" ||
|
|
428
|
-
value === "aborted"
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function isUserMessageRecord(value: unknown): value is UserMessage {
|
|
433
|
-
const record = toRecord(value);
|
|
434
|
-
if (!record || record.role !== "user") {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
readFiniteNumber(record, "timestamp") !== null &&
|
|
440
|
-
(typeof record.content === "string" ||
|
|
441
|
-
(Array.isArray(record.content) &&
|
|
442
|
-
record.content.every(
|
|
443
|
-
(block) => isTextContentBlock(block) || isImageContentBlock(block),
|
|
444
|
-
)))
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function parseAssistantMessageRecord(value: unknown): AssistantMessage | null {
|
|
449
|
-
const record = toRecord(value);
|
|
450
|
-
if (!record || record.role !== "assistant") {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const timestamp = readFiniteNumber(record, "timestamp");
|
|
455
|
-
if (
|
|
456
|
-
!Array.isArray(record.content) ||
|
|
457
|
-
!record.content.every(
|
|
458
|
-
(block) =>
|
|
459
|
-
isTextContentBlock(block) ||
|
|
460
|
-
isThinkingContentBlock(block) ||
|
|
461
|
-
isToolCallContentBlock(block),
|
|
462
|
-
) ||
|
|
463
|
-
typeof record.api !== "string" ||
|
|
464
|
-
typeof record.provider !== "string" ||
|
|
465
|
-
typeof record.model !== "string" ||
|
|
466
|
-
!isStopReason(record.stopReason) ||
|
|
467
|
-
(record.errorMessage !== undefined &&
|
|
468
|
-
typeof record.errorMessage !== "string") ||
|
|
469
|
-
timestamp === null
|
|
470
|
-
) {
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
role: "assistant",
|
|
476
|
-
content: record.content,
|
|
477
|
-
api: record.api,
|
|
478
|
-
provider: record.provider,
|
|
479
|
-
model: record.model,
|
|
480
|
-
usage: isAssistantUsage(record.usage)
|
|
481
|
-
? record.usage
|
|
482
|
-
: structuredClone(EMPTY_ASSISTANT_USAGE),
|
|
483
|
-
stopReason: record.stopReason,
|
|
484
|
-
...(typeof record.errorMessage === "string"
|
|
485
|
-
? { errorMessage: record.errorMessage }
|
|
486
|
-
: {}),
|
|
487
|
-
timestamp,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function isToolResultMessageRecord(value: unknown): value is ToolResultMessage {
|
|
492
|
-
const record = toRecord(value);
|
|
493
|
-
if (!record || record.role !== "toolResult") {
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return (
|
|
498
|
-
typeof record.toolCallId === "string" &&
|
|
499
|
-
typeof record.toolName === "string" &&
|
|
500
|
-
typeof record.isError === "boolean" &&
|
|
501
|
-
Array.isArray(record.content) &&
|
|
502
|
-
record.content.every(
|
|
503
|
-
(block) => isTextContentBlock(block) || isImageContentBlock(block),
|
|
504
|
-
) &&
|
|
505
|
-
readFiniteNumber(record, "timestamp") !== null
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function isUiMessageRecord(value: unknown): value is UiMessage {
|
|
510
|
-
const record = toRecord(value);
|
|
511
|
-
if (!record || record.role !== "ui") {
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const timestamp = readFiniteNumber(record, "timestamp");
|
|
516
|
-
if (timestamp === null) {
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (record.kind === "info") {
|
|
521
|
-
return (
|
|
522
|
-
typeof record.content === "string" &&
|
|
523
|
-
(record.format === undefined || record.format === "markdown")
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return (
|
|
528
|
-
record.kind === "todo" &&
|
|
529
|
-
Array.isArray(record.todos) &&
|
|
530
|
-
record.todos.every(
|
|
531
|
-
(todo) =>
|
|
532
|
-
typeof todo === "object" &&
|
|
533
|
-
todo !== null &&
|
|
534
|
-
typeof (todo as { content?: unknown }).content === "string" &&
|
|
535
|
-
((todo as { status?: unknown }).status === "pending" ||
|
|
536
|
-
(todo as { status?: unknown }).status === "in_progress" ||
|
|
537
|
-
(todo as { status?: unknown }).status === "completed"),
|
|
538
|
-
)
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function parsePersistedMessage(data: string): PersistedMessage | null {
|
|
543
|
-
let parsed: unknown;
|
|
544
|
-
try {
|
|
545
|
-
parsed = JSON.parse(data) as unknown;
|
|
546
|
-
} catch {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
isUserMessageRecord(parsed) ||
|
|
552
|
-
isToolResultMessageRecord(parsed) ||
|
|
553
|
-
isUiMessageRecord(parsed)
|
|
554
|
-
) {
|
|
555
|
-
return parsed;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return parseAssistantMessageRecord(parsed);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/** Read the first-user preview cached by the session-list query. */
|
|
562
|
-
function readFirstUserPreview(messageData: string | null): string | null {
|
|
563
|
-
if (!messageData) {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const message = parsePersistedMessage(messageData);
|
|
568
|
-
if (!message || message.role !== "user") {
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return getMultipartUserPreview(message.content);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
306
|
/**
|
|
576
307
|
* List sessions for a working directory, most recently updated first.
|
|
577
308
|
*
|
|
@@ -668,16 +399,6 @@ export function createUiTodoMessage(todos: readonly TodoItem[]): UiTodoMessage {
|
|
|
668
399
|
};
|
|
669
400
|
}
|
|
670
401
|
|
|
671
|
-
/**
|
|
672
|
-
* Check whether a persisted message is a UI-only message.
|
|
673
|
-
*
|
|
674
|
-
* @param message - Message to inspect.
|
|
675
|
-
* @returns `true` when the message is a {@link UiMessage}.
|
|
676
|
-
*/
|
|
677
|
-
function isUiMessage(message: PersistedMessage): message is UiMessage {
|
|
678
|
-
return message.role === "ui";
|
|
679
|
-
}
|
|
680
|
-
|
|
681
402
|
/**
|
|
682
403
|
* Filter persisted session history down to model-visible pi-ai messages.
|
|
683
404
|
*
|
|
@@ -692,87 +413,6 @@ export function filterModelMessages(
|
|
|
692
413
|
);
|
|
693
414
|
}
|
|
694
415
|
|
|
695
|
-
function toRecord(value: unknown): Record<string, unknown> | null {
|
|
696
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
697
|
-
? (value as Record<string, unknown>)
|
|
698
|
-
: null;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function readFiniteNumber(
|
|
702
|
-
record: Record<string, unknown>,
|
|
703
|
-
key: string,
|
|
704
|
-
): number | null {
|
|
705
|
-
const value = record[key];
|
|
706
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Return an assistant message's usage when the persisted shape is valid.
|
|
711
|
-
*
|
|
712
|
-
* Session rows are treated as untrusted at runtime because older builds or
|
|
713
|
-
* external tooling may have stored assistant messages without a `usage`
|
|
714
|
-
* payload. Invalid or missing usage is ignored instead of crashing session
|
|
715
|
-
* loading or stats calculations.
|
|
716
|
-
*
|
|
717
|
-
* @param message - Message to inspect.
|
|
718
|
-
* @returns The assistant usage payload, or `null` when it is missing/invalid.
|
|
719
|
-
*/
|
|
720
|
-
export function getAssistantUsage(
|
|
721
|
-
message: PersistedMessage | Message,
|
|
722
|
-
): AssistantMessage["usage"] | null {
|
|
723
|
-
if (message.role !== "assistant") {
|
|
724
|
-
return null;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const messageRecord = toRecord(message);
|
|
728
|
-
const usageRecord = toRecord(messageRecord?.usage);
|
|
729
|
-
const costRecord = toRecord(usageRecord?.cost);
|
|
730
|
-
if (!usageRecord || !costRecord) {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const input = readFiniteNumber(usageRecord, "input");
|
|
735
|
-
const output = readFiniteNumber(usageRecord, "output");
|
|
736
|
-
const cacheRead = readFiniteNumber(usageRecord, "cacheRead");
|
|
737
|
-
const cacheWrite = readFiniteNumber(usageRecord, "cacheWrite");
|
|
738
|
-
const totalTokens = readFiniteNumber(usageRecord, "totalTokens");
|
|
739
|
-
const costInput = readFiniteNumber(costRecord, "input");
|
|
740
|
-
const costOutput = readFiniteNumber(costRecord, "output");
|
|
741
|
-
const costCacheRead = readFiniteNumber(costRecord, "cacheRead");
|
|
742
|
-
const costCacheWrite = readFiniteNumber(costRecord, "cacheWrite");
|
|
743
|
-
const costTotal = readFiniteNumber(costRecord, "total");
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
input === null ||
|
|
747
|
-
output === null ||
|
|
748
|
-
cacheRead === null ||
|
|
749
|
-
cacheWrite === null ||
|
|
750
|
-
totalTokens === null ||
|
|
751
|
-
costInput === null ||
|
|
752
|
-
costOutput === null ||
|
|
753
|
-
costCacheRead === null ||
|
|
754
|
-
costCacheWrite === null ||
|
|
755
|
-
costTotal === null
|
|
756
|
-
) {
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return {
|
|
761
|
-
input,
|
|
762
|
-
output,
|
|
763
|
-
cacheRead,
|
|
764
|
-
cacheWrite,
|
|
765
|
-
totalTokens,
|
|
766
|
-
cost: {
|
|
767
|
-
input: costInput,
|
|
768
|
-
output: costOutput,
|
|
769
|
-
cacheRead: costCacheRead,
|
|
770
|
-
cacheWrite: costCacheWrite,
|
|
771
|
-
total: costTotal,
|
|
772
|
-
},
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
416
|
function runInImmediateTransaction<T>(db: Database, callback: () => T): T {
|
|
777
417
|
if (db.inTransaction) {
|
|
778
418
|
return callback();
|
|
@@ -1080,17 +720,6 @@ export function forkSession(db: Database, sourceId: string): Session {
|
|
|
1080
720
|
// Stats
|
|
1081
721
|
// ---------------------------------------------------------------------------
|
|
1082
722
|
|
|
1083
|
-
/**
|
|
1084
|
-
* Compute cumulative token and cost statistics from a message history.
|
|
1085
|
-
*
|
|
1086
|
-
* Iterates over the messages, summing `usage` fields from assistant messages
|
|
1087
|
-
* only (user and tool-result messages do not carry usage data). This is
|
|
1088
|
-
* designed to be called once on session load, with the result maintained
|
|
1089
|
-
* in-memory via a running accumulator during the session.
|
|
1090
|
-
*
|
|
1091
|
-
* @param messages - The full persisted message history for a session.
|
|
1092
|
-
* @returns Aggregated {@link SessionStats}.
|
|
1093
|
-
*/
|
|
1094
723
|
/**
|
|
1095
724
|
* Add one persisted message's assistant usage to cumulative session stats.
|
|
1096
725
|
*
|
|
@@ -1117,14 +746,25 @@ export function addMessageToStats(
|
|
|
1117
746
|
};
|
|
1118
747
|
}
|
|
1119
748
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
let stats: SessionStats = {
|
|
749
|
+
/** Create a zeroed cumulative session-stats object. */
|
|
750
|
+
export function createEmptySessionStats(): SessionStats {
|
|
751
|
+
return {
|
|
1124
752
|
totalInput: 0,
|
|
1125
753
|
totalOutput: 0,
|
|
1126
754
|
totalCost: 0,
|
|
1127
755
|
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Compute cumulative token and cost statistics from a message history.
|
|
760
|
+
*
|
|
761
|
+
* @param messages - Full persisted session history.
|
|
762
|
+
* @returns Aggregated cumulative session stats.
|
|
763
|
+
*/
|
|
764
|
+
export function computeStats(
|
|
765
|
+
messages: readonly PersistedMessage[],
|
|
766
|
+
): SessionStats {
|
|
767
|
+
let stats = createEmptySessionStats();
|
|
1128
768
|
|
|
1129
769
|
for (const message of messages) {
|
|
1130
770
|
stats = addMessageToStats(stats, message);
|
|
@@ -1282,3 +922,69 @@ export function computeContextTokens(
|
|
|
1282
922
|
|
|
1283
923
|
return contextTokens;
|
|
1284
924
|
}
|
|
925
|
+
|
|
926
|
+
interface MutableConversationState {
|
|
927
|
+
messages: PersistedMessage[];
|
|
928
|
+
stats: SessionStats;
|
|
929
|
+
contextTokens: number;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Derive the in-memory conversation snapshot for a persisted message history.
|
|
934
|
+
*
|
|
935
|
+
* @param messages - Persisted messages to expose in memory.
|
|
936
|
+
* @returns Message history plus derived stats and context-token estimate.
|
|
937
|
+
*/
|
|
938
|
+
export function createConversationSnapshot(messages: PersistedMessage[] = []): {
|
|
939
|
+
messages: PersistedMessage[];
|
|
940
|
+
stats: SessionStats;
|
|
941
|
+
contextTokens: number;
|
|
942
|
+
} {
|
|
943
|
+
return {
|
|
944
|
+
messages,
|
|
945
|
+
stats: computeStats(messages),
|
|
946
|
+
contextTokens: computeContextTokens(messages),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Replace the current in-memory conversation state from a message history.
|
|
952
|
+
*
|
|
953
|
+
* @param state - Mutable conversation state.
|
|
954
|
+
* @param messages - Replacement persisted message history.
|
|
955
|
+
*/
|
|
956
|
+
export function replaceConversationState<T extends MutableConversationState>(
|
|
957
|
+
state: T,
|
|
958
|
+
messages: PersistedMessage[],
|
|
959
|
+
): void {
|
|
960
|
+
const snapshot = createConversationSnapshot(messages);
|
|
961
|
+
state.messages = snapshot.messages;
|
|
962
|
+
state.stats = snapshot.stats;
|
|
963
|
+
state.contextTokens = snapshot.contextTokens;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Clear the current in-memory conversation state.
|
|
968
|
+
*
|
|
969
|
+
* @param state - Mutable conversation state.
|
|
970
|
+
*/
|
|
971
|
+
export function clearConversationState<T extends MutableConversationState>(
|
|
972
|
+
state: T,
|
|
973
|
+
): void {
|
|
974
|
+
replaceConversationState(state, []);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Append one persisted message to the in-memory conversation state.
|
|
979
|
+
*
|
|
980
|
+
* @param state - Mutable conversation state.
|
|
981
|
+
* @param message - Message to append.
|
|
982
|
+
*/
|
|
983
|
+
export function appendConversationMessage<T extends MutableConversationState>(
|
|
984
|
+
state: T,
|
|
985
|
+
message: PersistedMessage,
|
|
986
|
+
): void {
|
|
987
|
+
state.messages.push(message);
|
|
988
|
+
state.stats = addMessageToStats(state.stats, message);
|
|
989
|
+
state.contextTokens = addMessageToContextTokens(state.contextTokens, message);
|
|
990
|
+
}
|