@monotykamary/pi-ledger 0.2.0 → 0.4.0

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.
@@ -0,0 +1,890 @@
1
+ import { AgentSession } from '@earendil-works/pi-coding-agent';
2
+
3
+ const REGISTRY_SYMBOL = Symbol.for('@monotykamary/pi-ledger/in-process-agent-session-observer/v1');
4
+ const REGISTRY_BRAND = '@monotykamary/pi-ledger/in-process-agent-session-observer/v1';
5
+ const PATCH_BRAND = `${REGISTRY_BRAND}/patch`;
6
+
7
+ export interface InProcessModelRef {
8
+ readonly provider?: string;
9
+ readonly modelId?: string;
10
+ }
11
+
12
+ export interface InProcessUsageCost {
13
+ readonly input: number;
14
+ readonly output: number;
15
+ readonly cacheRead: number;
16
+ readonly cacheWrite: number;
17
+ readonly total: number;
18
+ }
19
+
20
+ export interface InProcessAssistantUsage {
21
+ readonly input: number;
22
+ readonly output: number;
23
+ readonly cacheRead: number;
24
+ readonly cacheWrite: number;
25
+ readonly reasoning?: number;
26
+ readonly totalTokens: number;
27
+ readonly cost?: InProcessUsageCost;
28
+ }
29
+
30
+ export interface InProcessSessionMetadata {
31
+ readonly sessionId?: string;
32
+ readonly sessionFile?: string;
33
+ readonly model?: InProcessModelRef;
34
+ readonly responseId?: string;
35
+ }
36
+
37
+ export interface InProcessAssistantUsageEvent extends InProcessSessionMetadata {
38
+ readonly type: 'assistant_usage';
39
+ readonly timestamp: number;
40
+ readonly stopReason?: string;
41
+ readonly usage: InProcessAssistantUsage;
42
+ }
43
+
44
+ export interface InProcessToolCallRef {
45
+ readonly toolCallId: string;
46
+ readonly toolName?: string;
47
+ }
48
+
49
+ export interface InProcessToolIntervalEvent extends InProcessSessionMetadata {
50
+ readonly type: 'tool_interval';
51
+ readonly startedAt: number;
52
+ readonly endedAt: number;
53
+ readonly durationMs: number;
54
+ readonly toolCalls: readonly InProcessToolCallRef[];
55
+ }
56
+
57
+ export type InProcessObserverErrorPhase =
58
+ | 'install'
59
+ | 'attach'
60
+ | 'metadata'
61
+ | 'event'
62
+ | 'callback'
63
+ | 'uninstall';
64
+
65
+ export interface InProcessObserverError {
66
+ readonly phase: InProcessObserverErrorPhase;
67
+ readonly error: unknown;
68
+ }
69
+
70
+ export interface InProcessAgentSessionObserverCallbacks {
71
+ onAssistantUsage?: (event: InProcessAssistantUsageEvent) => void | Promise<void>;
72
+ onToolInterval?: (event: InProcessToolIntervalEvent) => void | Promise<void>;
73
+ onError?: (error: InProcessObserverError) => void | Promise<void>;
74
+ }
75
+
76
+ export interface AgentSessionClassLike {
77
+ readonly prototype: object;
78
+ }
79
+
80
+ export interface InProcessAgentSessionObserverDependencies {
81
+ readonly AgentSession: AgentSessionClassLike;
82
+ readonly now: () => number;
83
+ }
84
+
85
+ export interface InProcessAgentSessionObserverOptions extends InProcessAgentSessionObserverCallbacks {
86
+ readonly rootSessionIds?: Iterable<string>;
87
+ readonly dependencies?: Partial<InProcessAgentSessionObserverDependencies>;
88
+ }
89
+
90
+ export type InProcessObserverIncompatibility =
91
+ | 'global-registry'
92
+ | 'agent-session-prototype'
93
+ | 'prompt-descriptor'
94
+ | 'subscribe-method'
95
+ | 'prototype-conflict'
96
+ | 'prototype-patch';
97
+
98
+ export interface InProcessAgentSessionObserverHandle {
99
+ readonly installed: boolean;
100
+ readonly incompatibility?: InProcessObserverIncompatibility;
101
+ addRootSessionId(sessionId: string): void;
102
+ removeRootSessionId(sessionId: string): void;
103
+ uninstall(): void;
104
+ }
105
+
106
+ type PromptFunction = (this: unknown, ...args: unknown[]) => unknown;
107
+ type SessionEventListener = (event: unknown) => void;
108
+
109
+ type ObserverCallbacks = Readonly<InProcessAgentSessionObserverCallbacks>;
110
+
111
+ interface Subscriber {
112
+ readonly callbacks: ObserverCallbacks;
113
+ readonly roots: Set<string>;
114
+ }
115
+
116
+ interface SessionMetadata {
117
+ readonly sessionId?: string;
118
+ readonly sessionFile?: string;
119
+ readonly model?: InProcessModelRef;
120
+ }
121
+
122
+ interface SessionObservation {
123
+ generation: number;
124
+ subscribed: boolean;
125
+ unsubscribe?: () => void;
126
+ sessionRef: WeakRef<object>;
127
+ readonly listener: SessionEventListener;
128
+ readonly activeTools: Map<string, string | undefined>;
129
+ readonly unionToolCalls: Map<string, string | undefined>;
130
+ unionStartedAt?: number;
131
+ unionMetadata?: InProcessSessionMetadata;
132
+ lastModel?: InProcessModelRef;
133
+ lastResponseId?: string;
134
+ }
135
+
136
+ interface PatchState {
137
+ readonly brand: typeof PATCH_BRAND;
138
+ readonly prototype: object;
139
+ readonly originalDescriptor: PropertyDescriptor;
140
+ readonly wrappedDescriptor: PropertyDescriptor;
141
+ active: boolean;
142
+ generation: number;
143
+ now: () => number;
144
+ readonly subscribers: Map<symbol, Subscriber>;
145
+ readonly rootSessionIds: Map<string, number>;
146
+ readonly sessions: WeakMap<object, SessionObservation>;
147
+ readonly attachmentRefs: Set<WeakRef<SessionObservation>>;
148
+ readonly attachmentFinalizer: FinalizationRegistry<WeakRef<SessionObservation>>;
149
+ }
150
+
151
+ interface GlobalRegistry {
152
+ readonly brand: typeof REGISTRY_BRAND;
153
+ readonly patches: WeakMap<object, PatchState>;
154
+ }
155
+
156
+ function isObject(value: unknown): value is object {
157
+ return (typeof value === 'object' && value !== null) || typeof value === 'function';
158
+ }
159
+
160
+ function asRecord(value: unknown): Record<PropertyKey, unknown> | undefined {
161
+ return isObject(value) ? (value as Record<PropertyKey, unknown>) : undefined;
162
+ }
163
+
164
+ function asString(value: unknown): string | undefined {
165
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
166
+ }
167
+
168
+ function asFiniteNumber(value: unknown): number | undefined {
169
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
170
+ }
171
+
172
+ function nonNegativeNumber(value: unknown): number {
173
+ return Math.max(0, asFiniteNumber(value) ?? 0);
174
+ }
175
+
176
+ function invokeCallback<T>(
177
+ callback: ((value: T) => void | Promise<void>) | undefined,
178
+ value: T,
179
+ onError: (error: unknown) => void
180
+ ): void {
181
+ if (!callback) return;
182
+ try {
183
+ const result = callback(value);
184
+ if (result && typeof result.then === 'function') {
185
+ void Promise.resolve(result).catch(onError);
186
+ }
187
+ } catch (error) {
188
+ onError(error);
189
+ }
190
+ }
191
+
192
+ function reportDirect(
193
+ callback: InProcessAgentSessionObserverCallbacks['onError'],
194
+ phase: InProcessObserverErrorPhase,
195
+ error: unknown
196
+ ): void {
197
+ invokeCallback(callback, Object.freeze({ phase, error }), () => undefined);
198
+ }
199
+
200
+ function reportSubscriber(
201
+ subscriber: Subscriber,
202
+ phase: InProcessObserverErrorPhase,
203
+ error: unknown
204
+ ): void {
205
+ reportDirect(subscriber.callbacks.onError, phase, error);
206
+ }
207
+
208
+ function reportAll(state: PatchState, phase: InProcessObserverErrorPhase, error: unknown): void {
209
+ for (const subscriber of [...state.subscribers.values()]) {
210
+ reportSubscriber(subscriber, phase, error);
211
+ }
212
+ }
213
+
214
+ function isGlobalRegistry(value: unknown): value is GlobalRegistry {
215
+ try {
216
+ const record = asRecord(value);
217
+ return record?.brand === REGISTRY_BRAND && record.patches instanceof WeakMap;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ function getGlobalRegistry(
224
+ onError: InProcessAgentSessionObserverCallbacks['onError']
225
+ ): GlobalRegistry | undefined {
226
+ const holder = globalThis as unknown as Record<PropertyKey, unknown>;
227
+ let existing: unknown;
228
+ try {
229
+ existing = Reflect.get(holder, REGISTRY_SYMBOL);
230
+ } catch (error) {
231
+ reportDirect(onError, 'install', error);
232
+ return undefined;
233
+ }
234
+ if (existing !== undefined) {
235
+ if (isGlobalRegistry(existing)) return existing;
236
+ reportDirect(
237
+ onError,
238
+ 'install',
239
+ new Error('The process-global AgentSession observer registry is incompatible.')
240
+ );
241
+ return undefined;
242
+ }
243
+
244
+ const registry: GlobalRegistry = Object.freeze({
245
+ brand: REGISTRY_BRAND,
246
+ patches: new WeakMap<object, PatchState>(),
247
+ });
248
+ try {
249
+ Object.defineProperty(holder, REGISTRY_SYMBOL, {
250
+ value: registry,
251
+ writable: false,
252
+ enumerable: false,
253
+ configurable: false,
254
+ });
255
+ return registry;
256
+ } catch (error) {
257
+ reportDirect(onError, 'install', error);
258
+ return undefined;
259
+ }
260
+ }
261
+
262
+ function descriptorsEqual(left: PropertyDescriptor, right: PropertyDescriptor): boolean {
263
+ return (
264
+ left.value === right.value &&
265
+ left.get === right.get &&
266
+ left.set === right.set &&
267
+ left.writable === right.writable &&
268
+ left.enumerable === right.enumerable &&
269
+ left.configurable === right.configurable
270
+ );
271
+ }
272
+
273
+ function readDescriptor(prototype: object): PropertyDescriptor | undefined {
274
+ try {
275
+ return Object.getOwnPropertyDescriptor(prototype, 'prompt');
276
+ } catch {
277
+ return undefined;
278
+ }
279
+ }
280
+
281
+ function readPrototypeMethod(prototype: object, name: string): unknown {
282
+ try {
283
+ return Reflect.get(prototype, name);
284
+ } catch {
285
+ return undefined;
286
+ }
287
+ }
288
+
289
+ function isPatchState(value: unknown): value is PatchState {
290
+ try {
291
+ const record = asRecord(value);
292
+ return (
293
+ record?.brand === PATCH_BRAND &&
294
+ isObject(record.prototype) &&
295
+ isObject(record.originalDescriptor) &&
296
+ isObject(record.wrappedDescriptor) &&
297
+ typeof record.active === 'boolean' &&
298
+ typeof record.generation === 'number' &&
299
+ typeof record.now === 'function' &&
300
+ record.subscribers instanceof Map &&
301
+ record.rootSessionIds instanceof Map &&
302
+ record.sessions instanceof WeakMap &&
303
+ record.attachmentRefs instanceof Set &&
304
+ isObject(record.attachmentFinalizer)
305
+ );
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ function createPatchState(
312
+ prototype: object,
313
+ descriptor: PropertyDescriptor,
314
+ now: () => number
315
+ ): PatchState {
316
+ let state: PatchState;
317
+ const originalDescriptor = Object.freeze({ ...descriptor });
318
+ const originalPrompt = originalDescriptor.value as PromptFunction;
319
+ const wrapper: PromptFunction = function observedAgentSessionPrompt(
320
+ this: unknown,
321
+ ...args: unknown[]
322
+ ): unknown {
323
+ if (state.active) {
324
+ try {
325
+ attachSession(state, this);
326
+ } catch (error) {
327
+ reportAll(state, 'attach', error);
328
+ }
329
+ }
330
+ return Reflect.apply(originalPrompt, this, args);
331
+ };
332
+ const wrappedDescriptor = Object.freeze({ ...originalDescriptor, value: wrapper });
333
+ const attachmentRefs = new Set<WeakRef<SessionObservation>>();
334
+
335
+ state = {
336
+ brand: PATCH_BRAND,
337
+ prototype,
338
+ originalDescriptor,
339
+ wrappedDescriptor,
340
+ active: false,
341
+ generation: 0,
342
+ now,
343
+ subscribers: new Map<symbol, Subscriber>(),
344
+ rootSessionIds: new Map<string, number>(),
345
+ sessions: new WeakMap<object, SessionObservation>(),
346
+ attachmentRefs,
347
+ attachmentFinalizer: new FinalizationRegistry((ref) => attachmentRefs.delete(ref)),
348
+ };
349
+ return state;
350
+ }
351
+
352
+ function activatePatch(
353
+ state: PatchState,
354
+ now: () => number,
355
+ onError: InProcessAgentSessionObserverCallbacks['onError']
356
+ ): InProcessObserverIncompatibility | undefined {
357
+ const current = readDescriptor(state.prototype);
358
+ if (!current) {
359
+ reportDirect(onError, 'install', new Error('AgentSession.prototype.prompt is unavailable.'));
360
+ return 'prompt-descriptor';
361
+ }
362
+
363
+ if (state.active) {
364
+ if (!descriptorsEqual(current, state.wrappedDescriptor)) {
365
+ reportDirect(
366
+ onError,
367
+ 'install',
368
+ new Error('AgentSession.prototype.prompt changed while observation was installed.')
369
+ );
370
+ return 'prototype-conflict';
371
+ }
372
+ return undefined;
373
+ }
374
+
375
+ if (
376
+ !descriptorsEqual(current, state.originalDescriptor) &&
377
+ !descriptorsEqual(current, state.wrappedDescriptor)
378
+ ) {
379
+ reportDirect(
380
+ onError,
381
+ 'install',
382
+ new Error('AgentSession.prototype.prompt no longer matches the compatible descriptor.')
383
+ );
384
+ return 'prototype-conflict';
385
+ }
386
+
387
+ try {
388
+ if (!descriptorsEqual(current, state.wrappedDescriptor)) {
389
+ Object.defineProperty(state.prototype, 'prompt', state.wrappedDescriptor);
390
+ }
391
+ } catch (error) {
392
+ reportDirect(onError, 'install', error);
393
+ return 'prototype-patch';
394
+ }
395
+
396
+ state.now = now;
397
+ state.active = true;
398
+ state.generation += 1;
399
+ return undefined;
400
+ }
401
+
402
+ function resetObservation(observation: SessionObservation, generation: number): void {
403
+ observation.generation = generation;
404
+ observation.activeTools.clear();
405
+ observation.unionToolCalls.clear();
406
+ observation.unionStartedAt = undefined;
407
+ observation.unionMetadata = undefined;
408
+ observation.lastModel = undefined;
409
+ observation.lastResponseId = undefined;
410
+ }
411
+
412
+ function detachSessions(state: PatchState, reportingSubscriber: Subscriber): void {
413
+ for (const ref of [...state.attachmentRefs]) {
414
+ const observation = ref.deref();
415
+ if (!observation) {
416
+ state.attachmentRefs.delete(ref);
417
+ continue;
418
+ }
419
+ resetObservation(observation, state.generation);
420
+ if (!observation.subscribed || !observation.unsubscribe) continue;
421
+ try {
422
+ observation.unsubscribe();
423
+ } catch (error) {
424
+ reportSubscriber(reportingSubscriber, 'uninstall', error);
425
+ } finally {
426
+ observation.unsubscribe = undefined;
427
+ observation.subscribed = false;
428
+ }
429
+ }
430
+ }
431
+
432
+ function deactivatePatch(state: PatchState, reportingSubscriber: Subscriber): void {
433
+ state.active = false;
434
+ state.generation += 1;
435
+ detachSessions(state, reportingSubscriber);
436
+ if (state.subscribers.size > 0) return;
437
+
438
+ const current = readDescriptor(state.prototype);
439
+ if (!current) {
440
+ reportSubscriber(
441
+ reportingSubscriber,
442
+ 'uninstall',
443
+ new Error('AgentSession.prototype.prompt disappeared before restoration.')
444
+ );
445
+ return;
446
+ }
447
+ if (descriptorsEqual(current, state.originalDescriptor)) return;
448
+ if (!descriptorsEqual(current, state.wrappedDescriptor)) {
449
+ reportSubscriber(
450
+ reportingSubscriber,
451
+ 'uninstall',
452
+ new Error('AgentSession.prototype.prompt changed; refusing to overwrite the newer value.')
453
+ );
454
+ return;
455
+ }
456
+ try {
457
+ Object.defineProperty(state.prototype, 'prompt', state.originalDescriptor);
458
+ } catch (error) {
459
+ reportSubscriber(reportingSubscriber, 'uninstall', error);
460
+ }
461
+ }
462
+
463
+ function readSessionProperty(
464
+ state: PatchState,
465
+ session: object,
466
+ property: string,
467
+ phase: 'attach' | 'metadata' = 'metadata'
468
+ ): unknown {
469
+ try {
470
+ return Reflect.get(session, property);
471
+ } catch (error) {
472
+ reportAll(state, phase, error);
473
+ return undefined;
474
+ }
475
+ }
476
+
477
+ function normalizeModel(value: unknown): InProcessModelRef | undefined {
478
+ const model = asRecord(value);
479
+ if (!model) return undefined;
480
+ const provider = asString(model.provider);
481
+ const modelId = asString(model.id) ?? asString(model.modelId) ?? asString(model.model);
482
+ if (!provider && !modelId) return undefined;
483
+ return Object.freeze({
484
+ ...(provider ? { provider } : {}),
485
+ ...(modelId ? { modelId } : {}),
486
+ });
487
+ }
488
+
489
+ function readSessionMetadata(state: PatchState, session: object): SessionMetadata {
490
+ const sessionId = asString(readSessionProperty(state, session, 'sessionId'));
491
+ const sessionFile = asString(readSessionProperty(state, session, 'sessionFile'));
492
+ let model: InProcessModelRef | undefined;
493
+ try {
494
+ model = normalizeModel(readSessionProperty(state, session, 'model'));
495
+ } catch (error) {
496
+ reportAll(state, 'metadata', error);
497
+ }
498
+ return {
499
+ ...(sessionId ? { sessionId } : {}),
500
+ ...(sessionFile ? { sessionFile } : {}),
501
+ ...(model ? { model } : {}),
502
+ };
503
+ }
504
+
505
+ function metadataWithResponse(
506
+ metadata: SessionMetadata,
507
+ model: InProcessModelRef | undefined,
508
+ responseId: string | undefined
509
+ ): InProcessSessionMetadata {
510
+ return Object.freeze({
511
+ ...(metadata.sessionId ? { sessionId: metadata.sessionId } : {}),
512
+ ...(metadata.sessionFile ? { sessionFile: metadata.sessionFile } : {}),
513
+ ...(model ? { model } : {}),
514
+ ...(responseId ? { responseId } : {}),
515
+ });
516
+ }
517
+
518
+ function readNow(state: PatchState): number {
519
+ try {
520
+ const value = state.now();
521
+ if (Number.isFinite(value)) return value;
522
+ reportAll(
523
+ state,
524
+ 'event',
525
+ new Error('AgentSession observer clock returned a non-finite value.')
526
+ );
527
+ } catch (error) {
528
+ reportAll(state, 'event', error);
529
+ }
530
+ return Date.now();
531
+ }
532
+
533
+ function normalizeUsage(value: unknown): InProcessAssistantUsage | undefined {
534
+ const usage = asRecord(value);
535
+ if (!usage) return undefined;
536
+ const reasoning = asFiniteNumber(usage.reasoning);
537
+ const costRecord = asRecord(usage.cost);
538
+ const cost = costRecord
539
+ ? Object.freeze({
540
+ input: nonNegativeNumber(costRecord.input),
541
+ output: nonNegativeNumber(costRecord.output),
542
+ cacheRead: nonNegativeNumber(costRecord.cacheRead),
543
+ cacheWrite: nonNegativeNumber(costRecord.cacheWrite),
544
+ total: nonNegativeNumber(costRecord.total),
545
+ })
546
+ : undefined;
547
+ const input = nonNegativeNumber(usage.input);
548
+ const output = nonNegativeNumber(usage.output);
549
+ const cacheRead = nonNegativeNumber(usage.cacheRead);
550
+ const cacheWrite = nonNegativeNumber(usage.cacheWrite);
551
+ const suppliedTotal = asFiniteNumber(usage.totalTokens);
552
+ const totalTokens =
553
+ suppliedTotal === undefined
554
+ ? input + output + cacheRead + cacheWrite
555
+ : Math.max(0, suppliedTotal);
556
+ return Object.freeze({
557
+ input,
558
+ output,
559
+ cacheRead,
560
+ cacheWrite,
561
+ ...(reasoning !== undefined ? { reasoning: Math.max(0, reasoning) } : {}),
562
+ totalTokens,
563
+ ...(cost ? { cost } : {}),
564
+ });
565
+ }
566
+
567
+ function normalizeMessageModel(
568
+ message: Record<PropertyKey, unknown>,
569
+ fallback: InProcessModelRef | undefined
570
+ ): InProcessModelRef | undefined {
571
+ const provider = asString(message.provider) ?? fallback?.provider;
572
+ const modelId = asString(message.model) ?? fallback?.modelId;
573
+ if (!provider && !modelId) return undefined;
574
+ return Object.freeze({
575
+ ...(provider ? { provider } : {}),
576
+ ...(modelId ? { modelId } : {}),
577
+ });
578
+ }
579
+
580
+ function dispatchEvent(
581
+ state: PatchState,
582
+ event: InProcessAssistantUsageEvent | InProcessToolIntervalEvent
583
+ ): void {
584
+ if (event.sessionId && state.rootSessionIds.has(event.sessionId)) return;
585
+ for (const [id, subscriber] of [...state.subscribers.entries()]) {
586
+ if (!state.subscribers.has(id)) continue;
587
+ const onError = (error: unknown): void => reportSubscriber(subscriber, 'callback', error);
588
+ if (event.type === 'assistant_usage') {
589
+ invokeCallback(subscriber.callbacks.onAssistantUsage, event, onError);
590
+ } else {
591
+ invokeCallback(subscriber.callbacks.onToolInterval, event, onError);
592
+ }
593
+ }
594
+ }
595
+
596
+ function handleAssistantMessageEnd(
597
+ state: PatchState,
598
+ observation: SessionObservation,
599
+ metadata: SessionMetadata,
600
+ event: Record<PropertyKey, unknown>
601
+ ): void {
602
+ const message = asRecord(event.message);
603
+ if (!message || message.role !== 'assistant') return;
604
+
605
+ const model = normalizeMessageModel(message, metadata.model);
606
+ const responseId = asString(message.responseId);
607
+ observation.lastModel = model;
608
+ observation.lastResponseId = responseId;
609
+
610
+ const usage = normalizeUsage(message.usage);
611
+ if (!usage) return;
612
+ const timestamp = asFiniteNumber(message.timestamp) ?? readNow(state);
613
+ const stopReason = asString(message.stopReason);
614
+ const normalized = Object.freeze({
615
+ type: 'assistant_usage' as const,
616
+ ...metadataWithResponse(metadata, model, responseId),
617
+ timestamp,
618
+ ...(stopReason ? { stopReason } : {}),
619
+ usage,
620
+ });
621
+ dispatchEvent(state, normalized);
622
+ }
623
+
624
+ function handleToolStart(
625
+ state: PatchState,
626
+ observation: SessionObservation,
627
+ metadata: SessionMetadata,
628
+ event: Record<PropertyKey, unknown>
629
+ ): void {
630
+ const toolCallId = asString(event.toolCallId);
631
+ if (!toolCallId || observation.activeTools.has(toolCallId)) return;
632
+ const toolName = asString(event.toolName);
633
+ const startedAt = readNow(state);
634
+ if (observation.activeTools.size === 0) {
635
+ observation.unionStartedAt = startedAt;
636
+ observation.unionToolCalls.clear();
637
+ observation.unionMetadata = metadataWithResponse(
638
+ metadata,
639
+ observation.lastModel ?? metadata.model,
640
+ observation.lastResponseId
641
+ );
642
+ }
643
+ observation.activeTools.set(toolCallId, toolName);
644
+ observation.unionToolCalls.set(toolCallId, toolName);
645
+ }
646
+
647
+ function handleToolEnd(
648
+ state: PatchState,
649
+ observation: SessionObservation,
650
+ event: Record<PropertyKey, unknown>
651
+ ): void {
652
+ const toolCallId = asString(event.toolCallId);
653
+ if (!toolCallId || !observation.activeTools.has(toolCallId)) return;
654
+ observation.activeTools.delete(toolCallId);
655
+ if (observation.activeTools.size > 0) return;
656
+
657
+ const startedAt = observation.unionStartedAt ?? readNow(state);
658
+ const endedAt = Math.max(startedAt, readNow(state));
659
+ const metadata = observation.unionMetadata ?? {};
660
+ const toolCalls = Object.freeze(
661
+ [...observation.unionToolCalls.entries()].map(([id, name]) =>
662
+ Object.freeze({
663
+ toolCallId: id,
664
+ ...(name ? { toolName: name } : {}),
665
+ })
666
+ )
667
+ );
668
+ observation.unionStartedAt = undefined;
669
+ observation.unionMetadata = undefined;
670
+ observation.unionToolCalls.clear();
671
+
672
+ dispatchEvent(
673
+ state,
674
+ Object.freeze({
675
+ type: 'tool_interval' as const,
676
+ ...metadata,
677
+ startedAt,
678
+ endedAt,
679
+ durationMs: endedAt - startedAt,
680
+ toolCalls,
681
+ })
682
+ );
683
+ }
684
+
685
+ function handleSessionEvent(
686
+ state: PatchState,
687
+ observation: SessionObservation,
688
+ eventValue: unknown
689
+ ): void {
690
+ if (!state.active || state.subscribers.size === 0) {
691
+ resetObservation(observation, state.generation);
692
+ return;
693
+ }
694
+ if (observation.generation !== state.generation) {
695
+ resetObservation(observation, state.generation);
696
+ }
697
+
698
+ const session = observation.sessionRef.deref();
699
+ if (!session) return;
700
+ const metadata = readSessionMetadata(state, session);
701
+ if (metadata.sessionId && state.rootSessionIds.has(metadata.sessionId)) {
702
+ resetObservation(observation, state.generation);
703
+ return;
704
+ }
705
+
706
+ try {
707
+ const event = asRecord(eventValue);
708
+ const type = event ? asString(event.type) : undefined;
709
+ if (!event || !type) return;
710
+ if (type === 'message_end') {
711
+ handleAssistantMessageEnd(state, observation, metadata, event);
712
+ } else if (type === 'tool_execution_start') {
713
+ handleToolStart(state, observation, metadata, event);
714
+ } else if (type === 'tool_execution_end') {
715
+ handleToolEnd(state, observation, event);
716
+ }
717
+ } catch (error) {
718
+ reportAll(state, 'event', error);
719
+ }
720
+ }
721
+
722
+ function createSessionObservation(state: PatchState, session: object): SessionObservation {
723
+ let observation: SessionObservation;
724
+ const listener: SessionEventListener = (event) => {
725
+ handleSessionEvent(state, observation, event);
726
+ };
727
+ observation = {
728
+ generation: state.generation,
729
+ subscribed: false,
730
+ sessionRef: new WeakRef(session),
731
+ listener,
732
+ activeTools: new Map<string, string | undefined>(),
733
+ unionToolCalls: new Map<string, string | undefined>(),
734
+ };
735
+ const ref = new WeakRef(observation);
736
+ state.attachmentRefs.add(ref);
737
+ state.attachmentFinalizer.register(observation, ref, observation);
738
+ return observation;
739
+ }
740
+
741
+ function attachSession(state: PatchState, sessionValue: unknown): void {
742
+ if (!isObject(sessionValue)) return;
743
+ const session = sessionValue;
744
+ const sessionId = asString(readSessionProperty(state, session, 'sessionId'));
745
+ if (sessionId && state.rootSessionIds.has(sessionId)) return;
746
+
747
+ let observation = state.sessions.get(session);
748
+ if (!observation) {
749
+ observation = createSessionObservation(state, session);
750
+ state.sessions.set(session, observation);
751
+ } else {
752
+ observation.sessionRef = new WeakRef(session);
753
+ }
754
+ if (observation.subscribed) return;
755
+
756
+ const subscribe = readSessionProperty(state, session, 'subscribe', 'attach');
757
+ if (typeof subscribe !== 'function') {
758
+ reportAll(
759
+ state,
760
+ 'attach',
761
+ new Error('AgentSession instance does not expose the public subscribe method.')
762
+ );
763
+ return;
764
+ }
765
+
766
+ resetObservation(observation, state.generation);
767
+ try {
768
+ const unsubscribe = Reflect.apply(subscribe, session, [observation.listener]);
769
+ observation.unsubscribe = typeof unsubscribe === 'function' ? unsubscribe : undefined;
770
+ observation.subscribed = true;
771
+ } catch (error) {
772
+ reportAll(state, 'attach', error);
773
+ }
774
+ }
775
+
776
+ function decrementRoot(state: PatchState, sessionId: string): void {
777
+ const count = state.rootSessionIds.get(sessionId);
778
+ if (count === undefined) return;
779
+ if (count <= 1) state.rootSessionIds.delete(sessionId);
780
+ else state.rootSessionIds.set(sessionId, count - 1);
781
+ }
782
+
783
+ function unavailableHandle(
784
+ incompatibility: InProcessObserverIncompatibility
785
+ ): InProcessAgentSessionObserverHandle {
786
+ return {
787
+ installed: false,
788
+ incompatibility,
789
+ addRootSessionId() {},
790
+ removeRootSessionId() {},
791
+ uninstall() {},
792
+ };
793
+ }
794
+
795
+ export function installInProcessAgentSessionObserver(
796
+ options: InProcessAgentSessionObserverOptions = {}
797
+ ): InProcessAgentSessionObserverHandle {
798
+ const callbacks: ObserverCallbacks = Object.freeze({
799
+ onAssistantUsage: options.onAssistantUsage,
800
+ onToolInterval: options.onToolInterval,
801
+ onError: options.onError,
802
+ });
803
+ const agentSessionClass = options.dependencies?.AgentSession ?? AgentSession;
804
+ const now = options.dependencies?.now ?? Date.now;
805
+ const prototype = agentSessionClass?.prototype;
806
+ if (!isObject(prototype)) {
807
+ reportDirect(
808
+ callbacks.onError,
809
+ 'install',
810
+ new Error('The AgentSession class does not expose a compatible prototype.')
811
+ );
812
+ return unavailableHandle('agent-session-prototype');
813
+ }
814
+
815
+ const registry = getGlobalRegistry(callbacks.onError);
816
+ if (!registry) return unavailableHandle('global-registry');
817
+
818
+ let state = registry.patches.get(prototype);
819
+ if (state && !isPatchState(state)) {
820
+ reportDirect(
821
+ callbacks.onError,
822
+ 'install',
823
+ new Error('The existing AgentSession prototype observer is incompatible.')
824
+ );
825
+ return unavailableHandle('prototype-conflict');
826
+ }
827
+
828
+ if (!state) {
829
+ const descriptor = readDescriptor(prototype);
830
+ if (!descriptor || typeof descriptor.value !== 'function' || descriptor.writable !== true) {
831
+ reportDirect(
832
+ callbacks.onError,
833
+ 'install',
834
+ new Error('AgentSession.prototype.prompt is not a writable public method.')
835
+ );
836
+ return unavailableHandle('prompt-descriptor');
837
+ }
838
+ if (typeof readPrototypeMethod(prototype, 'subscribe') !== 'function') {
839
+ reportDirect(
840
+ callbacks.onError,
841
+ 'install',
842
+ new Error('AgentSession.prototype.subscribe is not a public method.')
843
+ );
844
+ return unavailableHandle('subscribe-method');
845
+ }
846
+ state = createPatchState(prototype, descriptor, now);
847
+ registry.patches.set(prototype, state);
848
+ }
849
+
850
+ const patch = state;
851
+ const incompatibility = activatePatch(patch, now, callbacks.onError);
852
+ if (incompatibility) return unavailableHandle(incompatibility);
853
+
854
+ const token = Symbol('pi-ledger-in-process-observer-subscriber');
855
+ const subscriber: Subscriber = { callbacks, roots: new Set<string>() };
856
+ patch.subscribers.set(token, subscriber);
857
+ let uninstalled = false;
858
+
859
+ const addRootSessionId = (sessionId: string): void => {
860
+ if (uninstalled || !asString(sessionId) || subscriber.roots.has(sessionId)) return;
861
+ subscriber.roots.add(sessionId);
862
+ patch.rootSessionIds.set(sessionId, (patch.rootSessionIds.get(sessionId) ?? 0) + 1);
863
+ };
864
+ const removeRootSessionId = (sessionId: string): void => {
865
+ if (uninstalled || !subscriber.roots.delete(sessionId)) return;
866
+ decrementRoot(patch, sessionId);
867
+ };
868
+
869
+ if (options.rootSessionIds) {
870
+ try {
871
+ for (const sessionId of options.rootSessionIds) addRootSessionId(sessionId);
872
+ } catch (error) {
873
+ reportSubscriber(subscriber, 'install', error);
874
+ }
875
+ }
876
+
877
+ return {
878
+ installed: true,
879
+ addRootSessionId,
880
+ removeRootSessionId,
881
+ uninstall(): void {
882
+ if (uninstalled) return;
883
+ uninstalled = true;
884
+ for (const sessionId of subscriber.roots) decrementRoot(patch, sessionId);
885
+ subscriber.roots.clear();
886
+ patch.subscribers.delete(token);
887
+ if (patch.subscribers.size === 0) deactivatePatch(patch, subscriber);
888
+ },
889
+ };
890
+ }