@myagentroam/agent 0.11.59 → 0.11.60

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.
@@ -2,6 +2,7 @@ import type { AgentEvent } from '../runtime/events.js';
2
2
  import type { SessionStore } from './store/contracts.js';
3
3
  import type { SessionListInput } from './types.js';
4
4
  import type { AgentSessionRollbackInput } from './rollback.js';
5
+ import { type AgentSessionContextUsage } from './context-usage.js';
5
6
  import type { AgentSessionSummary, AgentSessionPage } from './summary.js';
6
7
  export type { AgentSessionSummary, AgentSessionPage } from './summary.js';
7
8
  import type { AgentSessionHistoryImageMetadata, AgentUserHistoryEntry } from './user-history.js';
@@ -23,12 +24,7 @@ export interface AgentSessionInputReceipt {
23
24
  readonly appliedExecutionId?: string;
24
25
  readonly appliedTurnId?: string;
25
26
  }
26
- export interface AgentSessionContextUsage {
27
- inputTokens: number;
28
- gcCount: number;
29
- /** Measured input decreases since Compact; undefined when GC usage is not yet paired. */
30
- gcReclaimedTokens?: number;
31
- }
27
+ export type { AgentSessionContextUsage } from './context-usage.js';
32
28
  export interface AgentSessionDeleteResult {
33
29
  readonly deleted: boolean;
34
30
  }
@@ -1,10 +1,12 @@
1
1
  import { AgentError } from '../error.js';
2
2
  import { publicHostedToolEvents } from '../runtime/event-projection.js';
3
3
  import { appliedInputRecord } from './deferred-input.js';
4
- import { compactSummaryFromCheckpoint } from './record-projection.js';
4
+ import { isAgentEvent } from './record-projection.js';
5
+ import { SessionContextUsageReader } from './context-usage.js';
5
6
  const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
6
7
  const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
7
8
  export function createSessionCatalog(store, borrowed) {
9
+ const contextUsage = new SessionContextUsageReader(store);
8
10
  let closure;
9
11
  const assertOpen = () => {
10
12
  if (closure)
@@ -182,103 +184,7 @@ export function createSessionCatalog(store, borrowed) {
182
184
  },
183
185
  contextUsage: async (sessionId) => {
184
186
  assertOpen();
185
- const page = await store.readPage(sessionId, { limit: 1000 });
186
- let inputTokens;
187
- for (const record of page.records.toReversed()) {
188
- if (!isAgentEvent(record.payload))
189
- continue;
190
- if (record.payload.type === 'context.compacted') {
191
- if (record.payload.retainedContext === true)
192
- return undefined;
193
- inputTokens = 0;
194
- break;
195
- }
196
- if (record.payload.type === 'usage.updated' && record.payload.purpose !== 'compact') {
197
- const tokens = record.payload.contextInputTokens ?? record.payload.inputTokens;
198
- if (typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens >= 0) {
199
- inputTokens = tokens;
200
- break;
201
- }
202
- }
203
- }
204
- if (inputTokens === undefined)
205
- return undefined;
206
- const usage = { inputTokens };
207
- let gcCount = 0;
208
- let gcReclaimedTokens = 0;
209
- let tokensKnown = true;
210
- let nextInputTokens;
211
- let nextModelId;
212
- let nextExecutionId;
213
- let requestInputTokens;
214
- const statistics = () => ({
215
- ...usage,
216
- gcCount,
217
- ...(tokensKnown ? { gcReclaimedTokens } : {})
218
- });
219
- let countPage = page;
220
- for (;;) {
221
- for (const record of countPage.records.toReversed()) {
222
- if (compactSummaryFromCheckpoint(record) !== undefined)
223
- return statistics();
224
- if (record.type === 'execution.header' &&
225
- record.executionId === nextExecutionId &&
226
- isRecord(record.payload))
227
- nextModelId =
228
- typeof record.payload.modelId === 'string' ? record.payload.modelId : undefined;
229
- if (isAgentEvent(record.payload) &&
230
- record.payload.type === 'usage.updated' &&
231
- record.payload.purpose !== 'compact') {
232
- const tokens = record.payload.contextInputTokens ?? record.payload.inputTokens;
233
- if (typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens >= 0) {
234
- requestInputTokens ??= tokens;
235
- nextInputTokens = tokens;
236
- nextModelId = undefined;
237
- nextExecutionId = record.executionId;
238
- }
239
- }
240
- if (record.type === 'model.attempt' &&
241
- isRecord(record.payload) &&
242
- record.payload.state === 'STARTED' &&
243
- record.payload.purpose === 'EXECUTION') {
244
- // Reverse traversal preserves the last usage update within each request.
245
- if (requestInputTokens !== undefined)
246
- nextInputTokens = requestInputTokens;
247
- requestInputTokens = undefined;
248
- }
249
- if (record.type === 'context.gc.completed' &&
250
- isRecord(record.payload) &&
251
- typeof record.payload.generation === 'number' &&
252
- Number.isSafeInteger(record.payload.generation) &&
253
- record.payload.generation > 0) {
254
- gcCount ||= record.payload.generation;
255
- const trigger = isRecord(record.payload.trigger) ? record.payload.trigger : undefined;
256
- const before = trigger?.contextInputTokens;
257
- if (typeof trigger?.usageSequence === 'number' &&
258
- trigger.usageSequence > 0 &&
259
- typeof before === 'number' &&
260
- Number.isSafeInteger(before) &&
261
- before >= 0 &&
262
- nextInputTokens !== undefined &&
263
- (nextModelId === undefined || nextModelId === trigger.modelId))
264
- gcReclaimedTokens += Math.max(0, before - nextInputTokens);
265
- else
266
- tokensKnown = false;
267
- nextInputTokens = undefined;
268
- nextModelId = undefined;
269
- nextExecutionId = undefined;
270
- requestInputTokens = undefined;
271
- if (record.payload.generation === 1)
272
- return statistics();
273
- }
274
- }
275
- if (countPage.previousBeforeSequence === undefined)
276
- return statistics();
277
- countPage = await store.readPage(sessionId, {
278
- limit: 1000,
279
- beforeSequence: countPage.previousBeforeSequence
280
- });
281
- }
187
+ return contextUsage.read(sessionId);
282
188
  },
283
189
  fork: async (sessionId, options) => {
284
190
  assertOpen();
@@ -325,7 +231,11 @@ export function createSessionCatalog(store, borrowed) {
325
231
  }
326
232
  },
327
233
  close: () => {
328
- closure ??= Promise.resolve().then(() => (borrowed ? undefined : store.close()));
234
+ closure ??= Promise.resolve().then(async () => {
235
+ await contextUsage.close();
236
+ if (!borrowed)
237
+ await store.close();
238
+ });
329
239
  return closure;
330
240
  }
331
241
  };
@@ -367,13 +277,3 @@ export function imageMetadata(value) {
367
277
  ]
368
278
  : []);
