@nijaru/pi-provider-compaction 0.0.8 → 0.0.9

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/provider.ts CHANGED
@@ -1,7 +1,11 @@
1
- import type { Provider, StreamOptions, Model, TranscriptContext } from "@earendil-works/pi-ai";
1
+ import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Provider, StreamOptions, Model, TranscriptContext } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
 
4
- export type ObserveRequest = (model: Model<any>, context: TranscriptContext, options: StreamOptions) => ((payload: unknown) => unknown | Promise<unknown>) | undefined;
4
+ export type PayloadObserver = ((payload: unknown) => unknown | Promise<unknown>) & {
5
+ complete?: (message: AssistantMessage) => void;
6
+ cacheRetention?: CacheRetention;
7
+ };
8
+ export type ObserveRequest = (model: Model<any>, context: TranscriptContext, options: StreamOptions) => PayloadObserver | undefined;
5
9
 
6
10
  export interface InstalledObserver {
7
11
  provider: string;
@@ -13,26 +17,101 @@ export interface InstalledObserver {
13
17
  // streams: this observer delegates unchanged and can release only its own slot.
14
18
  export const STREAM_OBSERVER = Symbol.for("@nijaru/pi-provider-compaction/stream-observer");
15
19
 
16
- function optionsWithObserver<T extends StreamOptions>(model: Model<any>, context: TranscriptContext, options: T | undefined, observe: ObserveRequest): T | undefined {
20
+ function observedStream<T extends StreamOptions>(model: Model<any>, context: TranscriptContext, options: T | undefined, observe: ObserveRequest, dispatch: (options: T | undefined) => AssistantMessageEventStream): AssistantMessageEventStream {
17
21
  let handler: ReturnType<ObserveRequest>;
18
22
  try { handler = observe(model, context, options ?? {}); }
19
- catch { return options; } // An expired or failing optional observer must not veto dispatch.
20
- if (!handler) return options;
21
- return {
23
+ catch { return dispatch(options); } // An expired optional observer must not veto dispatch.
24
+ if (!handler) return dispatch(options);
25
+ const stream = dispatch({
22
26
  ...options,
27
+ ...(handler.cacheRetention === undefined ? {} : { cacheRetention: handler.cacheRetention }),
23
28
  onPayload: async (payload: unknown, requestModel: Model<any>) => {
24
29
  const replacement = await options?.onPayload?.(payload, requestModel);
25
30
  const prepared = replacement === undefined ? payload : replacement;
26
31
  // Observation failures must not suppress or alter the portable request.
27
32
  try { return await handler(prepared); } catch { return prepared; }
28
33
  },
29
- } as T;
34
+ } as T);
35
+ // result() observes completion without consuming the public event iterator.
36
+ if (handler.complete) void stream.result().then(handler.complete).catch(() => {});
37
+ return stream;
38
+ }
39
+
40
+ /** One dispatch layer, with session-local selection and no released lease history. */
41
+ function observerLeases(provider: string, owned: () => boolean, restore: () => void) {
42
+ const leases = new Map<symbol, { observe: ObserveRequest; current: () => boolean }>();
43
+ let closed = false;
44
+ const active = (token: symbol) => !closed && leases.get(token)?.current() === true && owned();
45
+ const release = () => {
46
+ if (closed) return;
47
+ closed = true;
48
+ leases.clear();
49
+ restore();
50
+ };
51
+ const observe: ObserveRequest = (...args) => {
52
+ const handlers: PayloadObserver[] = [];
53
+ for (const [token, lease] of leases) {
54
+ try {
55
+ if (!active(token)) continue;
56
+ const handler = lease.observe(...args);
57
+ if (!handler) continue;
58
+ handlers.push(Object.assign((payload: unknown) => active(token) ? handler(payload) : payload, {
59
+ cacheRetention: handler.cacheRetention,
60
+ complete: handler.complete ? (message: AssistantMessage) => { if (active(token)) handler.complete!(message); } : undefined,
61
+ }));
62
+ } catch { /* A failed optional observer must not disable the other sessions. */ }
63
+ }
64
+ if (!handlers.length) return;
65
+ return Object.assign(async (payload: unknown) => {
66
+ for (const handler of handlers) {
67
+ try {
68
+ const replacement = await handler(payload);
69
+ if (replacement !== undefined) payload = replacement;
70
+ } catch { /* Keep the last valid request on optional observation failure. */ }
71
+ }
72
+ return payload;
73
+ }, {
74
+ cacheRetention: [...handlers].reverse().find((handler) => handler.cacheRetention !== undefined)?.cacheRetention,
75
+ complete: handlers.some((handler) => handler.complete) ? (message: AssistantMessage) => {
76
+ for (const handler of handlers) {
77
+ try { handler.complete?.(message); } catch { /* Isolate optional completion observers too. */ }
78
+ }
79
+ } : undefined,
80
+ });
81
+ };
82
+ return {
83
+ observe, release, isOpen: () => !closed,
84
+ add(observe: ObserveRequest, current: () => boolean = () => true): InstalledObserver {
85
+ const token = Symbol();
86
+ leases.set(token, { observe, current });
87
+ return { provider, isInstalled: () => active(token), remove() {
88
+ leases.delete(token);
89
+ if (!leases.size) release();
90
+ } };
91
+ },
92
+ };
93
+ }
94
+
95
+ type ObserverLeases = ReturnType<typeof observerLeases>;
96
+ // Pi's uncached module loader can instantiate this extension separately for
97
+ // sessions sharing one model runtime. Weak ownership must span those instances.
98
+ const LEASE_TABLES = Symbol.for("@nijaru/pi-provider-compaction/observer-leases-v1");
99
+ interface LeaseTables {
100
+ effectiveLeases: WeakMap<Provider, { owned(): boolean; leases: ObserverLeases }>;
101
+ effectiveRestores: WeakMap<Function, { base: Provider; restore(): void }>;
102
+ nativeLeases: WeakMap<Provider, ObserverLeases>;
103
+ legacyLeases: WeakMap<Provider["streamSimple"], ObserverLeases>;
30
104
  }
105
+ const shared = globalThis as typeof globalThis & { [LEASE_TABLES]?: LeaseTables };
106
+ const { effectiveLeases, effectiveRestores, nativeLeases, legacyLeases } = shared[LEASE_TABLES] ??= {
107
+ effectiveLeases: new WeakMap(), effectiveRestores: new WeakMap(),
108
+ nativeLeases: new WeakMap(), legacyLeases: new WeakMap(),
109
+ };
31
110
 
32
111
  /** Preserve method receivers, getters, optional members and dynamic catalogs. */
33
112
  export function decorateProvider(base: Provider, observe: ObserveRequest): Provider {
34
- const stream: Provider["stream"] = (model, context, options) => base.stream(model, context, optionsWithObserver(model, context, options, observe));
35
- const streamSimple: Provider["streamSimple"] = (model, context, options) => base.streamSimple(model, context, optionsWithObserver(model, context, options, observe));
113
+ const stream: Provider["stream"] = (model, context, options) => observedStream(model, context, options, observe, (next) => base.stream(model, context, next));
114
+ const streamSimple: Provider["streamSimple"] = (model, context, options) => observedStream(model, context, options, observe, (next) => base.streamSimple(model, context, next));
36
115
  const bound = new Map<PropertyKey, unknown>();
37
116
  return new Proxy(base, {
38
117
  get(target, key) {
@@ -46,6 +125,43 @@ export function decorateProvider(base: Provider, observe: ObserveRequest): Provi
46
125
  });
47
126
  }
48
127
 
128
+ /** Observe a builtin/composer in place, preserving its auth, catalogs and dispatch closure. */
129
+ export function leaseEffectiveProvider(base: Provider | undefined, current: () => Provider | undefined, observe: ObserveRequest): InstalledObserver | undefined {
130
+ if (!base) return;
131
+ const existing = effectiveLeases.get(base);
132
+ if (existing?.leases.isOpen() && existing.owned()) return existing.leases.add(observe, () => current() === base);
133
+ // External replacement invalidated the old pair. Restore its still-owned
134
+ // method before capturing delegates, rather than burying a released wrapper.
135
+ existing?.leases.release();
136
+ // A third party may have restored saved method values after their release.
137
+ // They remain inert, but must not become delegates of another observation layer.
138
+ const keys = ["stream", "streamSimple"] as const;
139
+ for (const key of keys) {
140
+ const saved = effectiveRestores.get(base[key]);
141
+ if (saved?.base === base) saved.restore();
142
+ }
143
+ const descriptors = keys.map((key) => Object.getOwnPropertyDescriptor(base, key));
144
+ if (descriptors.some((descriptor) => descriptor && (!("value" in descriptor) || !descriptor.configurable && !descriptor.writable)) || !Object.isExtensible(base) && descriptors.some((descriptor) => !descriptor)) return;
145
+ const originalStream = base.stream, originalSimple = base.streamSimple;
146
+ const owned = () => base.stream === stream && base.streamSimple === streamSimple;
147
+ const restore = () => {
148
+ for (const [index, key] of keys.entries()) if (base[key] === (key === "stream" ? stream : streamSimple)) {
149
+ const descriptor = descriptors[index];
150
+ if (descriptor) Object.defineProperty(base, key, descriptor);
151
+ else Reflect.deleteProperty(base, key);
152
+ }
153
+ };
154
+ const leases = observerLeases(base.id, owned, restore);
155
+ const stream: Provider["stream"] = (model, context, options) => observedStream(model, context, options, leases.observe, (next) => originalStream.call(base, model, context, next));
156
+ const streamSimple: Provider["streamSimple"] = (model, context, options) => observedStream(model, context, options, leases.observe, (next) => originalSimple.call(base, model, context, next));
157
+ try {
158
+ for (const [index, key] of keys.entries()) Object.defineProperty(base, key, { ...descriptors[index], value: key === "stream" ? stream : streamSimple, ...(descriptors[index] ? {} : { configurable: true, writable: true, enumerable: true }) });
159
+ } catch { leases.release(); return; }
160
+ for (const method of [stream, streamSimple]) effectiveRestores.set(method, { base, restore });
161
+ effectiveLeases.set(base, { owned, leases });
162
+ return leases.add(observe, () => current() === base);
163
+ }
164
+
49
165
  /**
50
166
  * Pi 0.87 has replacement registration, not middleware. Native BASE providers can
51
167
  * be decorated directly. Legacy stream overlays retain their configuration and
@@ -64,45 +180,44 @@ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe
64
180
  // wrapper; claiming an observer there would report native compaction as
65
181
  // installed while never capturing anything.
66
182
  if (typeof native.getModels !== "function" || !native.getModels().some((entry) => entry.api === model.api)) return;
67
- const isInstalled = () => registry.getRegisteredNativeProvider(id) === wrapper;
68
- const guarded: ObserveRequest = (...args) => {
69
- if (!isInstalled()) return;
70
- const handler = observe(...args);
71
- return handler ? (payload) => isInstalled() ? handler(payload) : payload : undefined;
72
- };
73
- const wrapper = decorateProvider(native, guarded);
183
+ const existing = nativeLeases.get(native);
184
+ if (existing) return existing.add(observe);
185
+ const leases = observerLeases(id, () => registry.getRegisteredNativeProvider(id) === wrapper, () => {
186
+ // Keep cleanup usable after bare SDK disposal invalidates the extension API.
187
+ if (registry.getRegisteredNativeProvider(id) === wrapper) registry.registerProvider(native);
188
+ nativeLeases.delete(wrapper);
189
+ });
190
+ const wrapper = decorateProvider(native, leases.observe);
191
+ nativeLeases.set(wrapper, leases);
74
192
  pi.registerProvider(wrapper);
75
- return {
76
- provider: id, isInstalled,
77
- remove() {
78
- // Keep cleanup usable after bare SDK disposal invalidates the extension API.
79
- if (isInstalled()) registry.registerProvider(native);
80
- },
81
- };
193
+ return leases.add(observe);
82
194
  }
83
195
  const original = registry.getRegisteredProviderConfig(id);
84
196
  // Refresh callbacks own mutable catalog state inside the composer. Replacing
85
197
  // that composer cannot promise preservation, so leave it alone.
86
- if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels) return;
87
- if (original.api !== model.api) return;
198
+ if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels || original.api !== model.api) {
199
+ // Lease the effective object's methods, NOT its registry slot. Re-registering
200
+ // a composed provider freezes catalogs and loses overlay/header semantics.
201
+ return leaseEffectiveProvider(registry.getProvider(id), () => registry.getProvider(id), observe);
202
+ }
88
203
  // An injected legacy streamSimple overlay would also intercept full stream()
89
204
  // calls and change reasoning semantics. Only decorate an existing stream owner.
205
+ const existing = legacyLeases.get(original.streamSimple);
206
+ if (existing) return existing.add(observe);
90
207
  const delegate = original.streamSimple.bind(original);
91
- const isInstalled = () => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper && !registry.getRegisteredNativeProvider(id);
92
- const guarded: ObserveRequest = (...args) => {
93
- if (!isInstalled()) return;
94
- const handler = observe(...args);
95
- return handler ? (payload) => isInstalled() ? handler(payload) : payload : undefined;
96
- };
97
- const wrapper: Provider["streamSimple"] = (requestModel, context, options) => delegate(requestModel, context, optionsWithObserver(requestModel, context, options, guarded));
98
- const remove = () => {
99
- if (!isInstalled()) return;
208
+ const owned = () => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper && !registry.getRegisteredNativeProvider(id);
209
+ const leases = observerLeases(id, owned, () => {
210
+ legacyLeases.delete(wrapper);
211
+ if (!owned()) return;
100
212
  // registerProvider merges undefined properties: remove our layer before restore.
101
213
  const restored = { ...registry.getRegisteredProviderConfig(id)!, streamSimple: original.streamSimple };
102
214
  registry.unregisterProvider(id);
103
215
  registry.registerProvider(id, restored);
104
- };
105
- Object.defineProperty(wrapper, STREAM_OBSERVER, { value: { delegate: original.streamSimple, release: remove } });
216
+ });
217
+ const wrapper: Provider["streamSimple"] = (requestModel, context, options) => observedStream(requestModel, context, options, leases.observe, (next) => delegate(requestModel, context, next));
218
+ // /fast owns the delegate: its cleanup must release all observing sessions.
219
+ Object.defineProperty(wrapper, STREAM_OBSERVER, { value: { delegate: original.streamSimple, release: leases.release } });
220
+ legacyLeases.set(wrapper, leases);
106
221
  pi.registerProvider(id, { ...original, streamSimple: wrapper });
107
- return { provider: id, isInstalled, remove };
222
+ return leases.add(observe);
108
223
  }