369
279
  }
370
- function isAgentEvent(value) {
371
- return (isRecord(value) &&
372
- typeof value.type === 'string' &&
373
- value.type !== 'reasoning.summary' &&
374
- typeof value.eventId === 'string' &&
375
- typeof value.sequence === 'number' &&
376
- typeof value.timestamp === 'string' &&
377
- typeof value.sessionId === 'string' &&
378
- typeof value.executionId === 'string');
379
- }
@@ -0,0 +1,19 @@
1
+ import type { SessionStore } from './store/contracts.js';
2
+ export interface AgentSessionContextUsage {
3
+ inputTokens: number;
4
+ gcCount: number;
5
+ /** Measured input decreases since Compact; undefined when GC usage is not yet paired. */
6
+ gcReclaimedTokens?: number;
7
+ }
8
+ /** Rebuild GC totals only when GC, its measured request, or retained history changes. */
9
+ export declare class SessionContextUsageReader {
10
+ private readonly store;
11
+ private readonly cache;
12
+ private readonly reading;
13
+ constructor(store: Pick<SessionStore, 'readPage'>);
14
+ read(sessionId: string): Promise<AgentSessionContextUsage | undefined>;
15
+ close(): Promise<void>;
16
+ private remember;
17
+ private reusable;
18
+ private readOnce;
19
+ }
@@ -0,0 +1,202 @@
1
+ import { compactSummaryFromCheckpoint, isAgentEvent } from './record-projection.js';
2
+ const MAX_CACHED_SESSIONS = 256;
3
+ /** Rebuild GC totals only when GC, its measured request, or retained history changes. */
4
+ export class SessionContextUsageReader {
5
+ store;
6
+ cache = new Map();
7
+ reading = new Map();
8
+ constructor(store) {
9
+ this.store = store;
10
+ }
11
+ async read(sessionId) {
12
+ let pending = this.reading.get(sessionId);
13
+ if (!pending) {
14
+ pending = this.readOnce(sessionId);
15
+ this.reading.set(sessionId, pending);
16
+ }
17
+ try {
18
+ const result = await pending;
19
+ return result === undefined ? undefined : { ...result };
20
+ }
21
+ finally {
22
+ if (this.reading.get(sessionId) === pending)
23
+ this.reading.delete(sessionId);
24
+ }
25
+ }
26
+ async close() {
27
+ await Promise.allSettled(this.reading.values());
28
+ this.cache.clear();
29
+ }
30
+ remember(sessionId, entry) {
31
+ this.cache.delete(sessionId);
32
+ this.cache.set(sessionId, entry);
33
+ if (this.cache.size > MAX_CACHED_SESSIONS)
34
+ this.cache.delete(this.cache.keys().next().value);
35
+ }
36
+ async reusable(sessionId, page, cached) {
37
+ for (;;) {
38
+ for (const record of page.records.toReversed()) {
39
+ if (record.sequence <= cached.cursor.sequence)
40
+ return (record.sequence === cached.cursor.sequence && record.recordId === cached.cursor.recordId);
41
+ if (compactSummaryFromCheckpoint(record) !== undefined ||
42
+ record.type === 'context.gc.completed')
43
+ return false;
44
+ if (cached.refreshOnUsage &&
45
+ ((isAgentEvent(record.payload) &&
46
+ record.payload.type === 'usage.updated' &&
47
+ record.payload.purpose !== 'compact') ||
48
+ (record.type === 'model.attempt' &&
49
+ isRecord(record.payload) &&
50
+ record.payload.purpose === 'EXECUTION')))
51
+ return false;
52
+ }
53
+ if (page.previousBeforeSequence === undefined)
54
+ return false;
55
+ page = await this.store.readPage(sessionId, {
56
+ limit: 1000,
57
+ beforeSequence: page.previousBeforeSequence
58
+ });
59
+ }
60
+ }
61
+ async readOnce(sessionId) {
62
+ const page = await this.store.readPage(sessionId, { limit: 1000 });
63
+ let inputTokens;
64
+ for (const record of page.records.toReversed()) {
65
+ if (!isAgentEvent(record.payload))
66
+ continue;
67
+ if (record.payload.type === 'context.compacted') {
68
+ if (record.payload.retainedContext === true)
69
+ return undefined;
70
+ inputTokens = 0;
71
+ break;
72
+ }
73
+ if (record.payload.type === 'usage.updated' && record.payload.purpose !== 'compact') {
74
+ const tokens = record.payload.contextInputTokens ?? record.payload.inputTokens;
75
+ if (typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens >= 0) {
76
+ inputTokens = tokens;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ if (inputTokens === undefined)
82
+ return undefined;
83
+ const usage = { inputTokens };
84
+ const tail = page.records.at(-1);
85
+ const cursor = { sequence: tail.sequence, recordId: tail.recordId };
86
+ const cached = this.cache.get(sessionId);
87
+ if (cached && (await this.reusable(sessionId, page, cached))) {
88
+ this.remember(sessionId, { ...cached, cursor });
89
+ return { ...usage, ...cached.statistics };
90
+ }
91
+ let gcCount = 0;
92
+ let gcReclaimedTokens = 0;
93
+ let tokensKnown = true;
94
+ let nextInputTokens;
95
+ let nextModelId;
96
+ let nextExecutionId;
97
+ let requestInputTokens;
98
+ let requestEnded = false;
99
+ let newerRequestStarted = false;
100
+ let nextInputMutable = false;
101
+ let refreshOnUsage = false;
102
+ let permanentlyUnavailable = false;
103
+ const statistics = () => {
104
+ const statistics = { gcCount, ...(tokensKnown ? { gcReclaimedTokens } : {}) };
105
+ this.remember(sessionId, {
106
+ cursor,
107
+ statistics,
108
+ refreshOnUsage: refreshOnUsage && !permanentlyUnavailable
109
+ });
110
+ return { ...usage, ...statistics };
111
+ };
112
+ let countPage = page;
113
+ for (;;) {
114
+ for (const record of countPage.records.toReversed()) {
115
+ if (compactSummaryFromCheckpoint(record) !== undefined)
116
+ return statistics();
117
+ if (record.type === 'execution.header' &&
118
+ record.executionId === nextExecutionId &&
119
+ isRecord(record.payload))
120
+ nextModelId =
121
+ typeof record.payload.modelId === 'string' ? record.payload.modelId : undefined;
122
+ if (isAgentEvent(record.payload) &&
123
+ record.payload.type === 'usage.updated' &&
124
+ record.payload.purpose !== 'compact') {
125
+ const tokens = record.payload.contextInputTokens ?? record.payload.inputTokens;
126
+ if (typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens >= 0) {
127
+ requestInputTokens ??= tokens;
128
+ nextInputTokens = tokens;
129
+ nextModelId = undefined;
130
+ nextExecutionId = record.executionId;
131
+ nextInputMutable = false;
132
+ }
133
+ }
134
+ if (record.type === 'model.attempt' &&
135
+ isRecord(record.payload) &&
136
+ record.payload.purpose === 'EXECUTION' &&
137
+ (record.payload.state === 'SUCCEEDED' || record.payload.state === 'FAILED'))
138
+ requestEnded = true;
139
+ if (record.type === 'model.attempt' &&
140
+ isRecord(record.payload) &&
141
+ record.payload.state === 'STARTED' &&
142
+ record.payload.purpose === 'EXECUTION') {
143
+ // Reverse traversal preserves the last usage update within each request.
144
+ if (requestInputTokens !== undefined) {
145
+ nextInputTokens = requestInputTokens;
146
+ nextInputMutable = !requestEnded && !newerRequestStarted;
147
+ }
148
+ requestInputTokens = undefined;
149
+ requestEnded = false;
150
+ newerRequestStarted = true;
151
+ }
152
+ if (record.type === 'context.gc.completed' &&
153
+ isRecord(record.payload) &&
154
+ typeof record.payload.generation === 'number' &&
155
+ Number.isSafeInteger(record.payload.generation) &&
156
+ record.payload.generation > 0) {
157
+ const latest = gcCount === 0;
158
+ gcCount ||= record.payload.generation;
159
+ const trigger = isRecord(record.payload.trigger) ? record.payload.trigger : undefined;
160
+ const before = trigger?.contextInputTokens;
161
+ const measuredBefore = typeof trigger?.usageSequence === 'number' &&
162
+ trigger.usageSequence > 0 &&
163
+ typeof before === 'number' &&
164
+ Number.isSafeInteger(before) &&
165
+ before >= 0;
166
+ if (measuredBefore &&
167
+ nextInputTokens !== undefined &&
168
+ (nextModelId === undefined || nextModelId === trigger.modelId))
169
+ gcReclaimedTokens += Math.max(0, before - nextInputTokens);
170
+ else
171
+ tokensKnown = false;
172
+ if (!measuredBefore ||
173
+ (!latest && nextInputTokens === undefined) ||
174
+ (nextInputTokens !== undefined &&
175
+ nextModelId !== undefined &&
176
+ nextModelId !== trigger?.modelId))
177
+ permanentlyUnavailable = true;
178
+ if (latest)
179
+ refreshOnUsage = nextInputTokens === undefined || nextInputMutable;
180
+ nextInputTokens = undefined;
181
+ nextModelId = undefined;
182
+ nextExecutionId = undefined;
183
+ requestInputTokens = undefined;
184
+ requestEnded = false;
185
+ newerRequestStarted = false;
186
+ nextInputMutable = false;
187
+ if (record.payload.generation === 1)
188
+ return statistics();
189
+ }
190
+ }
191
+ if (countPage.previousBeforeSequence === undefined)
192
+ return statistics();
193
+ countPage = await this.store.readPage(sessionId, {
194
+ limit: 1000,
195
+ beforeSequence: countPage.previousBeforeSequence
196
+ });
197
+ }
198
+ }
199
+ }
200
+ function isRecord(value) {
201
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
202
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ModelInputMetadata } from '../model/contracts.js';
2
+ import type { AgentEvent } from '../runtime/events.js';
2
3
  import type { CompactHistoryMessage, SessionRecord } from './record-contract.js';
4
+ export declare function isAgentEvent(value: unknown): value is AgentEvent;
3
5
  export declare function compactSummaryFromCheckpoint(record: SessionRecord | undefined): string | undefined;
4
6
  export declare function replacementHistoryFromCheckpoint(record: SessionRecord): CompactHistoryMessage[] | undefined;
5
7
  export declare function parseModelInputMetadata(value: unknown): ModelInputMetadata | undefined;
@@ -1,3 +1,15 @@
1
+ export function isAgentEvent(value) {
2
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
3
+ return false;
4
+ const event = value;
5
+ return (typeof event.type === 'string' &&
6
+ event.type !== 'reasoning.summary' &&
7
+ typeof event.eventId === 'string' &&
8
+ typeof event.sequence === 'number' &&
9
+ typeof event.timestamp === 'string' &&
10
+ typeof event.sessionId === 'string' &&
11
+ typeof event.executionId === 'string');
12
+ }
1
13
  export function compactSummaryFromCheckpoint(record) {
2
14
  if (record?.type !== 'compact.completed')
3
15
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.59",
3
+ "version": "0.11.60",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",