SaylorTwift HF Staff commited on
Commit
68d7816
·
verified ·
1 Parent(s): 4e23b01

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. packages/agent-core-v2/src/_base/contribution/registry.ts +55 -0
  2. packages/agent-core-v2/src/_base/di/cascadeEngine.ts +798 -0
  3. packages/agent-core-v2/src/_base/di/collection.ts +302 -0
  4. packages/agent-core-v2/src/_base/di/dependencyGraph.ts +262 -0
  5. packages/agent-core-v2/src/_base/di/descriptors.ts +14 -0
  6. packages/agent-core-v2/src/_base/di/errors.ts +30 -0
  7. packages/agent-core-v2/src/_base/di/fiber.ts +714 -0
  8. packages/agent-core-v2/src/_base/di/graph.ts +89 -0
  9. packages/agent-core-v2/src/_base/di/instantiation.ts +212 -0
  10. packages/agent-core-v2/src/_base/di/instantiationService.ts +1006 -0
  11. packages/agent-core-v2/src/_base/di/lifecycle.ts +709 -0
  12. packages/agent-core-v2/src/_base/di/scope.ts +300 -0
  13. packages/agent-core-v2/src/_base/di/scopeUnits.ts +110 -0
  14. packages/agent-core-v2/src/_base/di/service.ts +138 -0
  15. packages/agent-core-v2/src/_base/di/serviceCollection.ts +144 -0
  16. packages/agent-core-v2/src/_base/di/test.ts +53 -0
  17. packages/agent-core-v2/src/_base/di/testInstantiationService.ts +348 -0
  18. packages/agent-core-v2/src/_base/di/util/linkedList.ts +84 -0
  19. packages/agent-core-v2/src/_base/errors/codes.ts +70 -0
  20. packages/agent-core-v2/src/_base/errors/errorMessage.ts +23 -0
  21. packages/agent-core-v2/src/_base/errors/errors.ts +70 -0
  22. packages/agent-core-v2/src/_base/errors/serialize.ts +97 -0
  23. packages/agent-core-v2/src/_base/errors/unexpectedError.ts +31 -0
  24. packages/agent-core-v2/src/_base/execEnv/bufferedReadable.ts +52 -0
  25. packages/agent-core-v2/src/_base/execEnv/decodeText.ts +185 -0
  26. packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts +322 -0
  27. packages/agent-core-v2/src/_base/execEnv/globPattern.ts +46 -0
  28. packages/agent-core-v2/src/_base/execEnv/loginShellPath.ts +80 -0
  29. packages/agent-core-v2/src/_base/execEnv/shellPathBridge.ts +150 -0
  30. packages/agent-core-v2/src/_base/lifecycle/disposer.ts +37 -0
  31. packages/agent-core-v2/src/_base/lifecycle/errors.ts +12 -0
  32. packages/agent-core-v2/src/_base/lifecycle/index.ts +4 -0
  33. packages/agent-core-v2/src/_base/lifecycle/keyedResource.ts +150 -0
  34. packages/agent-core-v2/src/_base/lifecycle/ledger.ts +317 -0
  35. packages/agent-core-v2/src/_base/lifecycle/lifecycleMachine.ts +235 -0
  36. packages/agent-core-v2/src/_base/log/fileLog.ts +285 -0
  37. packages/agent-core-v2/src/_base/log/formatter.ts +196 -0
  38. packages/agent-core-v2/src/_base/log/log.ts +59 -0
  39. packages/agent-core-v2/src/_base/log/logConfig.ts +72 -0
  40. packages/agent-core-v2/src/_base/log/logService.ts +181 -0
  41. packages/agent-core-v2/src/_base/state/stateRegistry.ts +163 -0
  42. packages/agent-core-v2/src/_base/text/encoding.ts +113 -0
  43. packages/agent-core-v2/src/_base/text/frontmatter.ts +43 -0
  44. packages/agent-core-v2/src/_base/text/line-endings.ts +67 -0
  45. packages/agent-core-v2/src/_base/utils/abort.ts +93 -0
  46. packages/agent-core-v2/src/_base/utils/canonical-args.ts +24 -0
  47. packages/agent-core-v2/src/_base/utils/env.ts +10 -0
  48. packages/agent-core-v2/src/_base/utils/fileMeta.ts +113 -0
  49. packages/agent-core-v2/src/_base/utils/fs.ts +168 -0
  50. packages/agent-core-v2/src/_base/utils/hero-slug.ts +263 -0
packages/agent-core-v2/src/_base/contribution/registry.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Disposable, type IDisposable } from '../di/lifecycle';
2
+ import { Emitter, type Event } from '../event';
3
+
4
+ export interface ContributionRegistration<T> {
5
+ readonly sourceId: string;
6
+ readonly priority: number;
7
+ readonly contribution: T;
8
+ }
9
+
10
+ export interface RegisterContributionOptions {
11
+ readonly priority?: number;
12
+ }
13
+
14
+ export class ContributionRegistry<T> extends Disposable {
15
+ private readonly registrations = new Map<string, ContributionRegistration<T>>();
16
+ private readonly onDidChangeEmitter = this._register(new Emitter<string>());
17
+ readonly onDidChange: Event<string> = this.onDidChangeEmitter.event;
18
+
19
+ register(
20
+ sourceId: string,
21
+ contribution: T,
22
+ options?: RegisterContributionOptions,
23
+ ): IDisposable {
24
+ const registration: ContributionRegistration<T> = {
25
+ sourceId,
26
+ priority: options?.priority ?? 0,
27
+ contribution,
28
+ };
29
+ this.registrations.set(sourceId, registration);
30
+ this.onDidChangeEmitter.fire(sourceId);
31
+ let active = true;
32
+ return {
33
+ dispose: () => {
34
+ if (!active) return;
35
+ active = false;
36
+ if (this.registrations.get(sourceId) !== registration) return;
37
+ this.registrations.delete(sourceId);
38
+ this.onDidChangeEmitter.fire(sourceId);
39
+ },
40
+ };
41
+ }
42
+
43
+ unregister(sourceId: string): void {
44
+ if (!this.registrations.delete(sourceId)) return;
45
+ this.onDidChangeEmitter.fire(sourceId);
46
+ }
47
+
48
+ entries(): readonly ContributionRegistration<T>[] {
49
+ return [...this.registrations.values()];
50
+ }
51
+
52
+ get(sourceId: string): ContributionRegistration<T> | undefined {
53
+ return this.registrations.get(sourceId);
54
+ }
55
+ }
packages/agent-core-v2/src/_base/di/cascadeEngine.ts ADDED
@@ -0,0 +1,798 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { onUnexpectedError } from '../errors/unexpectedError';
2
+ import { Emitter, type Event } from '../event';
3
+ import { isPromiseLike } from '../lifecycle/disposer';
4
+ import type { SyncDescriptor } from './descriptors';
5
+ import {
6
+ PairIndex,
7
+ type DependencyGraph,
8
+ type ScopedToken,
9
+ } from './dependencyGraph';
10
+ import { CascadeConflictError } from './errors';
11
+ import type { ServiceIdentifier } from './instantiation';
12
+
13
+ export type UnitState = 'Pending' | 'Activating' | 'Active' | 'Unloading' | 'Failed';
14
+
15
+ export type CascadeAction = 'provide' | 'unprovide' | 'update';
16
+
17
+ export type UnitActivation = 'eager' | 'ondemand';
18
+
19
+ export interface CascadeChange {
20
+ readonly action: CascadeAction;
21
+ readonly token: ServiceIdentifier<any>;
22
+ readonly descriptor?: SyncDescriptor<unknown>;
23
+ readonly instance?: unknown;
24
+ readonly activation?: UnitActivation;
25
+ readonly config?: unknown;
26
+ readonly reason: string;
27
+ }
28
+
29
+ export interface CascadeHistoryEntry {
30
+ readonly seq: number;
31
+ readonly reason: string;
32
+ readonly changes: ReadonlyArray<{ token: string; action: CascadeAction }>;
33
+ readonly affected: readonly string[];
34
+ readonly tornDown: readonly string[];
35
+ readonly rebuilt: readonly string[];
36
+ readonly failed: readonly string[];
37
+ readonly abortWaited: boolean;
38
+ readonly abortTimedOut: boolean;
39
+ readonly durationMs: number;
40
+ }
41
+
42
+ export interface UnitSnapshot {
43
+ readonly token: string;
44
+ readonly state: UnitState;
45
+ readonly error?: string;
46
+ readonly everActive: boolean;
47
+ readonly inFlight: boolean;
48
+ }
49
+
50
+ export interface UnitStateChange {
51
+ readonly token: string;
52
+ readonly state: UnitState;
53
+ readonly error?: string;
54
+ }
55
+
56
+ export interface CascadeEngineOptions {
57
+ onWillCascade?: (
58
+ affected: readonly ScopedToken[],
59
+ reason: string,
60
+ ) => void | Promise<void>;
61
+ readonly abortWaitMs?: number;
62
+ readonly resolveTimeoutMs?: number;
63
+ readonly historyCapacity?: number;
64
+ readonly now?: () => number;
65
+ }
66
+
67
+ export interface CascadeHost {
68
+ isRegistered(token: ServiceIdentifier<any>): boolean;
69
+ ownerScopeOf(token: ServiceIdentifier<any>): object | undefined;
70
+ isMaterialized(token: ServiceIdentifier<any>): boolean;
71
+ materialize(token: ServiceIdentifier<any>): unknown;
72
+ retire(token: ServiceIdentifier<any>): void | Promise<void>;
73
+ applyProvide(
74
+ token: ServiceIdentifier<any>,
75
+ descriptor: SyncDescriptor<unknown>,
76
+ config: unknown,
77
+ ): number;
78
+ applyProvideInstance(
79
+ token: ServiceIdentifier<any>,
80
+ instance: unknown,
81
+ config: unknown,
82
+ ): number;
83
+ applyUnprovide(token: ServiceIdentifier<any>): void;
84
+ recipeOf(token: ServiceIdentifier<any>): SyncDescriptor<unknown> | undefined;
85
+ dependenciesOf(
86
+ recipe: SyncDescriptor<unknown>,
87
+ ): Array<ServiceIdentifier<any>>;
88
+ }
89
+
90
+ export interface CascadeScopeHandle {
91
+ readonly cascade: CascadeEngine;
92
+ readonly cascadeDisposed: boolean;
93
+ readonly cascadeDepth: number;
94
+ }
95
+
96
+ interface UnitRecord {
97
+ state: UnitState;
98
+ error?: unknown;
99
+ activation: UnitActivation;
100
+ everActive: boolean;
101
+ }
102
+
103
+ interface QueuedRequest {
104
+ readonly engine: CascadeEngine;
105
+ readonly change: CascadeChange;
106
+ readonly resolve: () => void;
107
+ readonly reject: (error: unknown) => void;
108
+ }
109
+
110
+ const DEFAULT_ABORT_WAIT_MS = 5000;
111
+ const DEFAULT_RESOLVE_TIMEOUT_MS = 30000;
112
+ const DEFAULT_HISTORY_CAPACITY = 200;
113
+
114
+ export class CascadeTree {
115
+ readonly graph: DependencyGraph;
116
+ readonly queue: QueuedRequest[] = [];
117
+ readonly engines = new Set<CascadeEngine>();
118
+ running = false;
119
+ orchestrator: object | undefined;
120
+ private readonly _inFlight = new PairIndex<true>();
121
+ private _settleWaiters: Array<() => void> = [];
122
+ private readonly _scopeSeq = new Map<object, number>();
123
+ private _nextScopeSeq = 0;
124
+ private readonly _droppedScopes = new WeakSet<object>();
125
+ private readonly _onDidAddEngine = new Emitter<CascadeEngine>();
126
+ readonly onDidAddEngine: Event<CascadeEngine> = this._onDidAddEngine.event;
127
+ private readonly _onDidRemoveEngine = new Emitter<CascadeEngine>();
128
+ readonly onDidRemoveEngine: Event<CascadeEngine> = this._onDidRemoveEngine.event;
129
+
130
+ constructor(graph: DependencyGraph) {
131
+ this.graph = graph;
132
+ }
133
+
134
+ addEngine(engine: CascadeEngine): void {
135
+ this.engines.add(engine);
136
+ this._onDidAddEngine.fire(engine);
137
+ }
138
+
139
+ removeEngine(engine: CascadeEngine): void {
140
+ if (this.engines.delete(engine)) {
141
+ this._onDidRemoveEngine.fire(engine);
142
+ }
143
+ }
144
+
145
+ inFlightSet(ref: ScopedToken, on: boolean): void {
146
+ if (on) {
147
+ this._inFlight.set(ref.scope, ref.token, true);
148
+ } else {
149
+ this._inFlight.delete(ref.scope, ref.token);
150
+ }
151
+ }
152
+
153
+ inFlightHas(ref: ScopedToken): boolean {
154
+ return this._inFlight.get(ref.scope, ref.token) !== undefined;
155
+ }
156
+
157
+ inFlightClear(refs: Iterable<ScopedToken>): void {
158
+ for (const ref of refs) {
159
+ this._inFlight.delete(ref.scope, ref.token);
160
+ }
161
+ }
162
+
163
+ seqOf(scope: object): number {
164
+ let seq = this._scopeSeq.get(scope);
165
+ if (seq === undefined) {
166
+ if (this._droppedScopes.has(scope)) {
167
+ return -1;
168
+ }
169
+ seq = this._nextScopeSeq++;
170
+ this._scopeSeq.set(scope, seq);
171
+ }
172
+ return seq;
173
+ }
174
+
175
+ dropScope(scope: object): void {
176
+ this._droppedScopes.add(scope);
177
+ this._scopeSeq.delete(scope);
178
+ this._inFlight.deleteScope(scope);
179
+ }
180
+
181
+ addSettleWaiter(waiter: () => void): void {
182
+ this._settleWaiters.push(waiter);
183
+ }
184
+
185
+ fireSettleWaiters(): void {
186
+ const waiters = this._settleWaiters.splice(0);
187
+ for (const waiter of waiters) {
188
+ waiter();
189
+ }
190
+ }
191
+ }
192
+
193
+ export class CascadeEngine {
194
+ private readonly _units = new Map<
195
+ ServiceIdentifier<any>,
196
+ UnitRecord
197
+ >();
198
+ private readonly _pendingIndex = new Map<
199
+ ServiceIdentifier<any>,
200
+ Set<ServiceIdentifier<any>>
201
+ >();
202
+ private readonly _history: CascadeHistoryEntry[] = [];
203
+ private _historySeq = 0;
204
+ private _disposed = false;
205
+ private _activationSuspended = 0;
206
+ private readonly _onDidChangeUnitState = new Emitter<UnitStateChange>();
207
+ readonly onDidChangeUnitState: Event<UnitStateChange> = this._onDidChangeUnitState.event;
208
+ private readonly _onDidCascade = new Emitter<CascadeHistoryEntry>();
209
+ readonly onDidCascade: Event<CascadeHistoryEntry> = this._onDidCascade.event;
210
+
211
+ constructor(
212
+ private readonly _host: CascadeHost,
213
+ private readonly _scope: CascadeScopeHandle,
214
+ private readonly _tree: CascadeTree,
215
+ private _options: CascadeEngineOptions = {},
216
+ ) {
217
+ this._tree.addEngine(this);
218
+ }
219
+
220
+ configure(options: CascadeEngineOptions): void {
221
+ this._options = { ...this._options, ...options };
222
+ }
223
+
224
+ stateOf(token: ServiceIdentifier<any>): UnitState | undefined {
225
+ return this._units.get(token)?.state;
226
+ }
227
+
228
+ activationOf(token: ServiceIdentifier<any>): UnitActivation | undefined {
229
+ return this._units.get(token)?.activation;
230
+ }
231
+
232
+ materializable(token: ServiceIdentifier<any>): boolean {
233
+ return this._host.recipeOf(token) !== undefined;
234
+ }
235
+
236
+ failureOf(token: ServiceIdentifier<any>): unknown {
237
+ const unit = this._units.get(token);
238
+ return unit?.state === 'Failed' ? unit.error : undefined;
239
+ }
240
+
241
+ isInFlight(token: ServiceIdentifier<any>): boolean {
242
+ const owner = this._host.ownerScopeOf(token) ?? this._scope;
243
+ return this._tree.inFlightHas({ scope: owner, token });
244
+ }
245
+
246
+ history(): readonly CascadeHistoryEntry[] {
247
+ return this._history;
248
+ }
249
+
250
+ pendingSnapshot(): ReadonlyMap<string, readonly string[]> {
251
+ const snapshot = new Map<string, readonly string[]>();
252
+ for (const [token, unit] of this._units) {
253
+ if (unit.state !== 'Pending') continue;
254
+ snapshot.set(
255
+ token.toString(),
256
+ this._missingDeps(token).map((dep) => dep.toString()),
257
+ );
258
+ }
259
+ return snapshot;
260
+ }
261
+
262
+ unitsSnapshot(): UnitSnapshot[] {
263
+ const snapshot: UnitSnapshot[] = [];
264
+ for (const [token, unit] of this._units) {
265
+ snapshot.push({
266
+ token: token.toString(),
267
+ state: unit.state,
268
+ error: unit.error === undefined ? undefined : serializeError(unit.error),
269
+ everActive: unit.everActive,
270
+ inFlight: this.isInFlight(token),
271
+ });
272
+ }
273
+ return snapshot;
274
+ }
275
+
276
+ submit(change: CascadeChange): Promise<void> {
277
+ if (this._disposed) {
278
+ return Promise.resolve();
279
+ }
280
+ return new Promise<void>((resolve, reject) => {
281
+ this._tree.queue.push({ engine: this, change, resolve, reject });
282
+ this._pump();
283
+ });
284
+ }
285
+
286
+ submitAll(changes: readonly CascadeChange[]): Promise<void> {
287
+ if (this._disposed || changes.length === 0) {
288
+ return Promise.resolve();
289
+ }
290
+ return new Promise<void>((resolve, reject) => {
291
+ for (const change of changes) {
292
+ this._tree.queue.push({ engine: this, change, resolve, reject });
293
+ }
294
+ this._pump();
295
+ });
296
+ }
297
+
298
+ update(token: ServiceIdentifier<any>, reason?: string): Promise<void> {
299
+ return this.submit({
300
+ action: 'update',
301
+ token,
302
+ reason: reason ?? `update ${String(token)}`,
303
+ });
304
+ }
305
+
306
+ whenIdle(): Promise<void> {
307
+ if (!this._tree.running && this._tree.queue.length === 0) {
308
+ return Promise.resolve();
309
+ }
310
+ return new Promise<void>((resolve) => {
311
+ this._tree.addSettleWaiter(() => {
312
+ void this.whenIdle().then(resolve);
313
+ });
314
+ });
315
+ }
316
+
317
+ suspendActivation(): void {
318
+ this._activationSuspended++;
319
+ }
320
+
321
+ resumeActivation(): void {
322
+ if (this._activationSuspended === 0) {
323
+ return;
324
+ }
325
+ this._activationSuspended--;
326
+ if (this._activationSuspended > 0 || this._disposed) {
327
+ return;
328
+ }
329
+ const rebuilt: string[] = [];
330
+ const failed: string[] = [];
331
+ this._recheckTreeFixpoint(rebuilt, failed);
332
+ }
333
+
334
+ private _recheckTreeFixpoint(rebuilt: string[], failed: string[]): void {
335
+ const enginesInOrder = [...this._tree.engines]
336
+ .filter((engine) => !engine._disposed)
337
+ .toSorted((a, b) => a._scope.cascadeDepth - b._scope.cascadeDepth);
338
+ for (;;) {
339
+ let progress = false;
340
+ for (const engine of enginesInOrder) {
341
+ const before = rebuilt.length + failed.length;
342
+ engine._recheckForCascade(rebuilt, failed);
343
+ if (rebuilt.length + failed.length > before) {
344
+ progress = true;
345
+ }
346
+ }
347
+ if (!progress) {
348
+ break;
349
+ }
350
+ }
351
+ }
352
+
353
+ resolveWhenAvailable<T>(
354
+ token: ServiceIdentifier<any>,
355
+ timeoutMs?: number,
356
+ ): Promise<T> {
357
+ if (!this.isInFlight(token)) {
358
+ try {
359
+ return Promise.resolve(this._host.materialize(token) as T);
360
+ } catch (error) {
361
+ return Promise.reject(error);
362
+ }
363
+ }
364
+ return new Promise<T>((resolve, reject) => {
365
+ const timer = setTimeout(() => {
366
+ reject(
367
+ new CascadeConflictError(
368
+ String(token),
369
+ 'timed out waiting for the in-flight cascade to settle',
370
+ ),
371
+ );
372
+ }, timeoutMs ?? this._options.resolveTimeoutMs ?? DEFAULT_RESOLVE_TIMEOUT_MS);
373
+ this._tree.addSettleWaiter(() => {
374
+ clearTimeout(timer);
375
+ try {
376
+ resolve(this._host.materialize(token) as T);
377
+ } catch (error) {
378
+ reject(error);
379
+ }
380
+ });
381
+ });
382
+ }
383
+
384
+ observedMaterialization(token: ServiceIdentifier<any>): void {
385
+ const unit = this._units.get(token);
386
+ if (unit !== undefined && unit.state === 'Pending') {
387
+ unit.everActive = true;
388
+ this._setUnitState(token, unit, 'Active', undefined);
389
+ }
390
+ }
391
+
392
+ dispose(): void {
393
+ this._disposed = true;
394
+ this._tree.removeEngine(this);
395
+ this._tree.dropScope(this._scope);
396
+ this._units.clear();
397
+ this._pendingIndex.clear();
398
+ const remaining: QueuedRequest[] = [];
399
+ for (const request of this._tree.queue) {
400
+ if (request.engine === this) {
401
+ request.resolve();
402
+ } else {
403
+ remaining.push(request);
404
+ }
405
+ }
406
+ this._tree.queue.length = 0;
407
+ this._tree.queue.push(...remaining);
408
+ this._onDidChangeUnitState.dispose();
409
+ this._onDidCascade.dispose();
410
+ }
411
+
412
+ _teardownForCascade(
413
+ token: ServiceIdentifier<any>,
414
+ tornDown: string[],
415
+ parkAsPending: boolean,
416
+ ): void | Promise<void> {
417
+ if (this._disposed || !this._host.isMaterialized(token)) {
418
+ return undefined;
419
+ }
420
+ this._setUnitState(token, this._unitFor(token), 'Unloading', undefined);
421
+ const out = this._host.retire(token);
422
+ tornDown.push(this._label({ scope: this._scope, token }));
423
+ if (parkAsPending) {
424
+ this._markPending(token, undefined, true);
425
+ }
426
+ return out;
427
+ }
428
+
429
+ _recheckForCascade(rebuilt: string[], failed: string[]): void {
430
+ if (this._disposed || this._activationSuspended > 0) {
431
+ return;
432
+ }
433
+ this._recheckPending(rebuilt, failed);
434
+ }
435
+
436
+ _applyChangeForCascade(change: CascadeChange): void {
437
+ if (this._disposed) {
438
+ return;
439
+ }
440
+ switch (change.action) {
441
+ case 'provide':
442
+ if (change.descriptor !== undefined) {
443
+ this._host.applyProvide(change.token, change.descriptor, change.config);
444
+ this._markPending(change.token, change.activation ?? 'eager', false);
445
+ } else {
446
+ this._host.applyProvideInstance(change.token, change.instance, change.config);
447
+ const unit = this._unitFor(change.token);
448
+ unit.everActive = true;
449
+ this._setUnitState(change.token, unit, 'Active', undefined);
450
+ }
451
+ break;
452
+ case 'unprovide':
453
+ this._host.applyUnprovide(change.token);
454
+ this._units.delete(change.token);
455
+ break;
456
+ case 'update':
457
+ this._markPending(change.token, undefined, true);
458
+ break;
459
+ }
460
+ }
461
+
462
+ private _pump(): void {
463
+ if (this._tree.running) {
464
+ return;
465
+ }
466
+ const batch = this._tree.queue.splice(0);
467
+ if (batch.length === 0) {
468
+ return;
469
+ }
470
+ this._tree.running = true;
471
+ const finish = (error?: unknown): void => {
472
+ this._tree.running = false;
473
+ for (const request of batch) {
474
+ if (error === undefined) {
475
+ request.resolve();
476
+ } else {
477
+ request.reject(error);
478
+ }
479
+ }
480
+ this._tree.fireSettleWaiters();
481
+ this._pump();
482
+ };
483
+ const orchestrator = batch[0]!.engine;
484
+ try {
485
+ const out = orchestrator._transact(batch);
486
+ if (isPromiseLike(out)) {
487
+ Promise.resolve(out).then(
488
+ () => { finish(); },
489
+ (error: unknown) => { finish(error); },
490
+ );
491
+ } else {
492
+ finish();
493
+ }
494
+ } catch (error) {
495
+ finish(error);
496
+ }
497
+ }
498
+
499
+ private _transact(batch: QueuedRequest[]): void | Promise<void> {
500
+ const changes = mergeBatch(batch);
501
+ const started = this._options.now?.() ?? Date.now();
502
+ const reason = changes.map(({ change }) => change.reason).join('; ');
503
+ const affected = this._tree.graph.affectedSet(
504
+ changes.map(({ engine, change }) => ({ scope: engine._scope, token: change.token })),
505
+ );
506
+ for (const ref of affected) {
507
+ this._tree.inFlightSet(ref, true);
508
+ }
509
+ this._tree.orchestrator = this._scope;
510
+ const complete = (abort: { waited: boolean; timedOut: boolean }): void | Promise<void> => {
511
+ const clear = (): void => {
512
+ this._tree.inFlightClear(affected);
513
+ this._tree.orchestrator = undefined;
514
+ };
515
+ let out: void | Promise<void>;
516
+ try {
517
+ out = this._applyTransaction(changes, affected, reason, started, abort);
518
+ } catch (error) {
519
+ clear();
520
+ throw error;
521
+ }
522
+ if (isPromiseLike(out)) {
523
+ return Promise.resolve(out).then(clear, (error: unknown) => {
524
+ clear();
525
+ throw error;
526
+ });
527
+ }
528
+ clear();
529
+ return undefined;
530
+ };
531
+ const wait = this._waitForAbort(affected, reason);
532
+ if (isPromiseLike(wait)) {
533
+ return Promise.resolve(wait).then(complete);
534
+ }
535
+ return complete(wait);
536
+ }
537
+
538
+ private _waitForAbort(
539
+ affected: readonly ScopedToken[],
540
+ reason: string,
541
+ ): { waited: boolean; timedOut: boolean } | Promise<{ waited: boolean; timedOut: boolean }> {
542
+ const hook = this._options.onWillCascade;
543
+ if (hook === undefined) {
544
+ return { waited: false, timedOut: false };
545
+ }
546
+ let out: void | Promise<void>;
547
+ try {
548
+ out = hook(affected, reason);
549
+ } catch (error) {
550
+ onUnexpectedError(error);
551
+ return { waited: false, timedOut: false };
552
+ }
553
+ if (!isPromiseLike(out)) {
554
+ return { waited: false, timedOut: false };
555
+ }
556
+ const waitMs = this._options.abortWaitMs ?? DEFAULT_ABORT_WAIT_MS;
557
+ return Promise.race([
558
+ Promise.resolve(out).then(
559
+ () => ({ waited: true, timedOut: false }),
560
+ (error: unknown) => {
561
+ onUnexpectedError(error);
562
+ return { waited: true, timedOut: false };
563
+ },
564
+ ),
565
+ new Promise<{ waited: boolean; timedOut: boolean }>((resolve) => {
566
+ setTimeout(() => { resolve({ waited: true, timedOut: true }); }, waitMs);
567
+ }),
568
+ ]);
569
+ }
570
+
571
+ private _applyTransaction(
572
+ changes: QueuedRequest[],
573
+ affected: readonly ScopedToken[],
574
+ reason: string,
575
+ started: number,
576
+ abort: { waited: boolean; timedOut: boolean },
577
+ ): void | Promise<void> {
578
+ const tornDown: string[] = [];
579
+ const rebuilt: string[] = [];
580
+ const failed: string[] = [];
581
+
582
+ const teardownOrder = this._tree.graph.reverseTopoOrder(affected);
583
+ let index = 0;
584
+ const step = (): void | Promise<void> => {
585
+ while (index < teardownOrder.length) {
586
+ const ref = teardownOrder[index]!;
587
+ index += 1;
588
+ const owner = engineOf(ref);
589
+ if (owner === undefined || owner._disposed) {
590
+ continue;
591
+ }
592
+ const removed = changes.some(
593
+ ({ engine, change }) =>
594
+ engine === owner && change.token === ref.token && change.action === 'unprovide',
595
+ );
596
+ const out = owner._teardownForCascade(ref.token, tornDown, !removed);
597
+ if (isPromiseLike(out)) {
598
+ return Promise.resolve(out).then(step);
599
+ }
600
+ }
601
+ return undefined;
602
+ };
603
+
604
+ const after = (): void => {
605
+ for (const { engine, change } of changes) {
606
+ engine._applyChangeForCascade(change);
607
+ }
608
+ this._recheckTreeFixpoint(rebuilt, failed);
609
+ this._pushHistory({
610
+ seq: ++this._historySeq,
611
+ reason,
612
+ changes: changes.map(({ engine, change }) => ({
613
+ token: this._label({ scope: engine._scope, token: change.token }),
614
+ action: change.action,
615
+ })),
616
+ affected: affected.map((ref) => this._label(ref)),
617
+ tornDown,
618
+ rebuilt,
619
+ failed,
620
+ abortWaited: abort.waited,
621
+ abortTimedOut: abort.timedOut,
622
+ durationMs: (this._options.now?.() ?? Date.now()) - started,
623
+ });
624
+ };
625
+
626
+ const drained = step();
627
+ if (isPromiseLike(drained)) {
628
+ return Promise.resolve(drained).then(after);
629
+ }
630
+ after();
631
+ return undefined;
632
+ }
633
+
634
+ private _unitFor(token: ServiceIdentifier<any>): UnitRecord {
635
+ let unit = this._units.get(token);
636
+ if (unit === undefined) {
637
+ unit = { state: 'Pending', activation: 'eager', everActive: false };
638
+ this._units.set(token, unit);
639
+ this._onDidChangeUnitState.fire({ token: token.toString(), state: 'Pending' });
640
+ }
641
+ return unit;
642
+ }
643
+
644
+ private _setUnitState(
645
+ token: ServiceIdentifier<any>,
646
+ unit: UnitRecord,
647
+ state: UnitState,
648
+ error: unknown,
649
+ ): void {
650
+ const changed = unit.state !== state;
651
+ unit.state = state;
652
+ unit.error = error;
653
+ if (changed) {
654
+ this._onDidChangeUnitState.fire({
655
+ token: token.toString(),
656
+ state,
657
+ error: error === undefined ? undefined : serializeError(error),
658
+ });
659
+ }
660
+ }
661
+
662
+ private _markPending(
663
+ token: ServiceIdentifier<any>,
664
+ activation?: UnitActivation,
665
+ everActive?: boolean,
666
+ ): void {
667
+ const unit = this._unitFor(token);
668
+ this._setUnitState(token, unit, 'Pending', undefined);
669
+ if (activation !== undefined) {
670
+ unit.activation = activation;
671
+ }
672
+ if (everActive !== undefined) {
673
+ unit.everActive = everActive;
674
+ }
675
+ }
676
+
677
+ private _recheckPending(rebuilt: string[], failed: string[]): void {
678
+ for (;;) {
679
+ this._pendingIndex.clear();
680
+ const satisfied: ServiceIdentifier<any>[] = [];
681
+ for (const [token, unit] of this._units) {
682
+ if (unit.state !== 'Pending') continue;
683
+ const missing = this._missingDeps(token);
684
+ if (missing.length === 0) {
685
+ if (unit.everActive || unit.activation === 'eager') {
686
+ satisfied.push(token);
687
+ }
688
+ } else {
689
+ for (const dep of missing) {
690
+ let waiters = this._pendingIndex.get(dep);
691
+ if (waiters === undefined) {
692
+ waiters = new Set();
693
+ this._pendingIndex.set(dep, waiters);
694
+ }
695
+ waiters.add(token);
696
+ }
697
+ }
698
+ }
699
+ if (satisfied.length === 0) {
700
+ return;
701
+ }
702
+ const ordered = this._tree.graph.topoOrder(
703
+ satisfied.map((token) => ({ scope: this._scope, token })),
704
+ );
705
+ for (const ref of ordered) {
706
+ this._activate(ref.token, rebuilt, failed);
707
+ }
708
+ for (const [token, unit] of this._units) {
709
+ if (
710
+ (unit.state === 'Pending' || unit.state === 'Activating') &&
711
+ this._host.isMaterialized(token)
712
+ ) {
713
+ unit.everActive = true;
714
+ this._setUnitState(token, unit, 'Active', undefined);
715
+ }
716
+ }
717
+ }
718
+ }
719
+
720
+ private _activate(token: ServiceIdentifier<any>, rebuilt: string[], failed: string[]): void {
721
+ const unit = this._unitFor(token);
722
+ this._setUnitState(token, unit, 'Activating', undefined);
723
+ try {
724
+ this._host.materialize(token);
725
+ unit.everActive = true;
726
+ this._setUnitState(token, unit, 'Active', undefined);
727
+ rebuilt.push(this._label({ scope: this._scope, token }));
728
+ } catch (error) {
729
+ this._setUnitState(token, unit, 'Failed', error);
730
+ failed.push(this._label({ scope: this._scope, token }));
731
+ }
732
+ }
733
+
734
+ private _missingDeps(token: ServiceIdentifier<any>): Array<ServiceIdentifier<any>> {
735
+ const recipe = this._host.recipeOf(token);
736
+ if (recipe === undefined) {
737
+ return [token];
738
+ }
739
+ return this._host
740
+ .dependenciesOf(recipe)
741
+ .filter((dep) => !this._isAvailable(dep));
742
+ }
743
+
744
+ private _isAvailable(dep: ServiceIdentifier<any>): boolean {
745
+ if (!this._host.isRegistered(dep)) {
746
+ return false;
747
+ }
748
+ const owner = this._host.ownerScopeOf(dep);
749
+ const engine = owner === undefined ? undefined : engineOf({ scope: owner, token: dep });
750
+ const state = engine?.stateOf(dep);
751
+ if (state === undefined || state === 'Active') {
752
+ return true;
753
+ }
754
+ return (
755
+ state === 'Pending' &&
756
+ engine !== undefined &&
757
+ engine.activationOf(dep) === 'ondemand' &&
758
+ engine.materializable(dep)
759
+ );
760
+ }
761
+
762
+ private _label(ref: ScopedToken): string {
763
+ if (ref.scope === this._tree.orchestrator) {
764
+ return ref.token.toString();
765
+ }
766
+ return `#${this._tree.seqOf(ref.scope)}:${ref.token.toString()}`;
767
+ }
768
+
769
+ _mergeKey(): string {
770
+ return String(this._tree.seqOf(this._scope));
771
+ }
772
+
773
+ private _pushHistory(entry: CascadeHistoryEntry): void {
774
+ this._history.push(entry);
775
+ const capacity = this._options.historyCapacity ?? DEFAULT_HISTORY_CAPACITY;
776
+ if (this._history.length > capacity) {
777
+ this._history.splice(0, this._history.length - capacity);
778
+ }
779
+ this._onDidCascade.fire(entry);
780
+ }
781
+ }
782
+
783
+ function serializeError(error: unknown): string {
784
+ return error instanceof Error ? error.message : String(error);
785
+ }
786
+
787
+ function engineOf(ref: ScopedToken): CascadeEngine | undefined {
788
+ return (ref.scope as Partial<CascadeScopeHandle>).cascade;
789
+ }
790
+
791
+ function mergeBatch(batch: QueuedRequest[]): QueuedRequest[] {
792
+ const byKey = new Map<string, QueuedRequest>();
793
+ for (const request of batch) {
794
+ const key = `${request.engine._mergeKey()}:${request.change.token.toString()}`;
795
+ byKey.set(key, request);
796
+ }
797
+ return [...byKey.values()];
798
+ }
packages/agent-core-v2/src/_base/di/collection.ts ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Emitter, type Event } from '../event';
2
+ import type { Ledger } from '../lifecycle/ledger';
3
+ import { storeCustomDependency, type ServiceIdentifier } from './instantiation';
4
+
5
+ export interface CollectionToken<T> {
6
+ (target: any, key: string | symbol | undefined, index: number): void;
7
+
8
+ readonly name: string;
9
+
10
+ readonly __t?: T;
11
+
12
+ toString(): string;
13
+ }
14
+
15
+ export interface DefinitionToken<T> extends CollectionToken<T> {
16
+ readonly __definition?: T;
17
+ }
18
+
19
+ export interface DefinitionRecord<T> {
20
+ readonly definition: T;
21
+ readonly owner: string;
22
+ readonly generation: number;
23
+ }
24
+
25
+ export interface DefinitionChange<T> {
26
+ readonly current: DefinitionRecord<T> | undefined;
27
+ readonly previous: DefinitionRecord<T> | undefined;
28
+ }
29
+
30
+ export interface DefinitionView<T> {
31
+ readonly current: DefinitionRecord<T> | undefined;
32
+ readonly onDidChangeDefinition: Event<DefinitionChange<T>>;
33
+ }
34
+
35
+ const _collectionTokens = new Map<string, CollectionToken<unknown>>();
36
+ const _collectionTokenSet = new WeakSet<object>();
37
+ const _definitionTokenSet = new WeakSet<object>();
38
+ const _collectionValidators = new WeakMap<
39
+ object,
40
+ (value: unknown, existing: readonly unknown[]) => void
41
+ >();
42
+
43
+ export function collection<T>(
44
+ name: string,
45
+ options: { readonly validate?: (value: T, existing: readonly T[]) => void } = {},
46
+ ): CollectionToken<T> {
47
+ const existing = _collectionTokens.get(name);
48
+ if (existing !== undefined) {
49
+ return existing as CollectionToken<T>;
50
+ }
51
+ const token = function collectionDecorator(
52
+ target: any,
53
+ _key: string | symbol | undefined,
54
+ index: number,
55
+ ): void {
56
+ if (arguments.length !== 3) {
57
+ throw new Error('@CollectionToken-decorator can only be used to decorate a parameter');
58
+ }
59
+ storeCustomDependency(token as unknown as ServiceIdentifier<any>, 'collection', target, index);
60
+ } as unknown as CollectionToken<T>;
61
+ Object.defineProperty(token, 'toString', {
62
+ value: () => `collection:${name}`,
63
+ enumerable: false,
64
+ });
65
+ Object.defineProperty(token, 'name', { value: name, enumerable: false, configurable: true });
66
+ _collectionTokens.set(name, token as CollectionToken<unknown>);
67
+ _collectionTokenSet.add(token);
68
+ if (options.validate !== undefined) {
69
+ _collectionValidators.set(
70
+ token,
71
+ options.validate as (value: unknown, existing: readonly unknown[]) => void,
72
+ );
73
+ }
74
+ return token;
75
+ }
76
+
77
+ export function definition<T>(name: string): DefinitionToken<T> {
78
+ const token = collection<T>(name, {
79
+ validate: (_value, existing) => {
80
+ if (existing.length > 0) throw new Error(`Definition ${name} already has an active provider`);
81
+ },
82
+ }) as DefinitionToken<T>;
83
+ _definitionTokenSet.add(token);
84
+ return token;
85
+ }
86
+
87
+ export function isCollectionToken(thing: unknown): thing is CollectionToken<unknown> {
88
+ return typeof thing === 'function' && _collectionTokenSet.has(thing);
89
+ }
90
+
91
+ export function isDefinitionToken(thing: unknown): thing is DefinitionToken<unknown> {
92
+ return typeof thing === 'function' && _definitionTokenSet.has(thing);
93
+ }
94
+
95
+ export interface CollectionRecord<T> {
96
+ readonly value: T;
97
+ readonly providerName: string;
98
+ readonly scopePath: string;
99
+ }
100
+
101
+ export interface CollectionChange<T> {
102
+ readonly added: readonly T[];
103
+ readonly removed: readonly T[];
104
+ }
105
+
106
+ export interface CollectionView<T> {
107
+ readonly items: readonly T[];
108
+ readonly records: readonly CollectionRecord<T>[];
109
+ readonly onDidChange: Event<CollectionChange<T>>;
110
+ }
111
+
112
+ interface StoredRecord {
113
+ readonly id: number;
114
+ readonly value: unknown;
115
+ readonly providerName: string;
116
+ readonly scopePath: string;
117
+ readonly provider: object;
118
+ readonly providerBook: Ledger;
119
+ }
120
+
121
+ export type { StoredRecord };
122
+
123
+ export class CollectionStore {
124
+ private readonly _records = new Map<
125
+ CollectionToken<unknown>,
126
+ Map<number, StoredRecord>
127
+ >();
128
+ private readonly _views = new Set<CollectionViewImpl<unknown>>();
129
+ private _nextId = 0;
130
+
131
+ constructor(private readonly _parentOf: (container: object) => object | undefined) {}
132
+
133
+ addRecord<T>(
134
+ token: CollectionToken<T>,
135
+ provider: object,
136
+ providerName: string,
137
+ scopePath: string,
138
+ providerBook: Ledger,
139
+ value: T,
140
+ ): () => void {
141
+ let records = this._records.get(token as CollectionToken<unknown>);
142
+ if (records === undefined) {
143
+ records = new Map();
144
+ this._records.set(token as CollectionToken<unknown>, records);
145
+ }
146
+ _collectionValidators.get(token)?.(
147
+ value,
148
+ [...records.values()].map((entry) => entry.value),
149
+ );
150
+ const record: StoredRecord = {
151
+ id: ++this._nextId,
152
+ value,
153
+ providerName,
154
+ scopePath,
155
+ provider,
156
+ providerBook,
157
+ };
158
+ records.set(record.id, record);
159
+ const fire = (view: CollectionViewImpl<unknown>, kind: 'added' | 'removed'): void => {
160
+ if (view.consumer === provider || this._isRelated(view.consumer, provider)) {
161
+ view._fireDelta(kind, [record]);
162
+ }
163
+ };
164
+ for (const view of this._views) {
165
+ if (view.token === (token as unknown as CollectionToken<unknown>)) {
166
+ fire(view, 'added');
167
+ }
168
+ }
169
+ return () => {
170
+ if (!records.delete(record.id)) {
171
+ return;
172
+ }
173
+ for (const view of this._views) {
174
+ if (view.token === (token as unknown as CollectionToken<unknown>)) {
175
+ fire(view, 'removed');
176
+ }
177
+ }
178
+ };
179
+ }
180
+
181
+ createView<T>(token: CollectionToken<T>, consumer: object): CollectionViewImpl<T> {
182
+ const view = new CollectionViewImpl<T>(this, token, consumer);
183
+ this._views.add(view as unknown as CollectionViewImpl<unknown>);
184
+ return view;
185
+ }
186
+
187
+ dropView(view: CollectionViewImpl<unknown>): void {
188
+ this._views.delete(view);
189
+ }
190
+
191
+ recordsFor<T>(token: CollectionToken<T>, consumer: object): CollectionRecord<T>[] {
192
+ const records = this._records.get(token as CollectionToken<unknown>);
193
+ if (records === undefined) {
194
+ return [];
195
+ }
196
+ const out: CollectionRecord<T>[] = [];
197
+ for (const record of records.values()) {
198
+ if (record.provider === consumer || this._isRelated(consumer, record.provider)) {
199
+ out.push({
200
+ value: record.value as T,
201
+ providerName: record.providerName,
202
+ scopePath: record.scopePath,
203
+ });
204
+ }
205
+ }
206
+ return out;
207
+ }
208
+
209
+ storedRecordsFor(
210
+ token: CollectionToken<unknown>,
211
+ consumer: object,
212
+ ): readonly StoredRecord[] {
213
+ const records = this._records.get(token);
214
+ if (records === undefined) {
215
+ return [];
216
+ }
217
+ const out: StoredRecord[] = [];
218
+ for (const record of records.values()) {
219
+ if (record.provider === consumer || this._isRelated(consumer, record.provider)) {
220
+ out.push(record);
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+
226
+ definitionFor<T>(token: CollectionToken<T>, consumer: object): DefinitionRecord<T> | undefined {
227
+ const record = this.storedRecordsFor(
228
+ token as CollectionToken<unknown>,
229
+ consumer,
230
+ )[0];
231
+ if (record === undefined) return undefined;
232
+ return {
233
+ definition: record.value as T,
234
+ owner: `${record.providerName}@${record.scopePath}`,
235
+ generation: record.id,
236
+ };
237
+ }
238
+
239
+ private _isRelated(consumer: object, provider: object): boolean {
240
+ for (let c: object | undefined = consumer; c !== undefined; c = this._parentOf(c)) {
241
+ if (c === provider) return true;
242
+ }
243
+ for (let p: object | undefined = provider; p !== undefined; p = this._parentOf(p)) {
244
+ if (p === consumer) return true;
245
+ }
246
+ return false;
247
+ }
248
+ }
249
+
250
+ export class CollectionViewImpl<T> implements CollectionView<T>, DefinitionView<T> {
251
+ private readonly _onDidChange = new Emitter<CollectionChange<T>>();
252
+ private readonly _onDidChangeDefinition = new Emitter<DefinitionChange<T>>();
253
+ readonly onDidChange: Event<CollectionChange<T>> = this._onDidChange.event;
254
+ readonly onDidChangeDefinition: Event<DefinitionChange<T>> =
255
+ this._onDidChangeDefinition.event;
256
+
257
+ constructor(
258
+ private readonly _store: CollectionStore,
259
+ readonly token: CollectionToken<T>,
260
+ readonly consumer: object,
261
+ ) {}
262
+
263
+ get records(): CollectionRecord<T>[] {
264
+ return this._store.recordsFor(this.token, this.consumer);
265
+ }
266
+
267
+ get items(): readonly T[] {
268
+ return this.records.map((record) => record.value);
269
+ }
270
+
271
+ get current(): DefinitionRecord<T> | undefined {
272
+ return this._store.definitionFor(this.token, this.consumer);
273
+ }
274
+
275
+ _fireDelta(kind: 'added' | 'removed', records: readonly StoredRecord[]): void {
276
+ const previous = kind === 'removed' ? this.definitionRecord(records[0]) : undefined;
277
+ const values = records.map((record) => record.value as T);
278
+ this._onDidChange.fire(
279
+ kind === 'added'
280
+ ? { added: values, removed: [] }
281
+ : { added: [], removed: values },
282
+ );
283
+ if (isDefinitionToken(this.token)) {
284
+ this._onDidChangeDefinition.fire({ current: this.current, previous });
285
+ }
286
+ }
287
+
288
+ dispose(): void {
289
+ this._store.dropView(this as unknown as CollectionViewImpl<unknown>);
290
+ this._onDidChange.dispose();
291
+ this._onDidChangeDefinition.dispose();
292
+ }
293
+
294
+ private definitionRecord(record: StoredRecord | undefined): DefinitionRecord<T> | undefined {
295
+ if (record === undefined) return undefined;
296
+ return {
297
+ definition: record.value as T,
298
+ owner: `${record.providerName}@${record.scopePath}`,
299
+ generation: record.id,
300
+ };
301
+ }
302
+ }
packages/agent-core-v2/src/_base/di/dependencyGraph.ts ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ServiceIdentifier } from './instantiation';
2
+
3
+ export interface ScopedToken {
4
+ readonly scope: object;
5
+ readonly token: ServiceIdentifier<any>;
6
+ }
7
+
8
+ export type DependencyEdgeKind = 'instance' | 'collection';
9
+
10
+ export interface DependencyEdge {
11
+ readonly consumer: ScopedToken;
12
+ readonly dependency: ScopedToken;
13
+ readonly kind: DependencyEdgeKind;
14
+ }
15
+
16
+ export class PairIndex<V> {
17
+ private readonly _map = new Map<object, Map<
18
+ ServiceIdentifier<any>,
19
+ V
20
+ >>();
21
+
22
+ get(scope: object, token: ServiceIdentifier<unknown>): V | undefined {
23
+ return this._map.get(scope)?.get(token);
24
+ }
25
+
26
+ set(scope: object, token: ServiceIdentifier<unknown>, value: V): void {
27
+ let inner = this._map.get(scope);
28
+ if (inner === undefined) {
29
+ inner = new Map();
30
+ this._map.set(scope, inner);
31
+ }
32
+ inner.set(token, value);
33
+ }
34
+
35
+ delete(scope: object, token: ServiceIdentifier<unknown>): void {
36
+ const inner = this._map.get(scope);
37
+ if (inner === undefined) return;
38
+ inner.delete(token);
39
+ if (inner.size === 0) {
40
+ this._map.delete(scope);
41
+ }
42
+ }
43
+
44
+ deleteScope(scope: object): void {
45
+ this._map.delete(scope);
46
+ }
47
+
48
+ get size(): number {
49
+ return this._map.size;
50
+ }
51
+
52
+ entries(): Array<[ScopedToken, V]> {
53
+ const out: Array<[ScopedToken, V]> = [];
54
+ for (const [scope, inner] of this._map) {
55
+ for (const [token, value] of inner) {
56
+ out.push([{ scope, token }, value]);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ clear(): void {
63
+ this._map.clear();
64
+ }
65
+ }
66
+
67
+ export class DependencyGraph {
68
+ private readonly _refByInstance = new Map<object, ScopedToken>();
69
+ private readonly _instanceByRef = new PairIndex<object>();
70
+ private readonly _out = new Map<object, PairIndex<DependencyEdgeKind>>();
71
+ private readonly _in = new PairIndex<Map<object, DependencyEdgeKind>>();
72
+
73
+ addInstance(
74
+ instance: object,
75
+ scope: object,
76
+ token: ServiceIdentifier<any>,
77
+ ): void {
78
+ const ref: ScopedToken = { scope, token };
79
+ this._refByInstance.set(instance, ref);
80
+ this._instanceByRef.set(scope, token, instance);
81
+ }
82
+
83
+ removeInstance(instance: object): void {
84
+ const ref = this._refByInstance.get(instance);
85
+ if (ref !== undefined) {
86
+ this._refByInstance.delete(instance);
87
+ if (this._instanceByRef.get(ref.scope, ref.token) === instance) {
88
+ this._instanceByRef.delete(ref.scope, ref.token);
89
+ }
90
+ }
91
+ const out = this._out.get(instance);
92
+ if (out !== undefined) {
93
+ for (const [dependency] of out.entries()) {
94
+ const inbound = this._in.get(dependency.scope, dependency.token);
95
+ if (inbound !== undefined) {
96
+ inbound.delete(instance);
97
+ if (inbound.size === 0) {
98
+ this._in.delete(dependency.scope, dependency.token);
99
+ }
100
+ }
101
+ }
102
+ this._out.delete(instance);
103
+ }
104
+ }
105
+
106
+ removeScope(scope: object): void {
107
+ for (const [instance, ref] of this._refByInstance) {
108
+ if (ref.scope === scope) {
109
+ this.removeInstance(instance);
110
+ }
111
+ }
112
+ this._instanceByRef.deleteScope(scope);
113
+ this._in.deleteScope(scope);
114
+ for (const [instance, out] of this._out) {
115
+ out.deleteScope(scope);
116
+ if (out.size === 0) {
117
+ this._out.delete(instance);
118
+ }
119
+ }
120
+ }
121
+
122
+ addEdge(
123
+ consumerInstance: object,
124
+ dependency: ScopedToken,
125
+ kind: DependencyEdgeKind = 'instance',
126
+ ): void {
127
+ let out = this._out.get(consumerInstance);
128
+ if (out === undefined) {
129
+ out = new PairIndex();
130
+ this._out.set(consumerInstance, out);
131
+ }
132
+ out.set(dependency.scope, dependency.token, kind);
133
+
134
+ let inbound = this._in.get(dependency.scope, dependency.token);
135
+ if (inbound === undefined) {
136
+ inbound = new Map();
137
+ this._in.set(dependency.scope, dependency.token, inbound);
138
+ }
139
+ inbound.set(consumerInstance, kind);
140
+ }
141
+
142
+ affectedSet(changed: Iterable<ScopedToken>): ScopedToken[] {
143
+ const seen = new PairIndex<true>();
144
+ const queue: ScopedToken[] = [];
145
+ const push = (ref: ScopedToken): void => {
146
+ if (seen.get(ref.scope, ref.token) !== undefined) return;
147
+ seen.set(ref.scope, ref.token, true);
148
+ queue.push(ref);
149
+ };
150
+ for (const ref of changed) {
151
+ push(ref);
152
+ }
153
+ const affected: ScopedToken[] = [];
154
+ while (queue.length > 0) {
155
+ const ref = queue.pop()!;
156
+ affected.push(ref);
157
+ const inbound = this._in.get(ref.scope, ref.token);
158
+ if (inbound === undefined) continue;
159
+ for (const [consumerInstance, kind] of inbound) {
160
+ if (kind !== 'instance') continue;
161
+ const consumer = this._refByInstance.get(consumerInstance);
162
+ if (consumer === undefined) continue;
163
+ push(consumer);
164
+ }
165
+ }
166
+ return affected;
167
+ }
168
+
169
+ topoOrder(tokens: Iterable<ScopedToken>): ScopedToken[] {
170
+ const subset = new PairIndex<ScopedToken>();
171
+ for (const ref of tokens) {
172
+ subset.set(ref.scope, ref.token, ref);
173
+ }
174
+ const ordered: ScopedToken[] = [];
175
+ const done = new PairIndex<true>();
176
+ const visit = (ref: ScopedToken): void => {
177
+ if (done.get(ref.scope, ref.token) !== undefined) return;
178
+ done.set(ref.scope, ref.token, true);
179
+ for (const dependency of this._dependenciesOf(ref, subset)) {
180
+ visit(dependency);
181
+ }
182
+ ordered.push(ref);
183
+ };
184
+ for (const [, ref] of subset.entries()) {
185
+ visit(ref);
186
+ }
187
+ return ordered;
188
+ }
189
+
190
+ reverseTopoOrder(tokens: Iterable<ScopedToken>): ScopedToken[] {
191
+ return this.topoOrder(tokens).toReversed();
192
+ }
193
+
194
+ findCycle(label: (ref: ScopedToken) => string): string[] | null {
195
+ const state = new PairIndex<'visiting' | 'done'>();
196
+ const path: ScopedToken[] = [];
197
+ const visit = (ref: ScopedToken): string[] | null => {
198
+ state.set(ref.scope, ref.token, 'visiting');
199
+ path.push(ref);
200
+ for (const dependency of this._dependenciesOf(ref, undefined)) {
201
+ const mark = state.get(dependency.scope, dependency.token);
202
+ if (mark === 'done') continue;
203
+ if (mark === 'visiting') {
204
+ const start = path.findIndex(
205
+ (entry) =>
206
+ entry.scope === dependency.scope && entry.token === dependency.token,
207
+ );
208
+ return [...path.slice(start), dependency].map(label);
209
+ }
210
+ const cycle = visit(dependency);
211
+ if (cycle !== null) return cycle;
212
+ }
213
+ path.pop();
214
+ state.set(ref.scope, ref.token, 'done');
215
+ return null;
216
+ };
217
+ for (const [ref] of this._instanceByRef.entries()) {
218
+ if (state.get(ref.scope, ref.token) !== undefined) continue;
219
+ const cycle = visit(ref);
220
+ if (cycle !== null) return cycle;
221
+ }
222
+ return null;
223
+ }
224
+
225
+ edges(): DependencyEdge[] {
226
+ const edges: DependencyEdge[] = [];
227
+ for (const [consumerInstance, out] of this._out) {
228
+ const consumer = this._refByInstance.get(consumerInstance);
229
+ if (consumer === undefined) continue;
230
+ for (const [dependency, kind] of out.entries()) {
231
+ edges.push({ consumer, dependency, kind });
232
+ }
233
+ }
234
+ return edges;
235
+ }
236
+
237
+ clear(): void {
238
+ this._refByInstance.clear();
239
+ this._instanceByRef.clear();
240
+ this._out.clear();
241
+ this._in.clear();
242
+ }
243
+
244
+ private _dependenciesOf(
245
+ ref: ScopedToken,
246
+ subset: PairIndex<ScopedToken> | undefined,
247
+ ): ScopedToken[] {
248
+ const instance = this._instanceByRef.get(ref.scope, ref.token);
249
+ if (instance === undefined) return [];
250
+ const out = this._out.get(instance);
251
+ if (out === undefined) return [];
252
+ const result: ScopedToken[] = [];
253
+ for (const [dependency, kind] of out.entries()) {
254
+ if (kind !== 'instance') continue;
255
+ if (subset !== undefined && subset.get(dependency.scope, dependency.token) === undefined) {
256
+ continue;
257
+ }
258
+ result.push(dependency);
259
+ }
260
+ return result;
261
+ }
262
+ }
packages/agent-core-v2/src/_base/di/descriptors.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class SyncDescriptor<T> {
2
+ public readonly ctor: any;
3
+
4
+ constructor(
5
+ ctor: new (...args: any[]) => T,
6
+ public readonly staticArguments: ReadonlyArray<any> = [],
7
+ ) {
8
+ this.ctor = ctor;
9
+ }
10
+ }
11
+
12
+ export interface SyncDescriptor0<T> {
13
+ readonly ctor: new () => T;
14
+ }
packages/agent-core-v2/src/_base/di/errors.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Graph } from './graph';
2
+
3
+ export class CyclicDependencyError extends Error {
4
+ readonly path: ReadonlyArray<string>;
5
+
6
+ constructor(pathOrGraph: ReadonlyArray<string> | Graph<any>) {
7
+ if (Array.isArray(pathOrGraph)) {
8
+ const path = pathOrGraph as ReadonlyArray<string>;
9
+ super(`Cyclic DI dependency detected: ${path.join(' → ')}`);
10
+ this.path = path;
11
+ } else {
12
+ const graph = pathOrGraph as Graph<any>;
13
+ const cycle = graph.findCycleSlow();
14
+ const detail = cycle ?? `UNABLE to detect cycle, dumping graph:\n${graph.toString()}`;
15
+ super(`cyclic dependency between services: ${detail}`);
16
+ this.path = cycle ? cycle.split(' -> ') : [];
17
+ }
18
+ this.name = 'CyclicDependencyError';
19
+ }
20
+ }
21
+
22
+ export class CascadeConflictError extends Error {
23
+ constructor(
24
+ readonly token: string,
25
+ readonly detail: string,
26
+ ) {
27
+ super(`Cascade conflict resolving '${token}': ${detail}`);
28
+ this.name = 'CascadeConflictError';
29
+ }
30
+ }
packages/agent-core-v2/src/_base/di/fiber.ts ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { IDisposable } from './lifecycle';
2
+ import type { Emitter } from '../event';
3
+ import { isPromiseLike, type EffectBody } from '../lifecycle/disposer';
4
+ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
5
+ import {
6
+ collection,
7
+ isCollectionToken,
8
+ type CollectionToken,
9
+ type CollectionView,
10
+ } from './collection';
11
+ import { SyncDescriptor } from './descriptors';
12
+ import {
13
+ isServiceIdentifier,
14
+ ScopeActivation,
15
+ _util,
16
+ type LiveRef,
17
+ type ServiceIdentifier,
18
+ } from './instantiation';
19
+
20
+ export enum FiberState {
21
+ Pending = 0,
22
+ Activating = 1,
23
+ Active = 2,
24
+ Unloading = 3,
25
+ Failed = 4,
26
+ }
27
+
28
+ export interface ConfigSchema {
29
+ readonly '~standard': {
30
+ readonly validate: (value: unknown) => unknown;
31
+ };
32
+ }
33
+
34
+ export interface RecipeStatics {
35
+ readonly name?: string;
36
+ readonly inject?: readonly ServiceIdentifier<any>[];
37
+ readonly Config?: ConfigSchema;
38
+ readonly meta?: Record<string, unknown>;
39
+ }
40
+
41
+ export type ServiceClassRecipe =
42
+ (new (...args: any[]) => unknown) & RecipeStatics;
43
+
44
+ export type ServiceFunctionRecipe = ((
45
+ fiber: Fiber,
46
+ config?: any,
47
+ ) => any) &
48
+ RecipeStatics;
49
+
50
+ export type ServiceObjectRecipe = {
51
+ apply(
52
+ fiber: Fiber,
53
+ config?: any,
54
+ ): any;
55
+ } & RecipeStatics;
56
+
57
+ export type ServiceRecipe =
58
+ | ServiceClassRecipe
59
+ | ServiceFunctionRecipe
60
+ | ServiceObjectRecipe;
61
+
62
+ export interface FiberProvideOptions {
63
+ readonly config?: unknown;
64
+ readonly activation?: ScopeActivation;
65
+ }
66
+
67
+ export interface Fiber {
68
+ readonly name: string;
69
+ readonly state: FiberState;
70
+ readonly config: unknown;
71
+
72
+ provide<T>(
73
+ id: ServiceIdentifier<T>,
74
+ recipe: ServiceClassRecipe,
75
+ opts?: FiberProvideOptions,
76
+ ): FiberHandle<T>;
77
+ provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
78
+ provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
79
+ provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
80
+
81
+ effect(body: EffectBody, label?: string): FiberHandle;
82
+
83
+ on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle;
84
+
85
+ get<T>(id: ServiceIdentifier<T>): T;
86
+ ref<T>(id: ServiceIdentifier<T>): LiveRef<T>;
87
+ }
88
+
89
+ export interface FiberHandle<T = unknown> extends PromiseLike<FiberHandle<T>> {
90
+ readonly name: string;
91
+ readonly state: FiberState;
92
+ readonly uid: number;
93
+ update(config?: unknown): Promise<void>;
94
+ dispose(): Promise<void>;
95
+ }
96
+
97
+ export class FiberProtocolError extends Error {
98
+ constructor(detail: string) {
99
+ super(detail);
100
+ this.name = 'FiberProtocolError';
101
+ }
102
+ }
103
+
104
+ export class ServiceRecipeError extends Error {
105
+ constructor(detail: string) {
106
+ super(detail);
107
+ this.name = 'ServiceRecipeError';
108
+ }
109
+ }
110
+
111
+ export interface ConstructionFrame {
112
+ readonly ctor: new (...args: any[]) => any;
113
+ readonly config: unknown;
114
+ readonly token: ServiceIdentifier<any> | undefined;
115
+ readonly host: FiberHost;
116
+ }
117
+
118
+ const _constructionStack: ConstructionFrame[] = [];
119
+
120
+ export function pushConstructionFrame(frame: ConstructionFrame): void {
121
+ _constructionStack.push(frame);
122
+ }
123
+
124
+ export function popConstructionFrame(): void {
125
+ _constructionStack.pop();
126
+ }
127
+
128
+ export function currentConstruction(): ConstructionFrame | undefined {
129
+ return _constructionStack.at(-1);
130
+ }
131
+
132
+ export const SERVICE_MARK = Symbol('serviceUnit');
133
+
134
+ export function isServiceRecipe(ctor: any): ctor is ServiceClassRecipe {
135
+ return typeof ctor === 'function' && ctor.prototype?.[SERVICE_MARK] === true;
136
+ }
137
+
138
+ export function isClassRecipe(recipe: unknown): recipe is ServiceClassRecipe {
139
+ return (
140
+ typeof recipe === 'function' &&
141
+ Object.prototype.hasOwnProperty.call(recipe, 'prototype')
142
+ );
143
+ }
144
+
145
+ export type BufferedOp = (runtime: Fiber) => void;
146
+
147
+ export interface UnitInternals {
148
+ readonly unitBook: Ledger;
149
+ takeUnitBuffer(): BufferedOp[] | null;
150
+ setUnitRuntime(runtime: Fiber): void;
151
+ }
152
+
153
+ export interface FiberHost {
154
+ mintUid(): number;
155
+ provideToken<T>(
156
+ id: ServiceIdentifier<T>,
157
+ descriptor: SyncDescriptor<T>,
158
+ options: {
159
+ readonly activation: 'eager' | 'ondemand';
160
+ readonly config?: unknown;
161
+ },
162
+ ): TokenProvideCore;
163
+ provideTokenInstance<T>(id: ServiceIdentifier<T>, instance: T): TokenProvideCore;
164
+ tokenState(id: ServiceIdentifier<any>): string | undefined;
165
+ updateToken(id: ServiceIdentifier<any>, config: unknown, hasConfig: boolean): Promise<void>;
166
+ resolveTokenWhenAvailable<T>(id: ServiceIdentifier<T>): Promise<T>;
167
+ resolveInstance<T>(id: ServiceIdentifier<T>): T;
168
+ materializedInstance<T>(id: ServiceIdentifier<T>): T | undefined;
169
+ liveRef<T>(id: ServiceIdentifier<T>): LiveRef<T>;
170
+ recordInstanceEdge(node: object | undefined, id: ServiceIdentifier<any>): void;
171
+ collectionView<T>(token: CollectionToken<T>): CollectionView<T>;
172
+ addCollectionRecord<T>(
173
+ token: CollectionToken<T>,
174
+ providerName: string,
175
+ providerBook: Ledger,
176
+ value: T,
177
+ ): () => void;
178
+ constructService<T>(ctor: new (...args: any[]) => T, config: unknown): T;
179
+ }
180
+
181
+ export interface TokenProvideCore {
182
+ uid(): number;
183
+ dispose(): Promise<void>;
184
+ release(): void;
185
+ }
186
+
187
+ export type FiberEventResolver = (
188
+ host: FiberHost,
189
+ event: string,
190
+ handler: (e: any) => void,
191
+ ) => IDisposable;
192
+
193
+ let _eventResolver: FiberEventResolver | undefined;
194
+
195
+ export function setFiberEventResolver(resolver: FiberEventResolver | undefined): void {
196
+ _eventResolver = resolver;
197
+ }
198
+
199
+ export function bindServiceUnit(instance: UnitInternals & IDisposable, frame: ConstructionFrame): void {
200
+ const buffer = instance.takeUnitBuffer();
201
+ if (buffer === null) {
202
+ return;
203
+ }
204
+ const ctor = (instance as any).constructor as ServiceClassRecipe;
205
+ const runtime = new FiberRuntime(
206
+ frame.host,
207
+ instance.unitBook,
208
+ recipeName(ctor),
209
+ frame.config,
210
+ frame.token,
211
+ new Set(_util.getInstanceDependencies(ctor as unknown as _util.DI_TARGET_OBJ).map((d) => d.id)),
212
+ instance,
213
+ );
214
+ instance.setUnitRuntime(runtime);
215
+ try {
216
+ for (const op of buffer) {
217
+ op(runtime);
218
+ }
219
+ } catch (error) {
220
+ instance.dispose();
221
+ throw error;
222
+ }
223
+ }
224
+
225
+ function recipeName(recipe: RecipeStatics & { name?: string }): string {
226
+ return recipe.name ?? 'anonymous';
227
+ }
228
+
229
+ function validateConfig(schema: ConfigSchema | undefined, config: unknown, name: string): unknown {
230
+ if (schema === undefined) {
231
+ return config;
232
+ }
233
+ const out = schema['~standard'].validate(config);
234
+ if (isPromiseLike(out)) {
235
+ throw new ServiceRecipeError(`config schema of unit '${name}' must validate synchronously`);
236
+ }
237
+ const result = out as {
238
+ readonly value?: unknown;
239
+ readonly issues?: ReadonlyArray<{ readonly message: string }>;
240
+ };
241
+ if (result.issues !== undefined && result.issues.length > 0) {
242
+ throw new ServiceRecipeError(
243
+ `invalid config for unit '${name}': ${result.issues.map((issue) => issue.message).join('; ')}`,
244
+ );
245
+ }
246
+ return 'value' in result ? result.value : config;
247
+ }
248
+
249
+ function mapUnitState(state: string | undefined): FiberState {
250
+ if (state === undefined) {
251
+ return FiberState.Active;
252
+ }
253
+ switch (state) {
254
+ case 'Pending':
255
+ return FiberState.Pending;
256
+ case 'Activating':
257
+ return FiberState.Activating;
258
+ case 'Unloading':
259
+ return FiberState.Unloading;
260
+ case 'Failed':
261
+ return FiberState.Failed;
262
+ default:
263
+ return FiberState.Active;
264
+ }
265
+ }
266
+
267
+ export class FiberRuntime implements Fiber {
268
+ constructor(
269
+ private readonly _host: FiberHost,
270
+ private readonly _book: Ledger,
271
+ readonly name: string,
272
+ readonly config: unknown,
273
+ private readonly _token: ServiceIdentifier<any> | undefined,
274
+ private readonly _declared: ReadonlySet<ServiceIdentifier<any>>,
275
+ private readonly _edgeNode: object | undefined,
276
+ ) {}
277
+
278
+ get state(): FiberState {
279
+ if (this._token !== undefined) {
280
+ return mapUnitState(this._host.tokenState(this._token));
281
+ }
282
+ return FiberState.Active;
283
+ }
284
+
285
+ provide<T>(
286
+ id: ServiceIdentifier<T>,
287
+ recipe: ServiceClassRecipe,
288
+ opts?: FiberProvideOptions,
289
+ ): FiberHandle<T>;
290
+ provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
291
+ provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
292
+ provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
293
+ provide(
294
+ first: ServiceIdentifier<any> | ServiceRecipe | CollectionToken<any>,
295
+ second?: any,
296
+ third?: FiberProvideOptions,
297
+ ): FiberHandle {
298
+ if (isCollectionToken(first)) {
299
+ return this._provideRecord(first, second);
300
+ }
301
+ if (isServiceIdentifier(first)) {
302
+ if (isClassRecipe(second)) {
303
+ return this._provideToken(first, second, third);
304
+ }
305
+ if (
306
+ typeof second === 'function' ||
307
+ (second !== null && typeof second === 'object' && typeof second.apply === 'function')
308
+ ) {
309
+ throw new ServiceRecipeError(
310
+ `token-bound provide of '${String(first)}' requires a class recipe or an instance (function/object recipes are only valid for the anonymous form)`,
311
+ );
312
+ }
313
+ return this._provideTokenInstance(first, second);
314
+ }
315
+ return this._provideAnonymous(first as ServiceRecipe, second as FiberProvideOptions | undefined);
316
+ }
317
+
318
+ effect(body: EffectBody, label?: string): FiberHandle {
319
+ const entry = this._book.effect(body, label ?? `effect:${this.name}`);
320
+ return new BasicFiberHandle({
321
+ name: label ?? `effect:${this.name}`,
322
+ uid: this._host.mintUid(),
323
+ state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
324
+ update: async () => {
325
+ await entry.dispose();
326
+ if (this._book.isActive) {
327
+ this._book.effect(body, label ?? `effect:${this.name}`);
328
+ }
329
+ },
330
+ dispose: async () => {
331
+ await entry.dispose();
332
+ },
333
+ whenActive: () => Promise.resolve(),
334
+ });
335
+ }
336
+
337
+ on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
338
+ let subscription: IDisposable;
339
+ if (typeof event === 'string') {
340
+ if (_eventResolver === undefined) {
341
+ throw new FiberProtocolError(
342
+ `no event resolver registered — cannot subscribe to '${event}'`,
343
+ );
344
+ }
345
+ subscription = _eventResolver(this._host, event, handler);
346
+ } else if (typeof event?.event === 'function') {
347
+ subscription = event.event(handler);
348
+ } else {
349
+ throw new FiberProtocolError(`unsupported event source for unit '${this.name}'`);
350
+ }
351
+ const label = typeof event === 'string' ? `on:${event}` : `on:${event.constructor?.name ?? 'emitter'}`;
352
+ const entry = this._book.register(() => {
353
+ subscription.dispose();
354
+ }, label);
355
+ return new BasicFiberHandle({
356
+ name: label,
357
+ uid: this._host.mintUid(),
358
+ state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
359
+ update: async () => {},
360
+ dispose: async () => {
361
+ await entry.dispose();
362
+ },
363
+ whenActive: () => Promise.resolve(),
364
+ });
365
+ }
366
+
367
+ get<T>(id: ServiceIdentifier<T>): T {
368
+ if (!this._declared.has(id)) {
369
+ throw new FiberProtocolError(
370
+ `unit '${this.name}' resolves undeclared dependency '${String(id)}' — declare it as a constructor parameter (class recipe) or in the inject static (function recipe)`,
371
+ );
372
+ }
373
+ this._host.recordInstanceEdge(this._edgeNode, id);
374
+ return this._host.resolveInstance(id);
375
+ }
376
+
377
+ ref<T>(id: ServiceIdentifier<T>): LiveRef<T> {
378
+ return this._host.liveRef(id);
379
+ }
380
+
381
+ private _provideToken<T>(
382
+ id: ServiceIdentifier<T>,
383
+ recipe: ServiceClassRecipe,
384
+ opts: FiberProvideOptions | undefined,
385
+ ): FiberHandle<T> {
386
+ if (!isClassRecipe(recipe)) {
387
+ throw new ServiceRecipeError(
388
+ `token-bound provide of '${String(id)}' requires a class recipe (function/object recipes are only valid for the anonymous form)`,
389
+ );
390
+ }
391
+ const name = recipe.name ?? String(id);
392
+ const config = validateConfig(recipe.Config, opts?.config, name);
393
+ const core = this._host.provideToken(
394
+ id,
395
+ new SyncDescriptor<T>(recipe as new (...args: any[]) => T),
396
+ {
397
+ activation: opts?.activation === ScopeActivation.OnDemand ? 'ondemand' : 'eager',
398
+ config,
399
+ },
400
+ );
401
+ const entry = this._book.register(() => core.dispose(), `provide:${String(id)}`);
402
+ return new BasicFiberHandle<T>({
403
+ name,
404
+ uid: undefined,
405
+ uidOf: () => core.uid(),
406
+ state: () => mapUnitState(this._host.tokenState(id)),
407
+ update: (next) => {
408
+ const hasConfig = next !== undefined;
409
+ return this._host.updateToken(
410
+ id,
411
+ hasConfig ? validateConfig(recipe.Config, next, name) : undefined,
412
+ hasConfig,
413
+ );
414
+ },
415
+ dispose: async () => {
416
+ await entry.dispose();
417
+ },
418
+ whenActive: () => this._host.resolveTokenWhenAvailable(id).then(() => undefined),
419
+ });
420
+ }
421
+
422
+ private _provideTokenInstance<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T> {
423
+ const core = this._host.provideTokenInstance(id, instance);
424
+ const entry = this._book.register(() => core.dispose(), `provide:${String(id)}`);
425
+ return new BasicFiberHandle<T>({
426
+ name: String(id),
427
+ uid: undefined,
428
+ uidOf: () => core.uid(),
429
+ state: () => mapUnitState(this._host.tokenState(id)),
430
+ update: () => this._host.updateToken(id, undefined, false),
431
+ dispose: async () => {
432
+ await entry.dispose();
433
+ },
434
+ whenActive: () => this._host.resolveTokenWhenAvailable(id).then(() => undefined),
435
+ });
436
+ }
437
+
438
+ private _provideAnonymous(recipe: ServiceRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
439
+ if (isClassRecipe(recipe)) {
440
+ return this._provideAnonymousClass(recipe, opts);
441
+ }
442
+ return this._provideFunction(recipe, opts);
443
+ }
444
+
445
+ private _provideAnonymousClass(recipe: ServiceClassRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
446
+ const name = recipeName(recipe);
447
+ let config = validateConfig(recipe.Config, opts?.config, name);
448
+ let state = FiberState.Activating;
449
+ let failure: unknown;
450
+ let instance: unknown;
451
+ let entry: LedgerEntry | undefined;
452
+ const construct = (nextConfig: unknown): void => {
453
+ state = FiberState.Activating;
454
+ instance = this._host.constructService(recipe, nextConfig);
455
+ entry = this._book.register(() => {
456
+ state = FiberState.Unloading;
457
+ (instance as Partial<IDisposable>).dispose?.();
458
+ }, `provide:${name}`);
459
+ state = FiberState.Active;
460
+ };
461
+ try {
462
+ construct(config);
463
+ } catch (error) {
464
+ state = FiberState.Failed;
465
+ failure = error;
466
+ throw error;
467
+ }
468
+ for (const dependency of _util.getInstanceDependencies(recipe as unknown as _util.DI_TARGET_OBJ)) {
469
+ this._host.recordInstanceEdge(this._edgeNode, dependency.id);
470
+ }
471
+ return new BasicFiberHandle({
472
+ name,
473
+ uid: this._host.mintUid(),
474
+ state: () => state,
475
+ update: async (next) => {
476
+ await entry?.dispose();
477
+ entry = undefined;
478
+ try {
479
+ if (next !== undefined) {
480
+ config = validateConfig(recipe.Config, next, name);
481
+ }
482
+ construct(config);
483
+ } catch (error) {
484
+ state = FiberState.Failed;
485
+ failure = error;
486
+ throw error;
487
+ }
488
+ },
489
+ dispose: async () => {
490
+ await entry?.dispose();
491
+ entry = undefined;
492
+ },
493
+ whenActive: () => (failure !== undefined ? Promise.reject(failure) : Promise.resolve()),
494
+ });
495
+ }
496
+
497
+ private _provideFunction(recipe: ServiceFunctionRecipe | ServiceObjectRecipe, opts: FiberProvideOptions | undefined): FiberHandle {
498
+ const name = recipeName(recipe);
499
+ const config = validateConfig(recipe.Config, opts?.config, name);
500
+ const book = new Ledger(`unit:${name}`);
501
+ const anchor = this._book.register((reason) => book.teardown(reason), `provide:${name}`);
502
+ const facade = new FiberRuntime(
503
+ this._host,
504
+ book,
505
+ name,
506
+ config,
507
+ undefined,
508
+ new Set(recipe.inject ?? []),
509
+ this._edgeNode,
510
+ );
511
+ const run = (): void => {
512
+ let out: unknown;
513
+ if (typeof recipe === 'function') {
514
+ out = recipe(facade, config);
515
+ } else {
516
+ out = recipe.apply(facade, config);
517
+ }
518
+ book.effect((() => out) as EffectBody, `effect:${name}`);
519
+ };
520
+ try {
521
+ run();
522
+ } catch (error) {
523
+ void book.teardown('unload');
524
+ anchor.release();
525
+ throw error;
526
+ }
527
+ return new BasicFiberHandle({
528
+ name,
529
+ uid: this._host.mintUid(),
530
+ state: () => (anchor.disposed ? FiberState.Unloading : FiberState.Active),
531
+ update: async () => {
532
+ await book.clear('unload');
533
+ try {
534
+ run();
535
+ } catch (error) {
536
+ void book.teardown('unload');
537
+ anchor.release();
538
+ throw error;
539
+ }
540
+ },
541
+ dispose: async () => {
542
+ await anchor.dispose();
543
+ },
544
+ whenActive: () => Promise.resolve(),
545
+ });
546
+ }
547
+
548
+ private _provideRecord<T>(token: CollectionToken<T>, value: T): FiberHandle {
549
+ const remove = this._host.addCollectionRecord(token, this.name, this._book, value);
550
+ const entry = this._book.register(() => {
551
+ remove();
552
+ }, `provide:${token.name}`);
553
+ return new BasicFiberHandle({
554
+ name: `${this.name}→${token.name}`,
555
+ uid: this._host.mintUid(),
556
+ state: () => (entry.disposed ? FiberState.Unloading : FiberState.Active),
557
+ update: async () => {},
558
+ dispose: async () => {
559
+ await entry.dispose();
560
+ },
561
+ whenActive: () => Promise.resolve(),
562
+ });
563
+ }
564
+ }
565
+
566
+ interface BasicHandleParts {
567
+ readonly name: string;
568
+ readonly uid?: number;
569
+ readonly uidOf?: () => number;
570
+ readonly state: () => FiberState;
571
+ readonly update: (config: unknown) => Promise<void>;
572
+ readonly dispose: () => Promise<void>;
573
+ readonly whenActive: () => Promise<void>;
574
+ }
575
+
576
+ class BasicFiberHandle<T> implements FiberHandle<T> {
577
+ constructor(private readonly _parts: BasicHandleParts) {}
578
+
579
+ get name(): string {
580
+ return this._parts.name;
581
+ }
582
+
583
+ get state(): FiberState {
584
+ return this._parts.state();
585
+ }
586
+
587
+ get uid(): number {
588
+ if (this._parts.uidOf !== undefined) {
589
+ return this._parts.uidOf();
590
+ }
591
+ return this._parts.uid!;
592
+ }
593
+
594
+ update(config?: unknown): Promise<void> {
595
+ return this._parts.update(config);
596
+ }
597
+
598
+ dispose(): Promise<void> {
599
+ return this._parts.dispose();
600
+ }
601
+
602
+ // eslint-disable-next-line eslint-plugin-unicorn(no-thenable)
603
+ then<TResult1 = FiberHandle<T>, TResult2 = never>(
604
+ onfulfilled?: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null,
605
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
606
+ ): PromiseLike<TResult1 | TResult2> {
607
+ return thenSettle(this._parts.whenActive().then(() => settledView(this)), onfulfilled, onrejected);
608
+ }
609
+ }
610
+
611
+ interface SettledHandleView {
612
+ readonly name: string;
613
+ readonly state: FiberState;
614
+ readonly uid: number;
615
+ update(config?: unknown): Promise<void>;
616
+ dispose(): Promise<void>;
617
+ }
618
+
619
+ function settledView<T>(handle: FiberHandle<T>): SettledHandleView {
620
+ return {
621
+ get name() {
622
+ return handle.name;
623
+ },
624
+ get state() {
625
+ return handle.state;
626
+ },
627
+ get uid() {
628
+ return handle.uid;
629
+ },
630
+ update: (config?: unknown) => handle.update(config),
631
+ dispose: () => handle.dispose(),
632
+ };
633
+ }
634
+
635
+ function thenSettle<T, TResult1, TResult2>(
636
+ settled: Promise<SettledHandleView>,
637
+ onfulfilled: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null | undefined,
638
+ onrejected: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined,
639
+ ): PromiseLike<TResult1 | TResult2> {
640
+ return settled.then(
641
+ onfulfilled as unknown as (value: SettledHandleView) => TResult1 | PromiseLike<TResult1>,
642
+ onrejected,
643
+ );
644
+ }
645
+
646
+ export class PendingFiberHandle<T> implements FiberHandle<T> {
647
+ private _real: FiberHandle<T> | undefined;
648
+ private _disposed = false;
649
+
650
+ constructor(private readonly _pendingName: string) {}
651
+
652
+ attach(real: FiberHandle<T>): void {
653
+ if (this._disposed) {
654
+ void real.dispose();
655
+ return;
656
+ }
657
+ this._real = real;
658
+ }
659
+
660
+ get name(): string {
661
+ return this._real?.name ?? this._pendingName;
662
+ }
663
+
664
+ get state(): FiberState {
665
+ return this._real?.state ?? FiberState.Activating;
666
+ }
667
+
668
+ get uid(): number {
669
+ if (this._real === undefined) {
670
+ throw new FiberProtocolError(
671
+ `handle '${this._pendingName}' has not been flushed yet (construction protocol)`,
672
+ );
673
+ }
674
+ return this._real.uid;
675
+ }
676
+
677
+ update(config?: unknown): Promise<void> {
678
+ if (this._real === undefined) {
679
+ return Promise.reject(
680
+ new FiberProtocolError(`handle '${this._pendingName}' has not been flushed yet`),
681
+ );
682
+ }
683
+ return this._real.update(config);
684
+ }
685
+
686
+ async dispose(): Promise<void> {
687
+ this._disposed = true;
688
+ await this._real?.dispose();
689
+ }
690
+
691
+ // eslint-disable-next-line eslint-plugin-unicorn(no-thenable) — FiberHandle is a thenable by design
692
+ then<TResult1 = FiberHandle<T>, TResult2 = never>(
693
+ onfulfilled?: ((value: FiberHandle<T>) => TResult1 | PromiseLike<TResult1>) | null,
694
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
695
+ ): PromiseLike<TResult1 | TResult2> {
696
+ if (this._real !== undefined) {
697
+ return this._real.then(onfulfilled, onrejected);
698
+ }
699
+ return thenSettle(Promise.resolve(settledView(this)), onfulfilled, onrejected);
700
+ }
701
+ }
702
+
703
+ const _scopeUnitsTokens = new Map<string, CollectionToken<ServiceRecipe>>();
704
+
705
+ export function ScopeUnits(kind: string): CollectionToken<ServiceRecipe> {
706
+ let token = _scopeUnitsTokens.get(kind);
707
+ if (token === undefined) {
708
+ token = collection<ServiceRecipe>(`scope-units:${kind}`);
709
+ _scopeUnitsTokens.set(kind, token);
710
+ }
711
+ return token;
712
+ }
713
+
714
+ export type { EffectBody };
packages/agent-core-v2/src/_base/di/graph.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class Node<T> {
2
+ readonly incoming = new Map<string, Node<T>>();
3
+ readonly outgoing = new Map<string, Node<T>>();
4
+
5
+ constructor(
6
+ readonly key: string,
7
+ readonly data: T
8
+ ) { }
9
+ }
10
+
11
+ export class Graph<T> {
12
+ private readonly _nodes = new Map<string, Node<T>>();
13
+
14
+ constructor(private readonly _hashFn: (element: T) => string) { }
15
+
16
+ roots(): Node<T>[] {
17
+ const ret: Node<T>[] = [];
18
+ for (const node of this._nodes.values()) {
19
+ if (node.outgoing.size === 0) {
20
+ ret.push(node);
21
+ }
22
+ }
23
+ return ret;
24
+ }
25
+
26
+ insertEdge(from: T, to: T): void {
27
+ const fromNode = this.lookupOrInsertNode(from);
28
+ const toNode = this.lookupOrInsertNode(to);
29
+ fromNode.outgoing.set(toNode.key, toNode);
30
+ toNode.incoming.set(fromNode.key, fromNode);
31
+ }
32
+
33
+ removeNode(data: T): void {
34
+ const key = this._hashFn(data);
35
+ this._nodes.delete(key);
36
+ for (const node of this._nodes.values()) {
37
+ node.outgoing.delete(key);
38
+ node.incoming.delete(key);
39
+ }
40
+ }
41
+
42
+ lookupOrInsertNode(data: T): Node<T> {
43
+ const key = this._hashFn(data);
44
+ let node = this._nodes.get(key);
45
+ if (!node) {
46
+ node = new Node(key, data);
47
+ this._nodes.set(key, node);
48
+ }
49
+ return node;
50
+ }
51
+
52
+ isEmpty(): boolean {
53
+ return this._nodes.size === 0;
54
+ }
55
+
56
+ toString(): string {
57
+ const data: string[] = [];
58
+ for (const [key, value] of this._nodes) {
59
+ data.push(`${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[...value.outgoing.keys()].join(',')}]\n`);
60
+ }
61
+ return data.join('\n');
62
+ }
63
+
64
+ findCycleSlow() {
65
+ for (const [id, node] of this._nodes) {
66
+ const seen = new Set<string>([id]);
67
+ const res = this._findCycle(node, seen);
68
+ if (res) {
69
+ return res;
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
76
+ for (const [id, outgoing] of node.outgoing) {
77
+ if (seen.has(id)) {
78
+ return [...seen, id].join(' -> ');
79
+ }
80
+ seen.add(id);
81
+ const value = this._findCycle(outgoing, seen);
82
+ if (value) {
83
+ return value;
84
+ }
85
+ seen.delete(id);
86
+ }
87
+ return undefined;
88
+ }
89
+ }
packages/agent-core-v2/src/_base/di/instantiation.ts ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { SyncDescriptor, SyncDescriptor0 } from './descriptors';
2
+ import type { CascadeEngine } from './cascadeEngine';
3
+ import type { Event } from '../event';
4
+ import type { DisposableStore, IDisposable } from './lifecycle';
5
+ import type { ServiceCollection } from './serviceCollection';
6
+
7
+ export type DependencyKind = 'instance' | 'collection' | 'ref';
8
+
9
+ export namespace _util {
10
+ export const serviceIds = new Map<string, ServiceIdentifier<any>>();
11
+ export const DI_TARGET = '$di$target';
12
+ export const DI_DEPENDENCIES = '$di$dependencies';
13
+
14
+ export interface ServiceDependency {
15
+ readonly id: ServiceIdentifier<any>;
16
+ readonly index: number;
17
+ readonly kind: DependencyKind;
18
+ }
19
+
20
+ export function getServiceDependencies(
21
+ ctor: DI_TARGET_OBJ,
22
+ ): ServiceDependency[] {
23
+ return ctor[DI_DEPENDENCIES] || [];
24
+ }
25
+
26
+ export function getInstanceDependencies(
27
+ ctor: DI_TARGET_OBJ,
28
+ ): ServiceDependency[] {
29
+ return getServiceDependencies(ctor).filter(
30
+ (dependency) => dependency.kind === 'instance',
31
+ );
32
+ }
33
+
34
+ export interface DI_TARGET_OBJ extends Function {
35
+ [DI_TARGET]: Function;
36
+ [DI_DEPENDENCIES]: { id: ServiceIdentifier<any>; index: number; kind: DependencyKind }[];
37
+ }
38
+ }
39
+
40
+ export type BrandedService = { _serviceBrand: undefined };
41
+
42
+ export interface IConstructorSignature<T, Args extends any[] = []> {
43
+ new <Services extends BrandedService[]>(...args: [...Args, ...Services]): T;
44
+ }
45
+
46
+ export type GetLeadingNonServiceArgs<TArgs extends any[]> =
47
+ TArgs extends [] ? []
48
+ : TArgs extends [...infer TFirst, BrandedService] ? GetLeadingNonServiceArgs<TFirst>
49
+ : TArgs;
50
+
51
+ export interface ServiceIdentifier<T> {
52
+ (target: any, key: string | symbol | undefined, index: number): void;
53
+
54
+ readonly type: T;
55
+
56
+ toString(): string;
57
+ }
58
+
59
+ function storeServiceDependency(
60
+ id: ServiceIdentifier<any>,
61
+ target: Function,
62
+ index: number,
63
+ kind: DependencyKind = 'instance',
64
+ ): void {
65
+ const t = target as _util.DI_TARGET_OBJ;
66
+ if (t[_util.DI_TARGET] === target) {
67
+ t[_util.DI_DEPENDENCIES].push({ id, index, kind });
68
+ } else {
69
+ t[_util.DI_DEPENDENCIES] = [{ id, index, kind }];
70
+ t[_util.DI_TARGET] = target;
71
+ }
72
+ }
73
+
74
+ export function storeCustomDependency(
75
+ id: ServiceIdentifier<any>,
76
+ kind: DependencyKind,
77
+ target: any,
78
+ index: number,
79
+ ): void {
80
+ storeServiceDependency(id, target, index, kind);
81
+ }
82
+
83
+ export function createDecorator<T>(name: string): ServiceIdentifier<T> {
84
+ const existing = _util.serviceIds.get(name);
85
+ if (existing) {
86
+ return existing as ServiceIdentifier<T>;
87
+ }
88
+
89
+ const id = function serviceDecorator(
90
+ target: any,
91
+ _key: string | symbol | undefined,
92
+ index: number,
93
+ ): void {
94
+ if (arguments.length !== 3) {
95
+ throw new Error(
96
+ '@IServiceName-decorator can only be used to decorate a parameter',
97
+ );
98
+ }
99
+ storeServiceDependency(id, target, index);
100
+ } as unknown as ServiceIdentifier<T>;
101
+
102
+ Object.defineProperty(id, 'toString', {
103
+ value: function toString(): string {
104
+ return name;
105
+ },
106
+ enumerable: false,
107
+ writable: false,
108
+ configurable: false,
109
+ });
110
+ Object.defineProperty(id, SERVICE_IDENTIFIER_MARK, { value: true, enumerable: false });
111
+
112
+ _util.serviceIds.set(name, id);
113
+ return id;
114
+ }
115
+
116
+ const SERVICE_IDENTIFIER_MARK = Symbol('serviceIdentifier');
117
+
118
+ export function isServiceIdentifier(thing: unknown): thing is ServiceIdentifier<unknown> {
119
+ return (
120
+ typeof thing === 'function' &&
121
+ (thing as unknown as Record<symbol, unknown>)[SERVICE_IDENTIFIER_MARK] === true
122
+ );
123
+ }
124
+
125
+ export function refineServiceDecorator<T1, T extends T1>(
126
+ serviceIdentifier: ServiceIdentifier<T1>,
127
+ ): ServiceIdentifier<T> {
128
+ return serviceIdentifier as ServiceIdentifier<T>;
129
+ }
130
+
131
+ export enum ScopeActivation {
132
+ OnScopeCreated = 0,
133
+ OnDemand = 1,
134
+ }
135
+
136
+ export interface LiveRef<T> {
137
+ readonly current: T | undefined;
138
+ readonly onDidChange: Event<void>;
139
+ }
140
+
141
+ export function ref<T>(
142
+ id: ServiceIdentifier<T>,
143
+ ): (target: object, key: string | symbol | undefined, index: number) => void {
144
+ return function refDecorator(target: any, _key: string | symbol | undefined, index: number): void {
145
+ if (arguments.length !== 3) {
146
+ throw new Error('@ref-decorator can only be used to decorate a parameter');
147
+ }
148
+ storeServiceDependency(id, target, index, 'ref');
149
+ };
150
+ }
151
+
152
+ export interface ServicesAccessor {
153
+ get<T>(id: ServiceIdentifier<T>): T;
154
+ }
155
+
156
+ export interface ProvideOptions {
157
+ readonly activation?: 'eager' | 'ondemand';
158
+ readonly config?: unknown;
159
+ }
160
+
161
+ export interface ProvideAllEntry<T = unknown> {
162
+ readonly id: ServiceIdentifier<T>;
163
+ readonly descriptor: SyncDescriptor<T>;
164
+ readonly options?: ProvideOptions;
165
+ }
166
+
167
+ export interface ProvideHandle extends IDisposable {
168
+ readonly uid: number;
169
+ }
170
+
171
+ export interface IInstantiationService {
172
+ readonly _serviceBrand: undefined;
173
+
174
+ readonly cascade: CascadeEngine;
175
+
176
+ invokeFunction<R, TS extends any[] = []>(
177
+ fn: (accessor: ServicesAccessor, ...args: TS) => R,
178
+ ...args: TS
179
+ ): R;
180
+ createInstance<T>(descriptor: SyncDescriptor0<T>): T;
181
+ createInstance<
182
+ Ctor extends new (
183
+ ...args: any[]
184
+ ) => unknown,
185
+ R extends InstanceType<Ctor>,
186
+ >(
187
+ ctor: Ctor,
188
+ ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>
189
+ ): R;
190
+ createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService;
191
+ provide<T>(
192
+ id: ServiceIdentifier<T>,
193
+ instanceOrDescriptor: T | SyncDescriptor<T>,
194
+ options?: ProvideOptions,
195
+ ): ProvideHandle;
196
+ provideAll(entries: ReadonlyArray<ProvideAllEntry>): void;
197
+ unprovide<T>(id: ServiceIdentifier<T>): void;
198
+ dispose(): void;
199
+ disposeAsync(): Promise<void>;
200
+ }
201
+
202
+ export const IInstantiationService: ServiceIdentifier<IInstantiationService> =
203
+ createDecorator<IInstantiationService>('instantiationService');
204
+
205
+ export interface ServiceCollectionLike {
206
+ set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: any): unknown;
207
+ get<T>(id: ServiceIdentifier<T>): any;
208
+ has(id: ServiceIdentifier<any>): boolean;
209
+ forEach(
210
+ callback: (id: ServiceIdentifier<any>, value: any) => void,
211
+ ): void;
212
+ }
packages/agent-core-v2/src/_base/di/instantiationService.ts ADDED
@@ -0,0 +1,1006 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SyncDescriptor } from './descriptors';
2
+ import { CascadeEngine, CascadeTree, type CascadeChange, type CascadeHost } from './cascadeEngine';
3
+ import {
4
+ CollectionStore,
5
+ type CollectionToken,
6
+ type CollectionView,
7
+ type CollectionViewImpl,
8
+ } from './collection';
9
+ import { DependencyGraph } from './dependencyGraph';
10
+ import { CascadeConflictError, CyclicDependencyError } from './errors';
11
+ import {
12
+ bindServiceUnit,
13
+ isServiceRecipe,
14
+ pushConstructionFrame,
15
+ popConstructionFrame,
16
+ type ConstructionFrame,
17
+ type FiberHost,
18
+ type TokenProvideCore,
19
+ type UnitInternals,
20
+ } from './fiber';
21
+ import { Graph } from './graph';
22
+ import {
23
+ IInstantiationService as IInstantiationServiceDecorator,
24
+ _util,
25
+ type IInstantiationService,
26
+ type LiveRef,
27
+ type ProvideAllEntry,
28
+ type ProvideHandle,
29
+ type ProvideOptions,
30
+ type ServiceIdentifier,
31
+ type ServicesAccessor,
32
+ } from './instantiation';
33
+ import { isDisposable, type DisposableStore, type IDisposable } from './lifecycle';
34
+ import { onUnexpectedError } from '../errors/unexpectedError';
35
+ import { Emitter } from '../event';
36
+ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
37
+ import type { Disposer } from '../lifecycle/disposer';
38
+ import { ServiceCollection } from './serviceCollection';
39
+
40
+ const enum TraceType {
41
+ None = 0,
42
+ Creation = 1,
43
+ Invocation = 2,
44
+ Branch = 3,
45
+ }
46
+
47
+ export class Trace {
48
+ static readonly all = new Set<string>();
49
+
50
+ private static readonly _None = new class extends Trace {
51
+ constructor() { super(TraceType.None, null); }
52
+ override stop() { }
53
+ override branch() { return this; }
54
+ };
55
+
56
+ static traceInvocation(_enableTracing: boolean, fn: any): Trace {
57
+ return !_enableTracing
58
+ ? Trace._None
59
+ : new Trace(
60
+ TraceType.Invocation,
61
+ fn.name ?? new Error('Trace invocation').stack!.split('\n').slice(3, 4).join('\n'),
62
+ );
63
+ }
64
+
65
+ static traceCreation(_enableTracing: boolean, ctor: any): Trace {
66
+ return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);
67
+ }
68
+
69
+ private static _totals: number = 0;
70
+ private readonly _start: number = Date.now();
71
+ private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];
72
+
73
+ private constructor(
74
+ readonly type: TraceType,
75
+ readonly name: string | null
76
+ ) { }
77
+
78
+ branch(id: ServiceIdentifier<any>, first: boolean): Trace {
79
+ const child = new Trace(TraceType.Branch, id.toString());
80
+ this._dep.push([id, first, child]);
81
+ return child;
82
+ }
83
+
84
+ stop() {
85
+ const dur = Date.now() - this._start;
86
+ Trace._totals += dur;
87
+
88
+ let causedCreation = false;
89
+
90
+ function printChild(n: number, trace: Trace) {
91
+ const res: string[] = [];
92
+ const prefix = '\t'.repeat(n);
93
+ for (const [id, first, child] of trace._dep) {
94
+ if (first && child) {
95
+ causedCreation = true;
96
+ res.push(`${prefix}CREATES -> ${String(id)}`);
97
+ const nested = printChild(n + 1, child);
98
+ if (nested) {
99
+ res.push(nested);
100
+ }
101
+ } else {
102
+ res.push(`${prefix}uses -> ${String(id)}`);
103
+ }
104
+ }
105
+ return res.join('\n');
106
+ }
107
+
108
+ const lines = [
109
+ `${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`,
110
+ printChild(1, this),
111
+ `DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`,
112
+ ];
113
+
114
+ if (dur > 2 || causedCreation) {
115
+ Trace.all.add(lines.join('\n'));
116
+ }
117
+ }
118
+
119
+ }
120
+
121
+ export class InstantiationService implements IInstantiationService {
122
+ declare readonly _serviceBrand: undefined;
123
+
124
+ readonly _globalGraph?: Graph<string>;
125
+
126
+ protected readonly _parent?: InstantiationService;
127
+
128
+ protected readonly _ledger = new Ledger('InstantiationService');
129
+
130
+ get dependencyGraph(): DependencyGraph {
131
+ return this._tree.graph;
132
+ }
133
+
134
+ private readonly _tree: CascadeTree;
135
+
136
+ readonly cascade: CascadeEngine;
137
+
138
+ private _parentLedgerEntry: LedgerEntry | undefined;
139
+
140
+ private readonly _instanceEntries = new Map<unknown, LedgerEntry>();
141
+
142
+ private readonly _provideEntries = new Map<
143
+ ServiceIdentifier<any>,
144
+ { readonly entry: LedgerEntry; readonly core: TokenProvideCore }
145
+ >();
146
+
147
+ private _cascadeResolving = false;
148
+
149
+ protected readonly _children = new Set<InstantiationService>();
150
+
151
+ private readonly _inProgress: ServiceIdentifier<any>[] = [];
152
+
153
+ private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
154
+
155
+ private readonly _collectionStore: CollectionStore;
156
+
157
+ private readonly _collectionViews = new Map<
158
+ CollectionToken<unknown>,
159
+ CollectionViewImpl<any>
160
+ >();
161
+
162
+ private readonly _edgeNodes = new Set<object>();
163
+
164
+ debugLabel: string | undefined;
165
+
166
+ private _fiberHost: FiberHost | undefined;
167
+
168
+ private _disposed = false;
169
+
170
+ constructor(
171
+ private readonly _services: ServiceCollection = new ServiceCollection(),
172
+ private readonly _strict: boolean = false,
173
+ parent?: InstantiationService,
174
+ protected readonly _enableTracing: boolean = false,
175
+ ) {
176
+ this._parent = parent;
177
+ this._globalGraph = _enableTracing ? parent?._globalGraph ?? new Graph(e => e) : undefined;
178
+ this._services.set(IInstantiationServiceDecorator, this);
179
+ this._tree = parent?._tree ?? new CascadeTree(new DependencyGraph());
180
+ this._collectionStore =
181
+ parent?._collectionStore ??
182
+ new CollectionStore((container) => (container as InstantiationService)._parent);
183
+ const host: CascadeHost = {
184
+ isRegistered: (token) => this._getServiceInstanceOrDescriptor(token) !== undefined,
185
+ ownerScopeOf: (token) => this._ownerOf(token),
186
+ isMaterialized: (token) => {
187
+ const value = this._services.get(token);
188
+ return value !== undefined && !(value instanceof SyncDescriptor);
189
+ },
190
+ materialize: (token) => {
191
+ this._cascadeResolving = true;
192
+ try {
193
+ return this._getOrCreateServiceInstance(
194
+ token,
195
+ Trace.traceCreation(false, CascadeEngine),
196
+ );
197
+ } finally {
198
+ this._cascadeResolving = false;
199
+ }
200
+ },
201
+ retire: (token) => this._retireUnit(token),
202
+ applyProvide: (token, descriptor, config) => {
203
+ this._services.set(token, descriptor, { config });
204
+ return this._services.uidOf(token)!;
205
+ },
206
+ applyProvideInstance: (token, instance, config) => {
207
+ this._services.set(token, instance, { config });
208
+ return this._services.uidOf(token)!;
209
+ },
210
+ applyUnprovide: (token) => {
211
+ this._services.delete(token);
212
+ },
213
+ recipeOf: (token) => {
214
+ const entry = this._services.entry(token);
215
+ if (entry === undefined) return undefined;
216
+ return entry.value instanceof SyncDescriptor ? entry.value : entry.recipe;
217
+ },
218
+ dependenciesOf: (recipe) =>
219
+ _util.getInstanceDependencies(recipe.ctor).map((dependency) => dependency.id),
220
+ };
221
+ this.cascade = new CascadeEngine(host, this, this._tree);
222
+ }
223
+
224
+ get cascadeDisposed(): boolean {
225
+ return this._disposed;
226
+ }
227
+
228
+ get cascadeDepth(): number {
229
+ return (this._parent?.cascadeDepth ?? -1) + 1;
230
+ }
231
+
232
+ private _ownerOf(id: ServiceIdentifier<any>): InstantiationService | undefined {
233
+ if (this._services.has(id)) {
234
+ return this;
235
+ }
236
+ return this._parent?._ownerOf(id);
237
+ }
238
+
239
+ invokeFunction<R, TS extends any[] = []>(
240
+ fn: (accessor: ServicesAccessor, ...args: TS) => R,
241
+ ...args: TS
242
+ ): R {
243
+ this._assertNotDisposed();
244
+ const _trace = Trace.traceInvocation(this._enableTracing, fn);
245
+ let done = false;
246
+ try {
247
+ const accessor: ServicesAccessor = {
248
+ get: <T>(id: ServiceIdentifier<T>): T => {
249
+ if (done) {
250
+ throw new Error(
251
+ 'service accessor is only valid during the invocation of its target method',
252
+ );
253
+ }
254
+ const result = this._getOrCreateServiceInstance(id, _trace);
255
+ if (!result) {
256
+ this._throwIfStrict(`[invokeFunction] unknown service '${String(id)}'`, false);
257
+ }
258
+ return result;
259
+ },
260
+ };
261
+ return fn(accessor, ...args);
262
+ } finally {
263
+ done = true;
264
+ _trace.stop();
265
+ }
266
+ }
267
+
268
+ provide<T>(
269
+ id: ServiceIdentifier<T>,
270
+ instanceOrDescriptor: T | SyncDescriptor<T>,
271
+ options?: ProvideOptions,
272
+ ): ProvideHandle {
273
+ this._assertNotDisposed();
274
+ const core = this._provideCore(id, instanceOrDescriptor, options);
275
+ const entry = this._ledger.register(() => {
276
+ void core.dispose();
277
+ }, `provide:${String(id)}`);
278
+ this._provideEntries.set(id, { entry, core });
279
+ return {
280
+ get uid(): number {
281
+ return core.uid();
282
+ },
283
+ dispose: () => {
284
+ void entry.dispose();
285
+ },
286
+ };
287
+ }
288
+
289
+ provideAll(entries: ReadonlyArray<ProvideAllEntry>): void {
290
+ this._assertNotDisposed();
291
+ const changes: CascadeChange[] = [];
292
+ for (const { id, descriptor, options } of entries) {
293
+ this._releaseProvideEntry(id);
294
+ changes.push({
295
+ action: 'provide',
296
+ token: id,
297
+ descriptor,
298
+ activation: options?.activation,
299
+ config: options?.config,
300
+ reason: `provide ${String(id)}`,
301
+ });
302
+ }
303
+ this.cascade.submitAll(changes).catch(onUnexpectedError);
304
+ for (const { id } of entries) {
305
+ const uid = this._services.uidOf(id);
306
+ if (uid === undefined) {
307
+ continue;
308
+ }
309
+ const entry = this._ledger.register(() => {
310
+ if (this._services.uidOf(id) === uid) {
311
+ void this._unprovideCore(id);
312
+ }
313
+ }, `provide:${String(id)}`);
314
+ this._provideEntries.set(id, {
315
+ entry,
316
+ core: {
317
+ uid: () => uid,
318
+ dispose: () => {
319
+ if (this._services.uidOf(id) === uid) {
320
+ return this._unprovideCore(id);
321
+ }
322
+ return Promise.resolve();
323
+ },
324
+ release: () => {},
325
+ },
326
+ });
327
+ }
328
+ }
329
+
330
+ private _provideCore<T>(
331
+ id: ServiceIdentifier<T>,
332
+ instanceOrDescriptor: T | SyncDescriptor<T>,
333
+ options?: ProvideOptions,
334
+ ): TokenProvideCore {
335
+ this._releaseProvideEntry(id);
336
+
337
+ if (
338
+ !(instanceOrDescriptor instanceof SyncDescriptor) &&
339
+ this._services.get(id) === instanceOrDescriptor
340
+ ) {
341
+ this._services.set(id, instanceOrDescriptor);
342
+ const uid = this._services.uidOf(id)!;
343
+ return {
344
+ uid: () => uid,
345
+ dispose: () => {
346
+ if (this._services.uidOf(id) === uid) {
347
+ return this._unprovideCore(id);
348
+ }
349
+ return Promise.resolve();
350
+ },
351
+ release: () => {},
352
+ };
353
+ }
354
+
355
+ const beforeUid = this._services.uidOf(id);
356
+ let appliedUid: number | undefined;
357
+ const availability = this._services.onDidChange(id, ({ newUid }) => {
358
+ if (newUid !== undefined && newUid !== beforeUid) {
359
+ appliedUid ??= newUid;
360
+ }
361
+ });
362
+ const release = (): void => {
363
+ availability.dispose();
364
+ };
365
+ const change: CascadeChange =
366
+ instanceOrDescriptor instanceof SyncDescriptor
367
+ ? {
368
+ action: 'provide',
369
+ token: id,
370
+ descriptor: instanceOrDescriptor,
371
+ activation: options?.activation,
372
+ config: options?.config,
373
+ reason: `provide ${String(id)}`,
374
+ }
375
+ : {
376
+ action: 'provide',
377
+ token: id,
378
+ instance: instanceOrDescriptor,
379
+ config: options?.config,
380
+ reason: `provide ${String(id)}`,
381
+ };
382
+ this.cascade.submit(change).catch(onUnexpectedError);
383
+ return {
384
+ uid: () => {
385
+ if (appliedUid === undefined) {
386
+ throw new Error(
387
+ `provide of '${String(id)}' has not been applied yet (cascade in flight)`,
388
+ );
389
+ }
390
+ return appliedUid;
391
+ },
392
+ dispose: () => {
393
+ release();
394
+ if (appliedUid !== undefined && this._services.uidOf(id) === appliedUid) {
395
+ return this._unprovideCore(id);
396
+ }
397
+ return Promise.resolve();
398
+ },
399
+ release,
400
+ };
401
+ }
402
+
403
+ private _unprovideCore<T>(id: ServiceIdentifier<T>): Promise<void> {
404
+ if (this._disposed) {
405
+ return Promise.resolve();
406
+ }
407
+ this._releaseProvideEntry(id);
408
+ if (this._services.get(id) === undefined) {
409
+ return Promise.resolve();
410
+ }
411
+ return this.cascade
412
+ .submit({ action: 'unprovide', token: id, reason: `unprovide ${String(id)}` })
413
+ .catch((error: unknown) => {
414
+ onUnexpectedError(error);
415
+ });
416
+ }
417
+
418
+ unprovide<T>(id: ServiceIdentifier<T>): void {
419
+ void this._unprovideCore(id);
420
+ }
421
+
422
+ private _releaseProvideEntry(id: ServiceIdentifier<any>): void {
423
+ const prev = this._provideEntries.get(id);
424
+ if (prev !== undefined) {
425
+ this._provideEntries.delete(id);
426
+ prev.entry.release();
427
+ prev.core.release();
428
+ }
429
+ }
430
+
431
+ private _retireUnit<T>(id: ServiceIdentifier<T>): void | Promise<void> {
432
+ const instance = this._services.get(id);
433
+ if (instance === undefined || instance instanceof SyncDescriptor) {
434
+ return undefined;
435
+ }
436
+ this._services.unmaterialize(id);
437
+ const entry = this._instanceEntries.get(instance);
438
+ if (entry === undefined) {
439
+ return undefined;
440
+ }
441
+ this._instanceEntries.delete(instance);
442
+ const serviceInstance = instance as object;
443
+ this._edgeNodes.delete(serviceInstance);
444
+ this._tree.graph.removeInstance(serviceInstance);
445
+ return entry.dispose();
446
+ }
447
+
448
+ private _nextUnitUid = 0;
449
+
450
+ get fiberHost(): FiberHost {
451
+ return this._getFiberHost();
452
+ }
453
+
454
+ get collectionStore(): CollectionStore {
455
+ return this._collectionStore;
456
+ }
457
+
458
+ get ledger(): Ledger {
459
+ return this._ledger;
460
+ }
461
+
462
+ get cascadeTree(): CascadeTree {
463
+ return this._tree;
464
+ }
465
+
466
+ get children(): readonly InstantiationService[] {
467
+ return [...this._children];
468
+ }
469
+
470
+ servicesSnapshot(): { token: string; uid: number }[] {
471
+ const snapshot: { token: string; uid: number }[] = [];
472
+ this._services.forEach((id) => {
473
+ snapshot.push({
474
+ token: id.toString(),
475
+ uid: this._services.uidOf(id)!,
476
+ });
477
+ });
478
+ return snapshot;
479
+ }
480
+
481
+ findIdentifier(tokenString: string): ServiceIdentifier<unknown> | undefined {
482
+ let found: ServiceIdentifier<unknown> | undefined;
483
+ this._services.forEach((id) => {
484
+ if (found === undefined && id.toString() === tokenString) {
485
+ found = id;
486
+ }
487
+ });
488
+ return found;
489
+ }
490
+
491
+ anchorKernelEntry(disposer: Disposer, label: string): LedgerEntry {
492
+ return this._ledger.register(disposer, label);
493
+ }
494
+
495
+ anchorKernelFinalizer(disposer: Disposer, label: string): LedgerEntry {
496
+ return this._ledger.registerFinalizer(disposer, label);
497
+ }
498
+
499
+ private _getFiberHost(): FiberHost {
500
+ this._fiberHost ??= {
501
+ mintUid: () => ++this._root()._nextUnitUid,
502
+ provideToken: (id, descriptor, options) => this._provideCore(id, descriptor, options),
503
+ provideTokenInstance: <T>(id: ServiceIdentifier<T>, instance: T) =>
504
+ this._provideCore(id, instance, undefined),
505
+ tokenState: (id: ServiceIdentifier<any>) => {
506
+ const owner = this._ownerOf(id) ?? this;
507
+ return owner.cascade.stateOf(id);
508
+ },
509
+ updateToken: async (id, config, hasConfig) => {
510
+ const owner = this._ownerOf(id);
511
+ if (owner === undefined) {
512
+ throw new Error(`update of unregistered token '${String(id)}'`);
513
+ }
514
+ if (hasConfig) {
515
+ owner._services.setConfig(id, config);
516
+ }
517
+ await owner.cascade.update(id);
518
+ },
519
+ resolveTokenWhenAvailable: <T>(id: ServiceIdentifier<T>): Promise<T> =>
520
+ this.cascade.resolveWhenAvailable<T>(id),
521
+ resolveInstance: <T>(id: ServiceIdentifier<T>): T =>
522
+ this.invokeFunction((accessor) => accessor.get(id)),
523
+ materializedInstance: <T>(id: ServiceIdentifier<T>): T | undefined =>
524
+ this._materializedInstanceOf(id),
525
+ liveRef: <T>(id: ServiceIdentifier<T>): LiveRef<T> => this._liveRef(id),
526
+ recordInstanceEdge: (node: object | undefined, id: ServiceIdentifier<any>) => {
527
+ if (node === undefined) {
528
+ return;
529
+ }
530
+ const owner = this._ownerOf(id);
531
+ if (owner !== undefined) {
532
+ this._edgeNodes.add(node);
533
+ this.dependencyGraph.addEdge(node, { scope: owner, token: id }, 'instance');
534
+ }
535
+ },
536
+ collectionView: <T>(token: CollectionToken<T>): CollectionView<T> =>
537
+ this._collectionView(token),
538
+ addCollectionRecord: <T>(
539
+ token: CollectionToken<T>,
540
+ providerName: string,
541
+ providerBook: Ledger,
542
+ value: T,
543
+ ) =>
544
+ this._collectionStore.addRecord(
545
+ token,
546
+ this,
547
+ providerName,
548
+ this._scopePath(),
549
+ providerBook,
550
+ value,
551
+ ),
552
+ constructService: <T>(ctor: new (...args: any[]) => T, config: unknown): T => {
553
+ return this._createInstance(ctor, [], Trace.traceCreation(this._enableTracing, ctor), {
554
+ config,
555
+ });
556
+ },
557
+ };
558
+ return this._fiberHost;
559
+ }
560
+
561
+ private _materializedInstanceOf<T>(id: ServiceIdentifier<T>): T | undefined {
562
+ const value = this._services.get(id);
563
+ if (value !== undefined) {
564
+ return value instanceof SyncDescriptor ? undefined : (value as T);
565
+ }
566
+ return this._parent?._materializedInstanceOf(id);
567
+ }
568
+
569
+ private _liveRef<T>(id: ServiceIdentifier<T>): LiveRef<T> {
570
+ const change = new Emitter<void>();
571
+ const chain: InstantiationService[] = [this];
572
+ for (let c = this._parent; c !== undefined; c = c._parent) {
573
+ chain.push(c);
574
+ }
575
+ const subscriptions = chain.map((container) =>
576
+ container._services.onDidChange(id, () => {
577
+ change.fire();
578
+ }),
579
+ );
580
+ this._ledger.register(() => {
581
+ for (const subscription of subscriptions) {
582
+ subscription.dispose();
583
+ }
584
+ change.dispose();
585
+ }, `ref:${String(id)}`);
586
+ const current = (): T | undefined => this._materializedInstanceOf(id);
587
+ const ref: LiveRef<T> = {
588
+ get current(): T | undefined {
589
+ return current();
590
+ },
591
+ onDidChange: change.event,
592
+ };
593
+ return ref;
594
+ }
595
+
596
+ private _collectionView<T>(token: CollectionToken<T>): CollectionView<T> {
597
+ let view = this._collectionViews.get(token as CollectionToken<unknown>);
598
+ if (view === undefined) {
599
+ view = this._collectionStore.createView(token, this);
600
+ this._collectionViews.set(token as CollectionToken<unknown>, view);
601
+ }
602
+ return view as CollectionViewImpl<T>;
603
+ }
604
+
605
+ private _scopePath(): string {
606
+ const labels: string[] = [this.debugLabel ?? `#${this._tree.seqOf(this)}`];
607
+ for (
608
+ let c: InstantiationService | undefined = this._parent;
609
+ c !== undefined;
610
+ c = c._parent
611
+ ) {
612
+ labels.unshift(c.debugLabel ?? `#${this._tree.seqOf(c)}`);
613
+ }
614
+ return labels.join('/');
615
+ }
616
+
617
+ createInstance<T>(descriptor: SyncDescriptor<T>, ...rest: any[]): T;
618
+ createInstance<T>(ctor: new (...args: any[]) => T, ...rest: any[]): T;
619
+ createInstance<T>(
620
+ ctorOrDescriptor: SyncDescriptor<T> | (new (...args: any[]) => T),
621
+ ...rest: any[]
622
+ ): T {
623
+ this._assertNotDisposed();
624
+ let _trace: Trace;
625
+ let result: T;
626
+ if (ctorOrDescriptor instanceof SyncDescriptor) {
627
+ _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);
628
+ result = this._createInstance(
629
+ ctorOrDescriptor.ctor,
630
+ ctorOrDescriptor.staticArguments.concat(rest),
631
+ _trace,
632
+ );
633
+ } else {
634
+ _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);
635
+ result = this._createInstance(ctorOrDescriptor, rest, _trace);
636
+ }
637
+ _trace.stop();
638
+ return result;
639
+ }
640
+
641
+ createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService {
642
+ this._assertNotDisposed();
643
+ if (!(services instanceof ServiceCollection)) {
644
+ throw new TypeError(
645
+ 'createChild requires a ServiceCollection instance (got something else)',
646
+ );
647
+ }
648
+ const child = this._createChildService(services);
649
+ this._children.add(child);
650
+ child._parentLedgerEntry = this._ledger.register(() => {
651
+ child.dispose();
652
+ }, 'child-instantiation');
653
+ store?.add(child);
654
+ return child;
655
+ }
656
+
657
+ protected _createChildService(services: ServiceCollection): InstantiationService {
658
+ return new InstantiationService(services, this._strict, this, this._enableTracing);
659
+ }
660
+
661
+ private _disposePromise: Promise<void> | undefined;
662
+
663
+ dispose(): void {
664
+ void this.disposeAsync();
665
+ }
666
+
667
+ disposeAsync(): Promise<void> {
668
+ this._disposePromise ??= this.disposeCore();
669
+ return this._disposePromise;
670
+ }
671
+
672
+ private disposeCore(): Promise<void> {
673
+ if (this._disposed) {
674
+ return Promise.resolve();
675
+ }
676
+ this._disposed = true;
677
+
678
+ const childTeardowns: Promise<void>[] = [];
679
+ let teardown: void | Promise<void> = undefined;
680
+ try {
681
+ for (const child of Array.from(this._children)) {
682
+ childTeardowns.push(child.disposeAsync());
683
+ }
684
+ this._children.clear();
685
+ teardown = this._ledger.teardown('scope-close');
686
+ this._services.dispose();
687
+ this.cascade.dispose();
688
+ for (const view of this._collectionViews.values()) {
689
+ view.dispose();
690
+ }
691
+ this._collectionViews.clear();
692
+ for (const node of this._edgeNodes) {
693
+ this._tree.graph.removeInstance(node);
694
+ }
695
+ this._edgeNodes.clear();
696
+ this._tree.graph.removeScope(this);
697
+ } finally {
698
+ this._children.clear();
699
+ this._parentLedgerEntry?.release();
700
+ this._parentLedgerEntry = undefined;
701
+ if (this._parent) {
702
+ this._parent._children.delete(this);
703
+ }
704
+ }
705
+ return Promise.all([...childTeardowns, Promise.resolve(teardown)]).then(() => undefined);
706
+ }
707
+
708
+ private _createInstance<T>(ctor: any, args: unknown[], _trace: Trace, unit?: {
709
+ id?: ServiceIdentifier<any>;
710
+ config?: unknown;
711
+ }): T {
712
+ const serviceDependencies = _util.getServiceDependencies(ctor).toSorted((a, b) => a.index - b.index);
713
+ const serviceArgs: unknown[] = [];
714
+ for (const dependency of serviceDependencies) {
715
+ const kind = dependency.kind ?? 'instance';
716
+ if (kind === 'collection') {
717
+ serviceArgs.push(this._collectionView(dependency.id as unknown as CollectionToken<unknown>));
718
+ continue;
719
+ }
720
+ if (kind === 'ref') {
721
+ serviceArgs.push(this._liveRef(dependency.id));
722
+ continue;
723
+ }
724
+ const service = this._getOrCreateServiceInstance(dependency.id, _trace);
725
+ if (!service) {
726
+ this._throwIfStrict(
727
+ `[createInstance] ${ctor.name} depends on UNKNOWN service ${String(dependency.id)}.`,
728
+ false,
729
+ );
730
+ }
731
+ serviceArgs.push(service);
732
+ }
733
+
734
+ const firstServiceArgPos =
735
+ serviceDependencies.length > 0 ? serviceDependencies[0]!.index : args.length;
736
+
737
+ if (args.length !== firstServiceArgPos) {
738
+ globalThis.console.trace(
739
+ `[createInstance] First service dependency of ${(ctor as { name?: string }).name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`,
740
+ );
741
+ const delta = firstServiceArgPos - args.length;
742
+ if (delta > 0) {
743
+ args = args.concat(Array.from({ length: delta }));
744
+ } else {
745
+ args = args.slice(0, firstServiceArgPos);
746
+ }
747
+ }
748
+
749
+ const finalArgs = args.concat(serviceArgs);
750
+ if (!isServiceRecipe(ctor)) {
751
+ return Reflect.construct<unknown[], T>(ctor, finalArgs);
752
+ }
753
+ const frame: ConstructionFrame = {
754
+ ctor,
755
+ config: unit?.config,
756
+ token: unit?.id,
757
+ host: this._getFiberHost(),
758
+ };
759
+ pushConstructionFrame(frame);
760
+ let instance: T;
761
+ try {
762
+ instance = Reflect.construct<unknown[], T>(ctor as new (...args: any[]) => T, finalArgs);
763
+ } finally {
764
+ popConstructionFrame();
765
+ }
766
+ bindServiceUnit(instance as UnitInternals & IDisposable, frame);
767
+ return instance;
768
+ }
769
+
770
+ protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
771
+ if (!this._cascadeResolving) {
772
+ if (this.cascade.isInFlight(id)) {
773
+ throw new CascadeConflictError(
774
+ String(id),
775
+ 'token is inside an in-flight cascade transaction',
776
+ );
777
+ }
778
+ const failure = this.cascade.failureOf(id);
779
+ if (failure !== undefined) {
780
+ throw failure as Error;
781
+ }
782
+ }
783
+ const entry = this._getServiceInstanceOrDescriptor(id);
784
+
785
+ if (entry instanceof SyncDescriptor) {
786
+ const root = this._root();
787
+ if (root._inProgress.includes(id)) {
788
+ const path = [...root._inProgress, id].map(String);
789
+ throw new CyclicDependencyError(path);
790
+ }
791
+
792
+ return this._safeCreateAndCacheServiceInstance(id, entry, _trace.branch(id, true));
793
+ }
794
+
795
+ _trace.branch(id, false);
796
+ return entry as T;
797
+ }
798
+
799
+ private _safeCreateAndCacheServiceInstance<T>(
800
+ id: ServiceIdentifier<T>,
801
+ desc: SyncDescriptor<T>,
802
+ _trace: Trace,
803
+ ): T {
804
+ if (this._activeInstantiations.has(id)) {
805
+ throw new Error(`illegal state - RECURSIVELY instantiating service '${String(id)}'`);
806
+ }
807
+ this._activeInstantiations.add(id);
808
+ try {
809
+ return this._createAndCacheServiceInstance(id, desc, _trace);
810
+ } finally {
811
+ this._activeInstantiations.delete(id);
812
+ }
813
+ }
814
+
815
+ private _createAndCacheServiceInstance<T>(
816
+ id: ServiceIdentifier<T>,
817
+ desc: SyncDescriptor<T>,
818
+ _trace: Trace,
819
+ ): T {
820
+ type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };
821
+ const graph = new Graph<Triple>(data => data.id.toString());
822
+
823
+ let cycleCount = 0;
824
+ const stack: Triple[] = [{ id, desc, _trace }];
825
+ const seen = new Set<string>();
826
+ while (stack.length > 0) {
827
+ const item = stack.pop()!;
828
+
829
+ if (seen.has(String(item.id))) {
830
+ continue;
831
+ }
832
+ seen.add(String(item.id));
833
+
834
+ graph.lookupOrInsertNode(item);
835
+
836
+ if (cycleCount++ > 1000) {
837
+ throw new CyclicDependencyError(graph);
838
+ }
839
+
840
+ for (const dependency of _util.getInstanceDependencies(item.desc.ctor)) {
841
+ const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
842
+ if (!instanceOrDesc) {
843
+ this._throwIfStrict(
844
+ `[createInstance] ${String(item.id)} depends on ${String(dependency.id)} which is NOT registered.`,
845
+ true,
846
+ );
847
+ }
848
+
849
+ this._globalGraph?.insertEdge(String(item.id), String(dependency.id));
850
+
851
+ if (instanceOrDesc instanceof SyncDescriptor) {
852
+ const d: Triple = {
853
+ id: dependency.id,
854
+ desc: instanceOrDesc,
855
+ _trace: item._trace.branch(dependency.id, true),
856
+ };
857
+ graph.insertEdge(item, d);
858
+ stack.push(d);
859
+ }
860
+ }
861
+ }
862
+
863
+ while (true) {
864
+ const roots = graph.roots();
865
+
866
+ if (roots.length === 0) {
867
+ if (!graph.isEmpty()) {
868
+ throw new CyclicDependencyError(graph);
869
+ }
870
+ break;
871
+ }
872
+
873
+ for (const { data } of roots) {
874
+ const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
875
+ if (instanceOrDesc instanceof SyncDescriptor) {
876
+ const instance = this._createServiceInstanceWithOwner(
877
+ data.id,
878
+ data.desc.ctor,
879
+ data.desc.staticArguments,
880
+ data._trace,
881
+ );
882
+ this._setCreatedServiceInstance(data.id, instance);
883
+ }
884
+ graph.removeNode(data);
885
+ }
886
+ }
887
+ return this._getServiceInstanceOrDescriptor(id) as T;
888
+ }
889
+
890
+ private _createServiceInstanceWithOwner<T>(
891
+ id: ServiceIdentifier<T>,
892
+ ctor: any,
893
+ args: ReadonlyArray<unknown> = [],
894
+ _trace: Trace,
895
+ ): T {
896
+ if (this._services.get(id) instanceof SyncDescriptor) {
897
+ return this._createServiceInstance(id, ctor, args, _trace);
898
+ }
899
+ if (this._parent) {
900
+ return this._parent._createServiceInstanceWithOwner(
901
+ id,
902
+ ctor,
903
+ args,
904
+ _trace,
905
+ );
906
+ }
907
+ throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
908
+ }
909
+
910
+ private _createServiceInstance<T>(
911
+ id: ServiceIdentifier<T>,
912
+ ctor: any,
913
+ args: ReadonlyArray<unknown> = [],
914
+ _trace: Trace,
915
+ ): T {
916
+ const root = this._root();
917
+ root._inProgress.push(id);
918
+ try {
919
+ const result = this._createInstance<T>(ctor, args.slice(), _trace, {
920
+ id,
921
+ config: this._services.configOf(id),
922
+ });
923
+ this.dependencyGraph.addInstance(result as object, this, id);
924
+ for (const dependency of _util.getServiceDependencies(ctor)) {
925
+ const kind = dependency.kind ?? 'instance';
926
+ if (kind === 'collection') {
927
+ this.dependencyGraph.addEdge(
928
+ result as object,
929
+ { scope: this, token: dependency.id },
930
+ 'collection',
931
+ );
932
+ continue;
933
+ }
934
+ if (kind === 'ref') {
935
+ continue;
936
+ }
937
+ const owner = this._ownerOf(dependency.id);
938
+ if (owner !== undefined) {
939
+ this.dependencyGraph.addEdge(
940
+ result as object,
941
+ { scope: owner, token: dependency.id },
942
+ 'instance',
943
+ );
944
+ }
945
+ }
946
+ const entry = this._ledger.register(() => {
947
+ this._instanceEntries.delete(result);
948
+ this.dependencyGraph.removeInstance(result as object);
949
+ if (isDisposable(result)) {
950
+ const out = result.dispose() as unknown as void | Promise<void>;
951
+ return out;
952
+ }
953
+ return undefined;
954
+ }, `service:${String(id)}`);
955
+ this._instanceEntries.set(result, entry);
956
+ this.cascade.observedMaterialization(id);
957
+ return result;
958
+ } finally {
959
+ const popIdx = root._inProgress.lastIndexOf(id);
960
+ if (popIdx >= 0) {
961
+ root._inProgress.splice(popIdx, 1);
962
+ }
963
+ }
964
+ }
965
+
966
+ private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
967
+ if (this._services.get(id) instanceof SyncDescriptor) {
968
+ this._services.materialize(id, instance);
969
+ } else if (this._parent) {
970
+ this._parent._setCreatedServiceInstance(id, instance);
971
+ } else {
972
+ throw new Error(
973
+ `illegal state - setting UNKNOWN service instance '${String(id)}'`,
974
+ );
975
+ }
976
+ }
977
+
978
+ private _getServiceInstanceOrDescriptor<T>(
979
+ id: ServiceIdentifier<T>,
980
+ ): T | SyncDescriptor<T> | undefined {
981
+ const instanceOrDesc = this._services.get(id);
982
+ if (instanceOrDesc === undefined && this._parent) {
983
+ return this._parent._getServiceInstanceOrDescriptor(id);
984
+ }
985
+ return instanceOrDesc;
986
+ }
987
+
988
+ private _throwIfStrict(msg: string, printWarning: boolean): void {
989
+ if (printWarning) {
990
+ globalThis.console.warn(msg);
991
+ }
992
+ if (this._strict) {
993
+ throw new Error(msg);
994
+ }
995
+ }
996
+
997
+ private _root(): InstantiationService {
998
+ return this._parent?._root() ?? this;
999
+ }
1000
+
1001
+ private _assertNotDisposed(): void {
1002
+ if (this._disposed) {
1003
+ throw new Error('InstantiationService has been disposed');
1004
+ }
1005
+ }
1006
+ }
packages/agent-core-v2/src/_base/di/lifecycle.ts ADDED
@@ -0,0 +1,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { onUnexpectedError } from '../errors/unexpectedError';
2
+ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
3
+
4
+ export interface IDisposableDebugLabel {
5
+ readonly debugLabel?: string;
6
+ }
7
+
8
+ function disposableLabel(d: IDisposable): string {
9
+ const debugLabel = (d as IDisposableDebugLabel).debugLabel;
10
+ if (typeof debugLabel === 'string' && debugLabel.length > 0) {
11
+ return debugLabel;
12
+ }
13
+ return `disposable:${d.constructor?.name ?? 'anonymous'}`;
14
+ }
15
+
16
+ export interface IDisposableTracker {
17
+ trackDisposable(disposable: IDisposable): void;
18
+ setParent(child: IDisposable, parent: IDisposable | null): void;
19
+ markAsDisposed(disposable: IDisposable): void;
20
+ markAsSingleton(disposable: IDisposable): void;
21
+ }
22
+
23
+ interface DisposableInfo {
24
+ value: IDisposable;
25
+ source: string | null;
26
+ parent: IDisposable | null;
27
+ isSingleton: boolean;
28
+ idx: number;
29
+ }
30
+
31
+ export class DisposableTracker implements IDisposableTracker {
32
+ private static idx = 0;
33
+ private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
34
+
35
+ private getDisposableData(d: IDisposable): DisposableInfo {
36
+ let val = this.livingDisposables.get(d);
37
+ if (!val) {
38
+ val = {
39
+ parent: null,
40
+ source: null,
41
+ isSingleton: false,
42
+ value: d,
43
+ idx: DisposableTracker.idx++,
44
+ };
45
+ this.livingDisposables.set(d, val);
46
+ }
47
+ return val;
48
+ }
49
+
50
+ trackDisposable(d: IDisposable): void {
51
+ const data = this.getDisposableData(d);
52
+ data.source ??= new Error('Disposable tracking').stack ?? null;
53
+ }
54
+
55
+ setParent(child: IDisposable, parent: IDisposable | null): void {
56
+ this.getDisposableData(child).parent = parent;
57
+ }
58
+
59
+ markAsDisposed(x: IDisposable): void {
60
+ this.livingDisposables.delete(x);
61
+ }
62
+
63
+ markAsSingleton(d: IDisposable): void {
64
+ this.getDisposableData(d).isSingleton = true;
65
+ }
66
+
67
+ private getRootParent(
68
+ data: DisposableInfo,
69
+ cache: Map<DisposableInfo, DisposableInfo>,
70
+ ): DisposableInfo {
71
+ const cached = cache.get(data);
72
+ if (cached) return cached;
73
+ const result = data.parent
74
+ ? this.getRootParent(this.getDisposableData(data.parent), cache)
75
+ : data;
76
+ cache.set(data, result);
77
+ return result;
78
+ }
79
+
80
+ getTrackedDisposables(): IDisposable[] {
81
+ const cache = new Map<DisposableInfo, DisposableInfo>();
82
+ return [...this.livingDisposables.entries()]
83
+ .filter(
84
+ ([, v]) => v.source !== null && !this.getRootParent(v, cache).isSingleton,
85
+ )
86
+ .map(([k]) => k);
87
+ }
88
+ }
89
+
90
+ let disposableTracker: IDisposableTracker | null = null;
91
+
92
+ export function setDisposableTracker(tracker: IDisposableTracker | null): void {
93
+ disposableTracker = tracker;
94
+ }
95
+
96
+ export function trackDisposable<T extends IDisposable>(x: T): T {
97
+ disposableTracker?.trackDisposable(x);
98
+ return x;
99
+ }
100
+
101
+ export function markAsDisposed(disposable: IDisposable): void {
102
+ disposableTracker?.markAsDisposed(disposable);
103
+ }
104
+
105
+ function setParentOfDisposable(
106
+ child: IDisposable,
107
+ parent: IDisposable | null,
108
+ ): void {
109
+ disposableTracker?.setParent(child, parent);
110
+ }
111
+
112
+ function setParentOfDisposables(
113
+ children: IDisposable[],
114
+ parent: IDisposable | null,
115
+ ): void {
116
+ if (!disposableTracker) return;
117
+ for (const child of children) {
118
+ disposableTracker.setParent(child, parent);
119
+ }
120
+ }
121
+
122
+ export function markAsSingleton<T extends IDisposable>(singleton: T): T {
123
+ disposableTracker?.markAsSingleton(singleton);
124
+ return singleton;
125
+ }
126
+
127
+ export interface IDisposable {
128
+ dispose(): void;
129
+ }
130
+
131
+ export function isDisposable<E>(thing: E): thing is E & IDisposable {
132
+ return (
133
+ typeof thing === 'object' &&
134
+ thing !== null &&
135
+ typeof (thing as unknown as IDisposable).dispose === 'function' &&
136
+ (thing as unknown as IDisposable).dispose.length === 0
137
+ );
138
+ }
139
+
140
+ export function dispose<T extends IDisposable>(disposable: T): T;
141
+ export function dispose<T extends IDisposable>(
142
+ disposable: T | undefined,
143
+ ): T | undefined;
144
+ export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(
145
+ disposables: A,
146
+ ): A;
147
+ export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
148
+ export function dispose<T extends IDisposable>(
149
+ disposables: ReadonlyArray<T>,
150
+ ): ReadonlyArray<T>;
151
+ export function dispose<T extends IDisposable>(
152
+ arg: T | Iterable<T> | undefined,
153
+ ): unknown {
154
+ if (arg === undefined || arg === null) return arg;
155
+ if (isIterable<T>(arg)) {
156
+ const errors: unknown[] = [];
157
+ for (const d of arg) {
158
+ if (d) {
159
+ try {
160
+ d.dispose();
161
+ } catch (error) {
162
+ errors.push(error);
163
+ }
164
+ }
165
+ }
166
+
167
+ if (errors.length === 1) {
168
+ throw errors[0];
169
+ }
170
+ if (errors.length > 1) {
171
+ throw new AggregateError(
172
+ errors,
173
+ 'Encountered errors while disposing of store',
174
+ );
175
+ }
176
+
177
+ return Array.isArray(arg) ? [] : arg;
178
+ }
179
+ (arg).dispose();
180
+ return arg;
181
+ }
182
+
183
+ function isIterable<T>(arg: unknown): arg is Iterable<T> {
184
+ return (
185
+ typeof arg === 'object' &&
186
+ arg !== null &&
187
+ typeof (arg as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function'
188
+ );
189
+ }
190
+
191
+ export function disposeIfDisposable<T extends IDisposable | object>(
192
+ disposables: Array<T>,
193
+ ): Array<T> {
194
+ const disposableValues: IDisposable[] = [];
195
+ for (const d of disposables) {
196
+ if (isDisposable(d)) {
197
+ disposableValues.push(d);
198
+ }
199
+ }
200
+ dispose(disposableValues);
201
+ return [];
202
+ }
203
+
204
+ class FunctionDisposable implements IDisposable {
205
+ private _isDisposed = false;
206
+ private readonly _fn: () => void;
207
+
208
+ constructor(fn: () => void) {
209
+ this._fn = fn;
210
+ trackDisposable(this);
211
+ }
212
+
213
+ dispose(): void {
214
+ if (this._isDisposed) return;
215
+ this._isDisposed = true;
216
+ markAsDisposed(this);
217
+ this._fn();
218
+ }
219
+ }
220
+
221
+ export function toDisposable(fn: () => void): IDisposable {
222
+ return new FunctionDisposable(fn);
223
+ }
224
+
225
+ export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
226
+ const parent = toDisposable(() => dispose(disposables));
227
+ setParentOfDisposables(disposables, parent);
228
+ return parent;
229
+ }
230
+
231
+ export class DisposableStore implements IDisposable {
232
+ private readonly _ledger = new Ledger('DisposableStore');
233
+ private readonly _entries = new Map<IDisposable, LedgerEntry>();
234
+ private _isDisposed = false;
235
+
236
+ constructor() {
237
+ trackDisposable(this);
238
+ }
239
+
240
+ get ledger(): Ledger {
241
+ return this._ledger;
242
+ }
243
+
244
+ add<T extends IDisposable>(d: T): T {
245
+ if ((d as unknown as DisposableStore) === this) {
246
+ throw new Error('Cannot register a disposable on itself!');
247
+ }
248
+ setParentOfDisposable(d, this);
249
+ if (this._isDisposed) {
250
+ d.dispose();
251
+ return d;
252
+ }
253
+ if (!this._entries.has(d)) {
254
+ this._entries.set(
255
+ d,
256
+ this._ledger.register(() => {
257
+ d.dispose();
258
+ }, disposableLabel(d)),
259
+ );
260
+ }
261
+ return d;
262
+ }
263
+
264
+ delete<T extends IDisposable>(d: T): void {
265
+ if (this._isDisposed) return;
266
+ if ((d as unknown as DisposableStore) === this) {
267
+ throw new Error('Cannot dispose a disposable on itself!');
268
+ }
269
+ const entry = this._entries.get(d);
270
+ if (entry) {
271
+ this._entries.delete(d);
272
+ entry.release();
273
+ }
274
+ d.dispose();
275
+ }
276
+
277
+ deleteAndLeak<T extends IDisposable>(d: T): void {
278
+ if (this._isDisposed) return;
279
+ const entry = this._entries.get(d);
280
+ if (entry) {
281
+ this._entries.delete(d);
282
+ entry.release();
283
+ setParentOfDisposable(d, null);
284
+ }
285
+ }
286
+
287
+ clear(): void {
288
+ if (this._entries.size === 0) return;
289
+ try {
290
+ void this._ledger.clear('scope-close');
291
+ } finally {
292
+ this._entries.clear();
293
+ }
294
+ }
295
+
296
+ dispose(): void {
297
+ if (this._isDisposed) return;
298
+ this._isDisposed = true;
299
+ markAsDisposed(this);
300
+ try {
301
+ void this._ledger.teardown('scope-close');
302
+ } finally {
303
+ this._entries.clear();
304
+ }
305
+ }
306
+
307
+ get isDisposed(): boolean {
308
+ return this._isDisposed;
309
+ }
310
+
311
+ assertNotDisposed(): void {
312
+ if (this._isDisposed) {
313
+ onUnexpectedError(new Error('Object disposed'));
314
+ }
315
+ }
316
+ }
317
+
318
+ export abstract class Disposable implements IDisposable {
319
+ protected readonly _store = new DisposableStore();
320
+
321
+ constructor() {
322
+ trackDisposable(this);
323
+ setParentOfDisposable(this._store, this);
324
+ }
325
+
326
+ protected _register<T extends IDisposable>(d: T): T {
327
+ if ((d as unknown as Disposable) === this) {
328
+ throw new Error('Cannot register a disposable on itself!');
329
+ }
330
+ return this._store.add(d);
331
+ }
332
+
333
+ dispose(): void {
334
+ markAsDisposed(this);
335
+ this._store.dispose();
336
+ }
337
+ }
338
+
339
+ export namespace Disposable {
340
+ export const None: IDisposable = Object.freeze({
341
+ dispose(): void {},
342
+ });
343
+ }
344
+
345
+ export class MutableDisposable<T extends IDisposable> implements IDisposable {
346
+ private readonly _ledger = new Ledger('MutableDisposable');
347
+ private _entry: LedgerEntry | undefined;
348
+ private _value: T | undefined;
349
+ private _isDisposed = false;
350
+
351
+ constructor() {
352
+ trackDisposable(this);
353
+ }
354
+
355
+ get value(): T | undefined {
356
+ return this._isDisposed ? undefined : this._value;
357
+ }
358
+
359
+ set value(value: T | undefined) {
360
+ if (this._isDisposed) {
361
+ if (value !== undefined) {
362
+ value.dispose();
363
+ }
364
+ return;
365
+ }
366
+ if (this._value === value) return;
367
+ this._entry?.release();
368
+ this._entry = undefined;
369
+ this._value?.dispose();
370
+ if (value) setParentOfDisposable(value, this);
371
+ this._value = value;
372
+ if (value) {
373
+ this._entry = this._ledger.register(() => {
374
+ value.dispose();
375
+ }, disposableLabel(value));
376
+ }
377
+ }
378
+
379
+ dispose(): void {
380
+ if (this._isDisposed) return;
381
+ this._isDisposed = true;
382
+ markAsDisposed(this);
383
+ const entry = this._entry;
384
+ const prev = this._value;
385
+ this._entry = undefined;
386
+ this._value = undefined;
387
+ entry?.release();
388
+ void this._ledger.teardown('scope-close');
389
+ if (prev !== undefined) {
390
+ prev.dispose();
391
+ }
392
+ }
393
+
394
+ clear(): void {
395
+ if (this._isDisposed) return;
396
+ this.value = undefined;
397
+ }
398
+
399
+ clearAndLeak(): T | undefined {
400
+ if (this._isDisposed) return undefined;
401
+ const prev = this._value;
402
+ this._value = undefined;
403
+ this._entry?.release();
404
+ this._entry = undefined;
405
+ if (prev !== undefined) setParentOfDisposable(prev, null);
406
+ return prev;
407
+ }
408
+ }
409
+
410
+ export class MandatoryMutableDisposable<T extends IDisposable> implements IDisposable {
411
+ private readonly _disposable = new MutableDisposable<T>();
412
+ private _isDisposed = false;
413
+
414
+ constructor(initialValue: T) {
415
+ this._disposable.value = initialValue;
416
+ }
417
+
418
+ get value(): T {
419
+ return this._disposable.value!;
420
+ }
421
+
422
+ set value(value: T) {
423
+ if (this._isDisposed || value === this._disposable.value) return;
424
+ this._disposable.value = value;
425
+ }
426
+
427
+ dispose(): void {
428
+ if (this._isDisposed) return;
429
+ this._isDisposed = true;
430
+ this._disposable.dispose();
431
+ }
432
+ }
433
+
434
+ export class RefCountedDisposable {
435
+ private _counter = 1;
436
+
437
+ constructor(private readonly _disposable: IDisposable) {}
438
+
439
+ acquire(): this {
440
+ this._counter += 1;
441
+ return this;
442
+ }
443
+
444
+ release(): this {
445
+ this._counter -= 1;
446
+ if (this._counter === 0) {
447
+ this._disposable.dispose();
448
+ }
449
+ return this;
450
+ }
451
+ }
452
+
453
+ export interface IReference<T> extends IDisposable {
454
+ readonly object: T;
455
+ }
456
+
457
+ export abstract class ReferenceCollection<T> {
458
+ private readonly references = new Map<
459
+ string,
460
+ { readonly object: T; counter: number }
461
+ >();
462
+
463
+ acquire(key: string, ...args: unknown[]): IReference<T> {
464
+ let reference = this.references.get(key);
465
+ if (!reference) {
466
+ reference = {
467
+ counter: 0,
468
+ object: this.createReferencedObject(key, ...args),
469
+ };
470
+ this.references.set(key, reference);
471
+ }
472
+
473
+ const { object } = reference;
474
+ let disposed = false;
475
+ const dispose = () => {
476
+ if (disposed) return;
477
+ disposed = true;
478
+ reference.counter -= 1;
479
+ if (reference.counter === 0) {
480
+ this.destroyReferencedObject(key, reference.object);
481
+ this.references.delete(key);
482
+ }
483
+ };
484
+
485
+ reference.counter += 1;
486
+ return { object, dispose };
487
+ }
488
+
489
+ protected abstract createReferencedObject(key: string, ...args: unknown[]): T;
490
+ protected abstract destroyReferencedObject(key: string, object: T): void;
491
+ }
492
+
493
+ export class AsyncReferenceCollection<T> {
494
+ constructor(private readonly referenceCollection: ReferenceCollection<Promise<T>>) {}
495
+
496
+ async acquire(key: string, ...args: unknown[]): Promise<IReference<T>> {
497
+ const ref = this.referenceCollection.acquire(key, ...args);
498
+
499
+ try {
500
+ const object = await ref.object;
501
+ return {
502
+ object,
503
+ dispose: () => { ref.dispose(); },
504
+ };
505
+ } catch (error) {
506
+ ref.dispose();
507
+ throw error;
508
+ }
509
+ }
510
+ }
511
+
512
+ export class ImmortalReference<T> implements IReference<T> {
513
+ constructor(public readonly object: T) {}
514
+ dispose(): void {}
515
+ }
516
+
517
+ export class DisposableMap<K, V extends IDisposable = IDisposable>
518
+ implements IDisposable
519
+ {
520
+ private readonly _store: Map<K, V>;
521
+ private _isDisposed = false;
522
+
523
+ constructor(store: Map<K, V> = new Map<K, V>()) {
524
+ this._store = store;
525
+ trackDisposable(this);
526
+ }
527
+
528
+ dispose(): void {
529
+ if (this._isDisposed) return;
530
+ this._isDisposed = true;
531
+ markAsDisposed(this);
532
+ this.clearAndDisposeAll();
533
+ }
534
+
535
+ clearAndDisposeAll(): void {
536
+ if (this._store.size === 0) return;
537
+ try {
538
+ dispose(this._store.values());
539
+ } finally {
540
+ this._store.clear();
541
+ }
542
+ }
543
+
544
+ has(key: K): boolean {
545
+ return this._store.has(key);
546
+ }
547
+
548
+ get size(): number {
549
+ return this._store.size;
550
+ }
551
+
552
+ get(key: K): V | undefined {
553
+ return this._store.get(key);
554
+ }
555
+
556
+ set(key: K, value: V, skipDisposeOnOverwrite = false): void {
557
+ if (this._isDisposed) {
558
+ console.warn(
559
+ new Error(
560
+ 'Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!',
561
+ ).stack,
562
+ );
563
+ return;
564
+ }
565
+ if (!skipDisposeOnOverwrite) {
566
+ const prev = this._store.get(key);
567
+ if (prev !== undefined && prev !== value) {
568
+ prev.dispose();
569
+ }
570
+ }
571
+ this._store.set(key, value);
572
+ setParentOfDisposable(value, this);
573
+ }
574
+
575
+ deleteAndDispose(key: K): void {
576
+ const value = this._store.get(key);
577
+ if (value !== undefined) {
578
+ value.dispose();
579
+ }
580
+ this._store.delete(key);
581
+ }
582
+
583
+ deleteAndLeak(key: K): V | undefined {
584
+ const value = this._store.get(key);
585
+ if (value !== undefined) setParentOfDisposable(value, null);
586
+ this._store.delete(key);
587
+ return value;
588
+ }
589
+
590
+ keys(): IterableIterator<K> {
591
+ return this._store.keys();
592
+ }
593
+
594
+ values(): IterableIterator<V> {
595
+ return this._store.values();
596
+ }
597
+
598
+ [Symbol.iterator](): IterableIterator<[K, V]> {
599
+ return this._store[Symbol.iterator]();
600
+ }
601
+ }
602
+
603
+ export class DisposableSet<V extends IDisposable = IDisposable>
604
+ implements IDisposable
605
+ {
606
+ private readonly _store: Set<V>;
607
+ private _isDisposed = false;
608
+
609
+ constructor(store: Set<V> = new Set<V>()) {
610
+ this._store = store;
611
+ trackDisposable(this);
612
+ }
613
+
614
+ dispose(): void {
615
+ if (this._isDisposed) return;
616
+ this._isDisposed = true;
617
+ markAsDisposed(this);
618
+ this.clearAndDisposeAll();
619
+ }
620
+
621
+ clearAndDisposeAll(): void {
622
+ if (this._store.size === 0) return;
623
+ try {
624
+ dispose(this._store.values());
625
+ } finally {
626
+ this._store.clear();
627
+ }
628
+ }
629
+
630
+ has(value: V): boolean {
631
+ return this._store.has(value);
632
+ }
633
+
634
+ get size(): number {
635
+ return this._store.size;
636
+ }
637
+
638
+ add(value: V): void {
639
+ if (this._isDisposed) {
640
+ console.warn(
641
+ new Error(
642
+ 'Trying to add a disposable to a DisposableSet that has already been disposed of. The added object will be leaked!',
643
+ ).stack,
644
+ );
645
+ return;
646
+ }
647
+ this._store.add(value);
648
+ setParentOfDisposable(value, this);
649
+ }
650
+
651
+ deleteAndDispose(value: V): void {
652
+ if (this._store.delete(value)) {
653
+ value.dispose();
654
+ }
655
+ }
656
+
657
+ deleteAndLeak(value: V): V | undefined {
658
+ if (this._store.delete(value)) {
659
+ setParentOfDisposable(value, null);
660
+ return value;
661
+ }
662
+ return undefined;
663
+ }
664
+
665
+ values(): IterableIterator<V> {
666
+ return this._store.values();
667
+ }
668
+
669
+ [Symbol.iterator](): IterableIterator<V> {
670
+ return this._store[Symbol.iterator]();
671
+ }
672
+ }
673
+
674
+ export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
675
+ const store = new DisposableStore();
676
+ try {
677
+ fn(store);
678
+ } finally {
679
+ store.dispose();
680
+ }
681
+ }
682
+
683
+ export function thenIfNotDisposed<T>(
684
+ promise: Promise<T>,
685
+ then: (result: T) => void,
686
+ ): IDisposable {
687
+ let disposed = false;
688
+ void promise.then((result) => {
689
+ if (disposed) return;
690
+ then(result);
691
+ });
692
+ return toDisposable(() => {
693
+ disposed = true;
694
+ });
695
+ }
696
+
697
+ export function thenRegisterOrDispose<T extends IDisposable>(
698
+ promise: Promise<T>,
699
+ store: DisposableStore,
700
+ ): Promise<T> {
701
+ return promise.then((disposable) => {
702
+ if (store.isDisposed) {
703
+ disposable.dispose();
704
+ } else {
705
+ store.add(disposable);
706
+ }
707
+ return disposable;
708
+ });
709
+ }
packages/agent-core-v2/src/_base/di/scope.ts ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BugIndicatingError } from '../errors/errors';
2
+ import { SyncDescriptor } from './descriptors';
3
+ import { ScopeActivation, type ProvideAllEntry } from './instantiation';
4
+ import type { ServiceIdentifier, ServicesAccessor, IInstantiationService } from './instantiation';
5
+ import { InstantiationService } from './instantiationService';
6
+ import { DisposableStore, type IDisposable } from './lifecycle';
7
+ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
8
+ import { ServiceCollection } from './serviceCollection';
9
+ import { watchScopeUnits } from './scopeUnits';
10
+
11
+ export { ScopeActivation };
12
+
13
+ export type ScopeKind = string;
14
+
15
+ let _scopeTopology: readonly string[] | undefined;
16
+
17
+ export function setScopeTopology(kinds: readonly string[]): void {
18
+ if (_scopeTopology === undefined) {
19
+ _scopeTopology = [...kinds];
20
+ return;
21
+ }
22
+ if (
23
+ _scopeTopology.length === kinds.length &&
24
+ _scopeTopology.every((kind, index) => kind === kinds[index])
25
+ ) {
26
+ return;
27
+ }
28
+ throw new BugIndicatingError(
29
+ `scope topology already declared as [${_scopeTopology.join(', ')}]; cannot redeclare as [${kinds.join(', ')}]`,
30
+ );
31
+ }
32
+
33
+ export interface ScopedEntry {
34
+ readonly scope: ScopeKind;
35
+ readonly id: ServiceIdentifier<unknown>;
36
+ readonly descriptor: SyncDescriptor<unknown>;
37
+ readonly domain: string;
38
+ readonly activation: ScopeActivation;
39
+ }
40
+
41
+ const _scopedRegistry: ScopedEntry[] = [];
42
+
43
+ function findScopedEntryIndex(scope: ScopeKind, id: ServiceIdentifier<unknown>): number {
44
+ return _scopedRegistry.findIndex((entry) => entry.scope === scope && entry.id === id);
45
+ }
46
+
47
+ export function registerScopedService<T>(
48
+ scope: ScopeKind,
49
+ id: ServiceIdentifier<T>,
50
+ ctor: new (...args: any[]) => T,
51
+ activation: ScopeActivation = ScopeActivation.OnScopeCreated,
52
+ domain: string = 'unknown',
53
+ ): void {
54
+ const existing = findScopedEntryIndex(scope, id as ServiceIdentifier<unknown>);
55
+ if (existing !== -1) {
56
+ throw new BugIndicatingError(
57
+ `duplicate scoped service registration for '${String(id)}' in scope '${scope}' (registered domain '${_scopedRegistry[existing]?.domain}', attempted domain '${domain}'); use overrideScopedService for intentional replacement`,
58
+ );
59
+ }
60
+ const descriptor = new SyncDescriptor<T>(ctor);
61
+ _scopedRegistry.push({
62
+ scope,
63
+ id: id as ServiceIdentifier<unknown>,
64
+ descriptor: descriptor as SyncDescriptor<unknown>,
65
+ domain,
66
+ activation,
67
+ });
68
+ }
69
+
70
+ export function overrideScopedService<T>(
71
+ scope: ScopeKind,
72
+ id: ServiceIdentifier<T>,
73
+ ctor: new (...args: any[]) => T,
74
+ activation: ScopeActivation = ScopeActivation.OnScopeCreated,
75
+ domain: string = 'unknown',
76
+ ): void {
77
+ const index = findScopedEntryIndex(scope, id as ServiceIdentifier<unknown>);
78
+ if (index === -1) {
79
+ throw new BugIndicatingError(
80
+ `overrideScopedService found no registration for '${String(id)}' in scope '${scope}' (domain '${domain}'); use registerScopedService for the initial registration`,
81
+ );
82
+ }
83
+ const descriptor = new SyncDescriptor<T>(ctor);
84
+ _scopedRegistry[index] = {
85
+ scope,
86
+ id: id as ServiceIdentifier<unknown>,
87
+ descriptor: descriptor as SyncDescriptor<unknown>,
88
+ domain,
89
+ activation,
90
+ };
91
+ }
92
+
93
+ export function getScopedServiceDescriptors(scope: ScopeKind): ReadonlyArray<ScopedEntry> {
94
+ return _scopedRegistry.filter((entry) => entry.scope === scope);
95
+ }
96
+
97
+ export function _clearScopedRegistryForTests(): void {
98
+ _scopedRegistry.length = 0;
99
+ }
100
+
101
+ export type ScopeSeed = ReadonlyArray<
102
+ readonly [ServiceIdentifier<any>, unknown]
103
+ >;
104
+
105
+ export interface ScopeOptions {
106
+ readonly id?: string;
107
+ readonly seeds?: ScopeSeed;
108
+ readonly configureContainer?: (container: InstantiationService) => void;
109
+ }
110
+
111
+ export interface IScopeHandle<K extends ScopeKind = ScopeKind> {
112
+ readonly id: string;
113
+ readonly kind: K;
114
+ readonly accessor: ServicesAccessor;
115
+ dispose(): void | Promise<void>;
116
+ }
117
+
118
+ export type IAppScopeHandle = IScopeHandle<'app'>;
119
+ export type ISessionScopeHandle = IScopeHandle<'session'>;
120
+ export type IAgentScopeHandle = IScopeHandle<'agent'>;
121
+
122
+ function buildCollection(seeds?: ScopeSeed): ServiceCollection {
123
+ const collection = new ServiceCollection();
124
+ if (seeds) {
125
+ for (const [id, value] of seeds) {
126
+ collection.set(id, value);
127
+ }
128
+ }
129
+ return collection;
130
+ }
131
+
132
+ function provideScopeServices(
133
+ instantiation: IInstantiationService,
134
+ kind: ScopeKind,
135
+ collection: ServiceCollection,
136
+ ): void {
137
+ const entries: ProvideAllEntry[] = [];
138
+ for (const entry of _scopedRegistry) {
139
+ if (entry.scope !== kind || collection.get(entry.id) !== undefined) {
140
+ continue;
141
+ }
142
+ entries.push({
143
+ id: entry.id,
144
+ descriptor: entry.descriptor,
145
+ options: {
146
+ activation: entry.activation === ScopeActivation.OnDemand ? 'ondemand' : 'eager',
147
+ },
148
+ });
149
+ }
150
+ instantiation.provideAll(entries);
151
+ }
152
+
153
+ export function createScopedChildHandle(
154
+ parent: IInstantiationService,
155
+ kind: ScopeKind,
156
+ id: string,
157
+ options: ScopeOptions = {},
158
+ ): IScopeHandle {
159
+ const collection = buildCollection(options.seeds);
160
+ const child = parent.createChild(collection);
161
+ (child as InstantiationService).debugLabel = id;
162
+ const engine = (child as InstantiationService).cascade;
163
+ engine.suspendActivation();
164
+ try {
165
+ watchScopeUnits(child as InstantiationService, kind);
166
+ options.configureContainer?.(child as InstantiationService);
167
+ provideScopeServices(child, kind, collection);
168
+ } catch (error) {
169
+ child.dispose();
170
+ throw error;
171
+ } finally {
172
+ engine.resumeActivation();
173
+ }
174
+ const accessor: ServicesAccessor = {
175
+ get: <T>(serviceId: ServiceIdentifier<T>): T =>
176
+ child.invokeFunction((a) => a.get(serviceId)),
177
+ };
178
+ return { id, kind, accessor, dispose: () => child.disposeAsync() };
179
+ }
180
+
181
+ export class Scope implements IDisposable {
182
+ readonly children = new Map<string, Scope>();
183
+ readonly accessor: ServicesAccessor;
184
+
185
+ private readonly _store = new DisposableStore();
186
+ private readonly _ledger: Ledger;
187
+ private _ledgerEntry: LedgerEntry | undefined;
188
+ private _disposed = false;
189
+
190
+ private constructor(
191
+ readonly id: string,
192
+ readonly kind: ScopeKind,
193
+ readonly instantiation: IInstantiationService,
194
+ private readonly _parent?: Scope,
195
+ ) {
196
+ this._ledger = new Ledger(`scope:${id}`);
197
+ this._ledger.register(() => {
198
+ this.instantiation.dispose();
199
+ }, 'instantiation');
200
+ this._ledger.register(() => {
201
+ this._store.dispose();
202
+ }, 'store');
203
+ this.accessor = {
204
+ get: <T>(serviceId: ServiceIdentifier<T>): T =>
205
+ instantiation.invokeFunction((a) => a.get(serviceId)),
206
+ };
207
+ }
208
+
209
+ get ledger(): Ledger {
210
+ return this._ledger;
211
+ }
212
+
213
+ static createApp(options: ScopeOptions = {}): Scope {
214
+ const kind: ScopeKind = 'app';
215
+ const collection = buildCollection(options.seeds);
216
+ const instantiation = new InstantiationService(collection, true);
217
+ instantiation.debugLabel = options.id ?? 'app';
218
+ instantiation.cascade.suspendActivation();
219
+ try {
220
+ watchScopeUnits(instantiation, kind);
221
+ options.configureContainer?.(instantiation);
222
+ provideScopeServices(instantiation, kind, collection);
223
+ } catch (error) {
224
+ instantiation.dispose();
225
+ throw error;
226
+ } finally {
227
+ instantiation.cascade.resumeActivation();
228
+ }
229
+ return new Scope(options.id ?? 'app', kind, instantiation);
230
+ }
231
+
232
+ private _assertNotDisposed(): void {
233
+ if (this._disposed) {
234
+ throw new Error(`Scope '${this.id}' has been disposed`);
235
+ }
236
+ }
237
+
238
+ createChild(kind: ScopeKind, id: string, options: ScopeOptions = {}): Scope {
239
+ this._assertNotDisposed();
240
+ if (_scopeTopology !== undefined) {
241
+ const parentIndex = _scopeTopology.indexOf(this.kind);
242
+ const childIndex = _scopeTopology.indexOf(kind);
243
+ if (parentIndex === -1 || childIndex === -1 || childIndex <= parentIndex) {
244
+ throw new Error(
245
+ `child scope kind '${kind}' must be greater than parent kind '${this.kind}' in the declared scope topology`,
246
+ );
247
+ }
248
+ }
249
+ if (this.children.has(id)) {
250
+ throw new Error(`Scope '${this.id}' already has a child with id '${id}'`);
251
+ }
252
+ const collection = buildCollection(options.seeds);
253
+ const childInstantiation = this.instantiation.createChild(collection);
254
+ (childInstantiation as InstantiationService).debugLabel = id;
255
+ const engine = (childInstantiation as InstantiationService).cascade;
256
+ engine.suspendActivation();
257
+ try {
258
+ watchScopeUnits(childInstantiation as InstantiationService, kind);
259
+ options.configureContainer?.(childInstantiation as InstantiationService);
260
+ provideScopeServices(childInstantiation, kind, collection);
261
+ } catch (error) {
262
+ childInstantiation.dispose();
263
+ throw error;
264
+ } finally {
265
+ engine.resumeActivation();
266
+ }
267
+ const child = new Scope(id, kind, childInstantiation, this);
268
+ this.children.set(id, child);
269
+ child._ledgerEntry = this._ledger.register(() => {
270
+ child.dispose();
271
+ }, `scope:${id}`);
272
+ return child;
273
+ }
274
+
275
+ toHandle(): IScopeHandle {
276
+ return { id: this.id, kind: this.kind, accessor: this.accessor, dispose: () => this.dispose() };
277
+ }
278
+
279
+ dispose(): void {
280
+ if (this._disposed) {
281
+ return;
282
+ }
283
+ this._disposed = true;
284
+
285
+ this._ledgerEntry?.release();
286
+ this._ledgerEntry = undefined;
287
+ try {
288
+ void this._ledger.teardown('scope-close');
289
+ } finally {
290
+ this.children.clear();
291
+ if (this._parent) {
292
+ this._parent.children.delete(this.id);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ export function createAppScope(options: ScopeOptions = {}): Scope {
299
+ return Scope.createApp(options);
300
+ }
packages/agent-core-v2/src/_base/di/scopeUnits.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { onUnexpectedError } from '../errors/unexpectedError';
2
+ import type { IDisposable } from './lifecycle';
3
+ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
4
+ import type { StoredRecord } from './collection';
5
+ import {
6
+ FiberRuntime,
7
+ isClassRecipe,
8
+ ScopeUnits,
9
+ type EffectBody,
10
+ type ServiceRecipe,
11
+ } from './fiber';
12
+ import type { InstantiationService } from './instantiationService';
13
+ import type { ScopeKind } from './scope';
14
+
15
+ export function watchScopeUnits(container: InstantiationService, kind: ScopeKind): void {
16
+ if (container.cascadeDisposed) {
17
+ return;
18
+ }
19
+ const token = ScopeUnits(kind);
20
+ const host = container.fiberHost;
21
+ const view = host.collectionView(token);
22
+ const foldLedger = new Ledger(`scope-units:${kind}`);
23
+ container.anchorKernelEntry((reason) => foldLedger.teardown(reason), `scope-units:${kind}`);
24
+
25
+ const materialized = new Map<number, () => void | Promise<void>>();
26
+
27
+ const materialize = (record: StoredRecord): void => {
28
+ const recipe = record.value as ServiceRecipe;
29
+ const name = record.providerName;
30
+ const unitLedger = new Ledger(`scope-units:${kind}:${name}`);
31
+ try {
32
+ if (isClassRecipe(recipe)) {
33
+ const instance = host.constructService(recipe, undefined) as Partial<IDisposable>;
34
+ unitLedger.register(() => {
35
+ return instance.dispose?.();
36
+ }, `unit:${name}`);
37
+ } else {
38
+ const facade = new FiberRuntime(
39
+ host,
40
+ unitLedger,
41
+ name,
42
+ undefined,
43
+ undefined,
44
+ new Set(recipe.inject ?? []),
45
+ undefined,
46
+ );
47
+ const out =
48
+ typeof recipe === 'function'
49
+ ? recipe(facade, undefined)
50
+ : recipe.apply(facade, undefined);
51
+ unitLedger.effect((() => out) as EffectBody, `effect:${name}`);
52
+ }
53
+ } catch (error) {
54
+ void unitLedger.teardown('unload');
55
+ onUnexpectedError(error);
56
+ return;
57
+ }
58
+
59
+ let retracted = false;
60
+ let providerEntry: LedgerEntry | undefined;
61
+ const retract = (): void | Promise<void> => {
62
+ if (retracted) {
63
+ return undefined;
64
+ }
65
+ retracted = true;
66
+ providerEntry?.release();
67
+ providerEntry = undefined;
68
+ materialized.delete(record.id);
69
+ return unitLedger.teardown('unload');
70
+ };
71
+ if (!record.providerBook.isActive) {
72
+ void retract();
73
+ return;
74
+ }
75
+ providerEntry = record.providerBook.register(() => {
76
+ void retract();
77
+ }, `scope-units:${kind}`);
78
+ foldLedger.register(() => {
79
+ return retract();
80
+ }, `record:${name}`);
81
+ materialized.set(record.id, retract);
82
+ };
83
+
84
+ const reconcile = (): void => {
85
+ if (!foldLedger.isActive) {
86
+ return;
87
+ }
88
+ const records = container.collectionStore.storedRecordsFor(token, container);
89
+ const seen = new Set<number>();
90
+ for (const record of records) {
91
+ seen.add(record.id);
92
+ if (!materialized.has(record.id)) {
93
+ materialize(record);
94
+ }
95
+ }
96
+ for (const [id, retract] of Array.from(materialized)) {
97
+ if (!seen.has(id)) {
98
+ void retract();
99
+ }
100
+ }
101
+ };
102
+
103
+ const subscription = view.onDidChange(() => {
104
+ reconcile();
105
+ });
106
+ foldLedger.register(() => {
107
+ subscription.dispose();
108
+ }, 'view-subscription');
109
+ reconcile();
110
+ }
packages/agent-core-v2/src/_base/di/service.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Emitter } from '../event';
2
+ import type { EffectBody } from '../lifecycle/disposer';
3
+ import type { Ledger } from '../lifecycle/ledger';
4
+ import type { CollectionToken } from './collection';
5
+ import {
6
+ currentConstruction,
7
+ FiberProtocolError,
8
+ FiberState,
9
+ PendingFiberHandle,
10
+ SERVICE_MARK,
11
+ type BufferedOp,
12
+ type Fiber,
13
+ type FiberHandle,
14
+ type FiberProvideOptions,
15
+ type FiberRuntime,
16
+ type RecipeStatics,
17
+ type ServiceClassRecipe,
18
+ type ServiceRecipe,
19
+ type UnitInternals,
20
+ } from './fiber';
21
+ import type { ServiceIdentifier, LiveRef } from './instantiation';
22
+ import { Disposable } from './lifecycle';
23
+
24
+ export abstract class Service extends Disposable implements Fiber, UnitInternals {
25
+ private __unitBuffer: BufferedOp[] | null;
26
+ private __unitRuntime: FiberRuntime | undefined;
27
+
28
+ readonly name: string;
29
+ readonly state: FiberState = FiberState.Active;
30
+ readonly config: unknown;
31
+
32
+ constructor() {
33
+ super();
34
+ const frame = currentConstruction();
35
+ if (
36
+ frame !== undefined &&
37
+ frame.ctor === (new.target as unknown as new (...args: any[]) => any)
38
+ ) {
39
+ this.__unitBuffer = [];
40
+ this.config = frame.config;
41
+ } else {
42
+ this.__unitBuffer = null;
43
+ this.config = undefined;
44
+ }
45
+ this.name = (this.constructor as any).name || 'anonymous';
46
+ }
47
+
48
+ provide<T>(
49
+ id: ServiceIdentifier<T>,
50
+ recipe: ServiceClassRecipe,
51
+ opts?: FiberProvideOptions,
52
+ ): FiberHandle<T>;
53
+ provide<T>(id: ServiceIdentifier<T>, instance: T): FiberHandle<T>;
54
+ provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
55
+ provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
56
+ provide(
57
+ first: any,
58
+ second?: any,
59
+ third?: FiberProvideOptions,
60
+ ): FiberHandle {
61
+ if (this.__unitBuffer !== null) {
62
+ const pending = new PendingFiberHandle(this._pendingName(first));
63
+ this.__unitBuffer.push((runtime) => {
64
+ pending.attach(runtime.provide(first, second, third));
65
+ });
66
+ return pending;
67
+ }
68
+ return this._runtime().provide(first, second, third);
69
+ }
70
+
71
+ effect(body: EffectBody, label?: string): FiberHandle {
72
+ if (this.__unitBuffer !== null) {
73
+ const pending = new PendingFiberHandle(label ?? `effect:${this.name}`);
74
+ this.__unitBuffer.push((runtime) => {
75
+ pending.attach(runtime.effect(body, label));
76
+ });
77
+ return pending;
78
+ }
79
+ return this._runtime().effect(body, label);
80
+ }
81
+
82
+ on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
83
+ const label = typeof event === 'string' ? `on:${event}` : 'on:emitter';
84
+ if (this.__unitBuffer !== null) {
85
+ const pending = new PendingFiberHandle(label);
86
+ this.__unitBuffer.push((runtime) => {
87
+ pending.attach(runtime.on(event, handler));
88
+ });
89
+ return pending;
90
+ }
91
+ return this._runtime().on(event, handler);
92
+ }
93
+
94
+ get<T>(id: ServiceIdentifier<T>): T {
95
+ return this._runtime().get(id);
96
+ }
97
+
98
+ ref<T>(id: ServiceIdentifier<T>): LiveRef<T> {
99
+ return this._runtime().ref(id);
100
+ }
101
+
102
+ get unitBook(): Ledger {
103
+ return this._store.ledger;
104
+ }
105
+
106
+ takeUnitBuffer(): BufferedOp[] | null {
107
+ const buffer = this.__unitBuffer;
108
+ this.__unitBuffer = null;
109
+ return buffer;
110
+ }
111
+
112
+ setUnitRuntime(runtime: FiberRuntime): void {
113
+ this.__unitRuntime = runtime;
114
+ }
115
+
116
+ private _runtime(): FiberRuntime {
117
+ if (this.__unitRuntime === undefined) {
118
+ if (this.__unitBuffer !== null) {
119
+ throw new FiberProtocolError(
120
+ `unit '${this.name}': get/ref/provide reads are not available during construction — declare dependencies as constructor parameters (构造期只写不读)`,
121
+ );
122
+ }
123
+ throw new FiberProtocolError(
124
+ `unit '${this.name}': no unit runtime is bound — a Service created by manual \`new\` has no capabilities; construct it through the container`,
125
+ );
126
+ }
127
+ return this.__unitRuntime;
128
+ }
129
+
130
+ private _pendingName(first: any): string {
131
+ if (typeof first === 'function') {
132
+ return (first as RecipeStatics).name ?? String(first);
133
+ }
134
+ return this.name;
135
+ }
136
+ }
137
+
138
+ (Service.prototype as unknown as Record<symbol, unknown>)[SERVICE_MARK] = true;
packages/agent-core-v2/src/_base/di/serviceCollection.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Emitter } from '../event';
2
+ import { SyncDescriptor } from './descriptors';
3
+ import type { ServiceIdentifier } from './instantiation';
4
+ import type { IDisposable } from './lifecycle';
5
+
6
+ export interface ServiceCollectionEntry<T = unknown> {
7
+ readonly value: T | SyncDescriptor<T>;
8
+ readonly uid: number;
9
+ readonly config?: unknown;
10
+ readonly recipe?: SyncDescriptor<T>;
11
+ }
12
+
13
+ export interface AvailabilityChange {
14
+ readonly oldUid: number | undefined;
15
+ readonly newUid: number | undefined;
16
+ }
17
+
18
+ export class ServiceCollection {
19
+ private readonly _entries = new Map<ServiceIdentifier<any>, ServiceCollectionEntry<any>>();
20
+ private readonly _emitters = new Map<
21
+ ServiceIdentifier<any>,
22
+ Emitter<AvailabilityChange>
23
+ >();
24
+ private _nextUid = 0;
25
+
26
+ constructor(
27
+ ...entries: ReadonlyArray<readonly [ServiceIdentifier<any>, unknown]>
28
+ ) {
29
+ for (const [id, value] of entries) {
30
+ this.set(id, value);
31
+ }
32
+ }
33
+
34
+ set<T>(
35
+ id: ServiceIdentifier<T>,
36
+ instanceOrDescriptor: T | SyncDescriptor<T>,
37
+ options?: { readonly config?: unknown },
38
+ ): T | SyncDescriptor<T> | undefined {
39
+ const prev = this._entries.get(id);
40
+ const uid = ++this._nextUid;
41
+ this._entries.set(id, {
42
+ value: instanceOrDescriptor,
43
+ uid,
44
+ config: options?.config,
45
+ });
46
+ this._emitterFor(id).fire({ oldUid: prev?.uid, newUid: uid });
47
+ return prev?.value as T | SyncDescriptor<T> | undefined;
48
+ }
49
+
50
+ materialize<T>(id: ServiceIdentifier<T>, instance: T): void {
51
+ const prev = this._entries.get(id);
52
+ if (prev === undefined || !(prev.value instanceof SyncDescriptor)) {
53
+ return;
54
+ }
55
+ this._entries.set(id, {
56
+ value: instance,
57
+ uid: prev.uid,
58
+ config: prev.config,
59
+ recipe: prev.value,
60
+ });
61
+ }
62
+
63
+ unmaterialize<T>(id: ServiceIdentifier<T>): void {
64
+ const prev = this._entries.get(id);
65
+ if (prev === undefined || prev.recipe === undefined) {
66
+ return;
67
+ }
68
+ this._entries.set(id, {
69
+ value: prev.recipe,
70
+ uid: prev.uid,
71
+ config: prev.config,
72
+ });
73
+ }
74
+
75
+ setConfig<T>(id: ServiceIdentifier<T>, config: unknown): void {
76
+ const prev = this._entries.get(id);
77
+ if (prev === undefined) {
78
+ return;
79
+ }
80
+ this._entries.set(id, { ...prev, config });
81
+ }
82
+
83
+ delete<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> | undefined {
84
+ const prev = this._entries.get(id);
85
+ if (prev === undefined) {
86
+ return undefined;
87
+ }
88
+ this._entries.delete(id);
89
+ this._emitterFor(id).fire({ oldUid: prev.uid, newUid: undefined });
90
+ return prev.value as T | SyncDescriptor<T> | undefined;
91
+ }
92
+
93
+ entry(id: ServiceIdentifier<any>): ServiceCollectionEntry | undefined {
94
+ return this._entries.get(id);
95
+ }
96
+
97
+ uidOf(id: ServiceIdentifier<any>): number | undefined {
98
+ return this._entries.get(id)?.uid;
99
+ }
100
+
101
+ configOf(id: ServiceIdentifier<any>): unknown {
102
+ return this._entries.get(id)?.config;
103
+ }
104
+
105
+ onDidChange<T>(
106
+ id: ServiceIdentifier<T>,
107
+ listener: (change: AvailabilityChange) => void,
108
+ ): IDisposable {
109
+ return this._emitterFor(id).event(listener);
110
+ }
111
+
112
+ has(id: ServiceIdentifier<any>): boolean {
113
+ return this._entries.has(id);
114
+ }
115
+
116
+ get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> | undefined {
117
+ return this._entries.get(id)?.value as T | SyncDescriptor<T> | undefined;
118
+ }
119
+
120
+ forEach(
121
+ callback: (
122
+ id: ServiceIdentifier<any>,
123
+ value: unknown,
124
+ ) => void,
125
+ ): void {
126
+ this._entries.forEach((entry, id) => { callback(id, entry.value); });
127
+ }
128
+
129
+ dispose(): void {
130
+ for (const emitter of this._emitters.values()) {
131
+ emitter.dispose();
132
+ }
133
+ this._emitters.clear();
134
+ }
135
+
136
+ private _emitterFor<T>(id: ServiceIdentifier<T>): Emitter<AvailabilityChange> {
137
+ let emitter = this._emitters.get(id);
138
+ if (emitter === undefined) {
139
+ emitter = new Emitter<AvailabilityChange>();
140
+ this._emitters.set(id, emitter);
141
+ }
142
+ return emitter;
143
+ }
144
+ }
packages/agent-core-v2/src/_base/di/test.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export {
2
+ createServices,
3
+ TestInstantiationService,
4
+ } from './testInstantiationService';
5
+ export type {
6
+ CreateServicesOptions,
7
+ ServiceGroup,
8
+ ServiceRegistration,
9
+ } from './testInstantiationService';
10
+
11
+ import { type ServiceIdentifier } from './instantiation';
12
+ import { createAppScope, createScopedChildHandle, Scope, type ScopeKind, type ScopeSeed } from './scope';
13
+
14
+ export interface ScopedTestHost {
15
+ readonly app: Scope;
16
+ child(kind: ScopeKind, id: string, stubs?: ScopeSeed): Scope;
17
+ childOf(parent: Scope, kind: ScopeKind, id: string, stubs?: ScopeSeed): Scope;
18
+ dispose(): void;
19
+ }
20
+
21
+ export function createScopedTestHost(appStubs: ScopeSeed = []): ScopedTestHost {
22
+ const app = createAppScope({ seeds: appStubs });
23
+ return {
24
+ app,
25
+ child(kind, id, stubs = []) {
26
+ if (kind === 'program') {
27
+ const handle = createScopedChildHandle(app.instantiation, kind, id, { seeds: stubs });
28
+ return {
29
+ id: handle.id,
30
+ kind: handle.kind,
31
+ accessor: handle.accessor,
32
+ dispose: () => {
33
+ void handle.dispose();
34
+ },
35
+ } as Scope;
36
+ }
37
+ return app.createChild(kind, id, { seeds: stubs });
38
+ },
39
+ childOf(parent, kind, id, stubs = []) {
40
+ return parent.createChild(kind, id, { seeds: stubs });
41
+ },
42
+ dispose() {
43
+ app.dispose();
44
+ },
45
+ };
46
+ }
47
+
48
+ export function stubPair<T>(
49
+ id: ServiceIdentifier<T>,
50
+ instance: T,
51
+ ): readonly [ServiceIdentifier<T>, T] {
52
+ return [id, instance];
53
+ }
packages/agent-core-v2/src/_base/di/testInstantiationService.ts ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as sinon from 'sinon';
2
+
3
+ import { SyncDescriptor, type SyncDescriptor0 } from './descriptors';
4
+ import {
5
+ type GetLeadingNonServiceArgs,
6
+ type ServiceIdentifier,
7
+ type ServicesAccessor,
8
+ } from './instantiation';
9
+ import { InstantiationService, Trace } from './instantiationService';
10
+ import { DisposableStore, dispose, isDisposable, toDisposable, type IDisposable } from './lifecycle';
11
+ import { ServiceCollection } from './serviceCollection';
12
+
13
+ type AnyConstructor<T = unknown> = new (...args: any[]) => T;
14
+
15
+ interface IServiceMock<T> {
16
+ id: ServiceIdentifier<T>;
17
+ service?: any;
18
+ }
19
+
20
+ const isSinonSpyLike = (fn: Function): fn is sinon.SinonSpy =>
21
+ fn && 'callCount' in fn;
22
+
23
+ export class TestInstantiationService extends InstantiationService implements IDisposable, ServicesAccessor {
24
+ private readonly _classStubs = new Map<Function, unknown>();
25
+ private readonly _parentTestService?: TestInstantiationService;
26
+
27
+ constructor(
28
+ private readonly _serviceCollection: ServiceCollection = new ServiceCollection(),
29
+ strict: boolean = false,
30
+ parent?: InstantiationService,
31
+ private readonly _properDispose: boolean = true,
32
+ ) {
33
+ super(_serviceCollection, strict, parent);
34
+ if (parent instanceof TestInstantiationService) {
35
+ this._parentTestService = parent;
36
+ }
37
+ }
38
+
39
+ public get<T>(id: ServiceIdentifier<T>): T {
40
+ return super._getOrCreateServiceInstance(
41
+ id,
42
+ Trace.traceCreation(false, TestInstantiationService),
43
+ );
44
+ }
45
+
46
+ public set<T>(
47
+ id: ServiceIdentifier<T>,
48
+ instanceOrDescriptor: T | SyncDescriptor<T>,
49
+ ): T | SyncDescriptor<T> | undefined {
50
+ const prev = this._serviceCollection.get(id);
51
+ this.provide(id, instanceOrDescriptor, { activation: 'ondemand' });
52
+ return prev;
53
+ }
54
+
55
+ public mock<T>(id: ServiceIdentifier<T>): T | sinon.SinonMock {
56
+ return this._create({ id }, { mock: true });
57
+ }
58
+
59
+ public stubInstance<T>(ctor: AnyConstructor<T>, instance: Partial<T>): void {
60
+ this._classStubs.set(ctor, instance);
61
+ }
62
+
63
+ protected _getClassStub(ctor: Function): unknown {
64
+ return this._classStubs.get(ctor) ?? this._parentTestService?._getClassStub(ctor);
65
+ }
66
+
67
+ public override createInstance<T>(descriptor: SyncDescriptor0<T>): T;
68
+ public override createInstance<
69
+ Ctor extends AnyConstructor,
70
+ R extends InstanceType<Ctor>,
71
+ >(
72
+ ctor: Ctor,
73
+ ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>
74
+ ): R;
75
+ public override createInstance(
76
+ ctorOrDescriptor: any,
77
+ ...rest: unknown[]
78
+ ): unknown {
79
+ const stub =
80
+ ctorOrDescriptor instanceof SyncDescriptor
81
+ ? this._getClassStub(ctorOrDescriptor.ctor)
82
+ : this._getClassStub(ctorOrDescriptor);
83
+
84
+ if (stub !== undefined) {
85
+ return stub;
86
+ }
87
+
88
+ if (ctorOrDescriptor instanceof SyncDescriptor) {
89
+ return super.createInstance(ctorOrDescriptor, ...rest);
90
+ }
91
+ return super.createInstance(ctorOrDescriptor, ...rest);
92
+ }
93
+
94
+ public stub<T>(
95
+ id: ServiceIdentifier<T>,
96
+ instanceOrDescriptor: Partial<NoInfer<T>> | SyncDescriptor<T>,
97
+ ): T | SyncDescriptor<T>;
98
+ public stub<T>(id: ServiceIdentifier<T>, ctor: AnyConstructor<T>): T;
99
+ public stub<T, V>(
100
+ id: ServiceIdentifier<T>,
101
+ obj: Partial<NoInfer<T>> | Function,
102
+ property: string,
103
+ value: V,
104
+ ): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
105
+ public stub<T, V>(
106
+ id: ServiceIdentifier<T>,
107
+ property: string,
108
+ value: V,
109
+ ): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
110
+ public stub<T>(
111
+ id: ServiceIdentifier<T>,
112
+ arg2: any,
113
+ arg3?: string,
114
+ arg4?: any,
115
+ ): T | SyncDescriptor<T> | sinon.SinonStub | sinon.SinonSpy {
116
+ if (arg2 instanceof SyncDescriptor && typeof arg3 !== 'string') {
117
+ this._serviceCollection.set(id, arg2);
118
+ return arg2;
119
+ }
120
+
121
+ if (typeof arg2 !== 'string' && typeof arg3 !== 'string') {
122
+ const service = this._create(arg2, { stub: true }) as T;
123
+ this._serviceCollection.set(id, service);
124
+ return service;
125
+ }
126
+
127
+ const service = typeof arg2 !== 'string' ? arg2 : undefined;
128
+ const property = typeof arg2 === 'string' ? arg2 : arg3;
129
+ const value = typeof arg2 === 'string' ? arg3 : arg4;
130
+
131
+ if (typeof property !== 'string') {
132
+ throw new TypeError('stub requires a method/property name');
133
+ }
134
+
135
+ const serviceMock: IServiceMock<T> = { id, service };
136
+ const stubObject = this._create(serviceMock, { stub: true }, Boolean(service && !property)) as Record<string, unknown>;
137
+ const replacement = this._createReplacement(value);
138
+
139
+ const current = stubObject[property] as { restore?: () => void } | undefined;
140
+ if (current && typeof current.restore === 'function') {
141
+ current.restore();
142
+ }
143
+ stubObject[property] = replacement;
144
+ return replacement;
145
+ }
146
+
147
+ public stubPromise<T>(
148
+ id?: ServiceIdentifier<T>,
149
+ fnProperty?: string,
150
+ value?: any,
151
+ ): T | sinon.SinonStub;
152
+ public stubPromise<T, V>(
153
+ id?: ServiceIdentifier<T>,
154
+ ctor?: any,
155
+ fnProperty?: string,
156
+ value?: V,
157
+ ): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
158
+ public stubPromise<T, V>(
159
+ id?: ServiceIdentifier<T>,
160
+ obj?: any,
161
+ fnProperty?: string,
162
+ value?: V,
163
+ ): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
164
+ public stubPromise(
165
+ arg1?: any,
166
+ arg2?: any,
167
+ arg3?: any,
168
+ arg4?: any,
169
+ ): unknown {
170
+ arg3 = typeof arg2 === 'string' ? Promise.resolve(arg3) : arg3;
171
+ arg4 = typeof arg2 !== 'string' && typeof arg3 === 'string' ? Promise.resolve(arg4) : arg4;
172
+ return this.stub(arg1, arg2, arg3, arg4);
173
+ }
174
+
175
+ public spy<T>(id: ServiceIdentifier<T>, property: string): sinon.SinonSpy {
176
+ const spy = sinon.spy();
177
+ this.stub(id, property, spy);
178
+ return spy;
179
+ }
180
+
181
+ private _create<T>(serviceMock: IServiceMock<T>, options: SinonOptions, reset?: boolean): T;
182
+ private _create<T>(ctor: any, options: SinonOptions): T | sinon.SinonMock;
183
+ private _create(arg1: any, options: SinonOptions, reset: boolean = false): any {
184
+ if (this._isServiceMock(arg1)) {
185
+ const service = this._getOrCreateService(arg1, options, reset);
186
+ if (options.mock) {
187
+ return sinon.mock(service);
188
+ }
189
+ this._serviceCollection.set(arg1.id, service);
190
+ return service;
191
+ }
192
+ return options.mock ? sinon.mock(arg1) : this._createStub(arg1);
193
+ }
194
+
195
+ private _getOrCreateService<T>(
196
+ serviceMock: IServiceMock<T>,
197
+ opts: SinonOptions,
198
+ reset?: boolean,
199
+ ): T {
200
+ const service = this._serviceCollection.get(serviceMock.id);
201
+ if (!reset && service && !(service instanceof SyncDescriptor)) {
202
+ if (opts.stub && this._hasSinonOption(service, 'stub')) {
203
+ return service as T;
204
+ }
205
+ if (opts.mock && this._hasSinonOption(service, 'mock')) {
206
+ return service as T;
207
+ }
208
+ return service as T;
209
+ }
210
+ return this._createService(serviceMock, opts);
211
+ }
212
+
213
+ private _createService<T>(serviceMock: IServiceMock<T>, opts: SinonOptions): T {
214
+ const existing = this._serviceCollection.get(serviceMock.id);
215
+ const source =
216
+ serviceMock.service
217
+ ?? (existing instanceof SyncDescriptor ? existing.ctor : undefined);
218
+ const service = this._createStub(source);
219
+ service.sinonOptions = opts;
220
+ return service as T;
221
+ }
222
+
223
+ private _createStub(arg: any): any {
224
+ if (arg instanceof SyncDescriptor) {
225
+ return sinon.createStubInstance(arg.ctor);
226
+ }
227
+ if (typeof arg === 'function') {
228
+ return sinon.createStubInstance(arg);
229
+ }
230
+ if (arg && typeof arg === 'object') {
231
+ return arg;
232
+ }
233
+ return Object.create(null);
234
+ }
235
+
236
+ private _createReplacement(value: any): sinon.SinonStub | sinon.SinonSpy {
237
+ if (typeof value === 'function') {
238
+ return isSinonSpyLike(value) ? value : sinon.spy(value);
239
+ }
240
+ return value ? sinon.stub().returns(value) : sinon.stub();
241
+ }
242
+
243
+ private _hasSinonOption(service: any, key: keyof SinonOptions): boolean {
244
+ return Boolean(service?.sinonOptions?.[key]);
245
+ }
246
+
247
+ private _isServiceMock(arg: any): arg is IServiceMock<unknown> {
248
+ return typeof arg === 'object' && arg !== null && 'id' in arg;
249
+ }
250
+
251
+ public override createChild(services: ServiceCollection): TestInstantiationService {
252
+ return super.createChild(services) as TestInstantiationService;
253
+ }
254
+
255
+ protected override _createChildService(services: ServiceCollection): InstantiationService {
256
+ return new TestInstantiationService(services, false, this);
257
+ }
258
+
259
+ public override dispose(): void {
260
+ sinon.restore();
261
+ if (this._properDispose) {
262
+ super.dispose();
263
+ }
264
+ }
265
+
266
+ public override disposeAsync(): Promise<void> {
267
+ sinon.restore();
268
+ if (this._properDispose) {
269
+ return super.disposeAsync();
270
+ }
271
+ return Promise.resolve();
272
+ }
273
+ }
274
+
275
+ interface SinonOptions {
276
+ mock?: boolean;
277
+ stub?: boolean;
278
+ }
279
+
280
+ export interface ServiceRegistration {
281
+ define<T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T): void;
282
+ defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
283
+ definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;
284
+ }
285
+
286
+ export type ServiceGroup = (reg: ServiceRegistration) => void;
287
+
288
+ export interface CreateServicesOptions {
289
+ readonly base?: readonly ServiceGroup[];
290
+ readonly additionalServices?: (reg: ServiceRegistration) => void;
291
+ readonly strict?: boolean;
292
+ }
293
+
294
+ export function createServices(
295
+ disposables: DisposableStore,
296
+ options: CreateServicesOptions = {},
297
+ ): TestInstantiationService {
298
+ const serviceCollection = new ServiceCollection();
299
+ const instanceIds = new Set<ServiceIdentifier<any>>();
300
+
301
+ const register = <T>(
302
+ id: ServiceIdentifier<T>,
303
+ value: T | Partial<T> | SyncDescriptor<T>,
304
+ isInstance: boolean,
305
+ overwrite: boolean,
306
+ ): void => {
307
+ if (overwrite || !serviceCollection.has(id)) {
308
+ serviceCollection.set(id, value as T | SyncDescriptor<T>);
309
+ }
310
+ if (isInstance) {
311
+ instanceIds.add(id);
312
+ }
313
+ };
314
+
315
+ const baseReg: ServiceRegistration = {
316
+ define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, false),
317
+ defineInstance: (id, instance) => register(id, instance, true, false),
318
+ definePartialInstance: (id, instance) => register(id, instance, true, false),
319
+ };
320
+
321
+ for (const group of options.base ?? []) {
322
+ group(baseReg);
323
+ }
324
+
325
+ if (options.additionalServices) {
326
+ const overrideReg: ServiceRegistration = {
327
+ define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, true),
328
+ defineInstance: (id, instance) => register(id, instance, true, true),
329
+ definePartialInstance: (id, instance) => register(id, instance, true, true),
330
+ };
331
+ options.additionalServices(overrideReg);
332
+ }
333
+
334
+ const instantiationService = disposables.add(
335
+ new TestInstantiationService(serviceCollection, options.strict ?? false),
336
+ );
337
+ disposables.add(toDisposable(() => {
338
+ const serviceDisposables: IDisposable[] = [];
339
+ for (const id of instanceIds) {
340
+ const instance = serviceCollection.get(id);
341
+ if (isDisposable(instance)) {
342
+ serviceDisposables.push(instance);
343
+ }
344
+ }
345
+ dispose(serviceDisposables);
346
+ }));
347
+ return instantiationService;
348
+ }
packages/agent-core-v2/src/_base/di/util/linkedList.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Node<E> {
2
+ static readonly Undefined = new Node<unknown>(undefined);
3
+
4
+ element: E;
5
+ next: Node<E> | typeof Node.Undefined;
6
+ prev: Node<E> | typeof Node.Undefined;
7
+
8
+ constructor(element: E) {
9
+ this.element = element;
10
+ this.next = Node.Undefined;
11
+ this.prev = Node.Undefined;
12
+ }
13
+ }
14
+
15
+ export class LinkedList<E> {
16
+ private _first: Node<E> | typeof Node.Undefined = Node.Undefined;
17
+ private _last: Node<E> | typeof Node.Undefined = Node.Undefined;
18
+ private _size: number = 0;
19
+
20
+ get size(): number {
21
+ return this._size;
22
+ }
23
+
24
+ isEmpty(): boolean {
25
+ return this._first === Node.Undefined;
26
+ }
27
+
28
+ push(element: E): () => void {
29
+ const newNode = new Node(element);
30
+ if (this._first === Node.Undefined) {
31
+ this._first = newNode;
32
+ this._last = newNode;
33
+ } else {
34
+ const oldLast = this._last as Node<E>;
35
+ this._last = newNode;
36
+ newNode.prev = oldLast;
37
+ oldLast.next = newNode;
38
+ }
39
+ this._size += 1;
40
+
41
+ let didRemove = false;
42
+ return () => {
43
+ if (!didRemove) {
44
+ didRemove = true;
45
+ this._remove(newNode);
46
+ }
47
+ };
48
+ }
49
+
50
+ shift(): E | undefined {
51
+ if (this._first === Node.Undefined) {
52
+ return undefined;
53
+ }
54
+ const node = this._first as Node<E>;
55
+ this._remove(node);
56
+ return node.element;
57
+ }
58
+
59
+ private _remove(node: Node<E>): void {
60
+ if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
61
+ const anchor = node.prev as Node<E>;
62
+ anchor.next = node.next;
63
+ (node.next as Node<E>).prev = anchor;
64
+ } else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
65
+ this._first = Node.Undefined;
66
+ this._last = Node.Undefined;
67
+ } else if (node.next === Node.Undefined) {
68
+ this._last = (this._last as Node<E>).prev!;
69
+ (this._last as Node<E>).next = Node.Undefined;
70
+ } else if (node.prev === Node.Undefined) {
71
+ this._first = (this._first as Node<E>).next!;
72
+ (this._first as Node<E>).prev = Node.Undefined;
73
+ }
74
+ this._size -= 1;
75
+ }
76
+
77
+ *[Symbol.iterator](): Iterator<E> {
78
+ let node = this._first;
79
+ while (node !== Node.Undefined) {
80
+ yield (node as Node<E>).element;
81
+ node = (node as Node<E>).next;
82
+ }
83
+ }
84
+ }
packages/agent-core-v2/src/_base/errors/codes.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ErrorInfo {
2
+ readonly title: string;
3
+ readonly retryable: boolean;
4
+ readonly public: boolean;
5
+ readonly action?: string;
6
+ }
7
+
8
+ export interface ErrorDomain {
9
+ readonly codes: { readonly [name: string]: string };
10
+ readonly retryable?: ReadonlyArray<string>;
11
+ readonly info?: { readonly [code: string]: ErrorInfo };
12
+ }
13
+
14
+ const registeredCodes = new Map<string, object>();
15
+ const retryableCodes = new Set<string>();
16
+ const infoOverrides: { [code: string]: ErrorInfo } = {};
17
+
18
+ export function registerErrorDomain(domain: ErrorDomain): void {
19
+ for (const code of Object.values(domain.codes)) {
20
+ const owner = registeredCodes.get(code);
21
+ if (owner !== undefined && owner !== domain.codes) {
22
+ throw new Error(`error code '${code}' is registered by two different domains`);
23
+ }
24
+ registeredCodes.set(code, domain.codes);
25
+ }
26
+ for (const code of domain.retryable ?? []) {
27
+ retryableCodes.add(code);
28
+ }
29
+ for (const [code, info] of Object.entries(domain.info ?? {})) {
30
+ infoOverrides[code] = info;
31
+ }
32
+ }
33
+
34
+ export function isErrorCode(code: unknown): code is string {
35
+ return typeof code === 'string' && registeredCodes.has(code);
36
+ }
37
+
38
+ export function errorInfo(code: string): ErrorInfo {
39
+ const override = infoOverrides[code];
40
+ if (override !== undefined) return override;
41
+ return {
42
+ title: code,
43
+ retryable: retryableCodes.has(code),
44
+ public: true,
45
+ };
46
+ }
47
+
48
+ export const CoreErrors = {
49
+ codes: {
50
+ INTERNAL: 'internal',
51
+ NOT_IMPLEMENTED: 'not_implemented',
52
+ VALIDATION_FAILED: 'validation.failed',
53
+ },
54
+ info: {
55
+ internal: {
56
+ title: 'Internal error',
57
+ retryable: false,
58
+ public: true,
59
+ action: 'Inspect logs or report the issue with diagnostics.',
60
+ },
61
+ not_implemented: {
62
+ title: 'Not implemented',
63
+ retryable: false,
64
+ public: true,
65
+ action: 'This feature is not implemented yet.',
66
+ },
67
+ },
68
+ } as const satisfies ErrorDomain;
69
+
70
+ registerErrorDomain(CoreErrors);
packages/agent-core-v2/src/_base/errors/errorMessage.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { isCodedError } from './serialize';
2
+
3
+ export function toErrorMessage(error: unknown, verbose = false): string {
4
+ if (isCodedError(error)) {
5
+ const base = `[${error.code}] ${error.message}`;
6
+ return verbose && error.details ? `${base} ${JSON.stringify(error.details)}` : base;
7
+ }
8
+ if (error instanceof Error) {
9
+ const base = error.message || error.name;
10
+ if (verbose && error.cause !== undefined) {
11
+ return `${base} (caused by: ${toErrorMessage(error.cause)})`;
12
+ }
13
+ return base;
14
+ }
15
+ if (typeof error === 'string') {
16
+ return error;
17
+ }
18
+ try {
19
+ return JSON.stringify(error);
20
+ } catch {
21
+ return String(error);
22
+ }
23
+ }
packages/agent-core-v2/src/_base/errors/errors.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CoreErrors } from './codes';
2
+ import type { ErrorCode } from '#/errors';
3
+
4
+ export class ExpectedError extends Error {
5
+ readonly isExpected = true;
6
+ }
7
+
8
+ export class ErrorNoTelemetry extends Error {
9
+ constructor(message?: string) {
10
+ super(message);
11
+ this.name = 'CodeExpectedError';
12
+ }
13
+
14
+ static fromError(error: Error): ErrorNoTelemetry {
15
+ const wrapped = new ErrorNoTelemetry(error.message);
16
+ wrapped.stack = error.stack;
17
+ return wrapped;
18
+ }
19
+
20
+ static isErrorNoTelemetry(error: unknown): error is ErrorNoTelemetry {
21
+ return error instanceof Error && error.name === 'CodeExpectedError';
22
+ }
23
+ }
24
+
25
+ export class BugIndicatingError extends Error {
26
+ constructor(message?: string) {
27
+ super(message ?? 'An unexpected bug occurred.');
28
+ this.name = 'BugIndicatingError';
29
+ }
30
+ }
31
+
32
+ export interface Error2Options {
33
+ readonly details?: Readonly<Record<string, unknown>>;
34
+ readonly cause?: unknown;
35
+ readonly name?: string;
36
+ }
37
+
38
+ export class Error2 extends Error {
39
+ readonly code: ErrorCode;
40
+ readonly details?: Readonly<Record<string, unknown>>;
41
+
42
+ constructor(code: ErrorCode, message: string, options?: Error2Options) {
43
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
44
+ this.name = options?.name ?? 'Error2';
45
+ this.code = code;
46
+ this.details = options?.details;
47
+ }
48
+ }
49
+
50
+ export function isError2(error: unknown): error is Error2 {
51
+ return error instanceof Error2;
52
+ }
53
+
54
+ export function unwrapErrorCause(error: unknown): unknown {
55
+ let current = error;
56
+ while (current instanceof Error2 && current.cause !== undefined) {
57
+ current = current.cause;
58
+ }
59
+ return current;
60
+ }
61
+
62
+ export class NotImplementedError extends Error2 {
63
+ constructor(feature?: string) {
64
+ super(
65
+ CoreErrors.codes.NOT_IMPLEMENTED,
66
+ feature ? `Not implemented: ${feature}` : 'Not implemented',
67
+ );
68
+ this.name = 'NotImplementedError';
69
+ }
70
+ }
packages/agent-core-v2/src/_base/errors/serialize.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CoreErrors, errorInfo, isErrorCode } from './codes';
2
+ import type { ErrorCode } from '#/errors';
3
+ import { Error2 } from './errors';
4
+
5
+ export interface ErrorPayload {
6
+ readonly code: ErrorCode;
7
+ readonly message: string;
8
+ readonly name?: string;
9
+ readonly details?: Readonly<Record<string, unknown>>;
10
+ readonly retryable: boolean;
11
+ readonly cause?: ErrorPayload;
12
+ }
13
+
14
+ export type KimiErrorPayload = ErrorPayload;
15
+
16
+ export interface CodedErrorShape {
17
+ readonly code: ErrorCode;
18
+ readonly message: string;
19
+ readonly name?: string;
20
+ readonly details?: Readonly<Record<string, unknown>>;
21
+ }
22
+
23
+ const MAX_CAUSE_DEPTH = 8;
24
+
25
+ export function isCodedError(error: unknown): error is CodedErrorShape {
26
+ if (error === null || typeof error !== 'object') {
27
+ return false;
28
+ }
29
+ const code = (error as { readonly code?: unknown }).code;
30
+ return isErrorCode(code);
31
+ }
32
+
33
+ export function makeErrorPayload(
34
+ code: ErrorCode,
35
+ message: string,
36
+ options?: {
37
+ readonly details?: Readonly<Record<string, unknown>>;
38
+ readonly name?: string;
39
+ },
40
+ ): ErrorPayload {
41
+ return {
42
+ code,
43
+ message,
44
+ name: options?.name,
45
+ details: options?.details,
46
+ retryable: errorInfo(code).retryable,
47
+ };
48
+ }
49
+
50
+ export function toErrorPayload(error: unknown): ErrorPayload {
51
+ return toErrorPayloadAtDepth(error, 0);
52
+ }
53
+
54
+ function toErrorPayloadAtDepth(error: unknown, depth: number): ErrorPayload {
55
+ const payload = toShallowErrorPayload(error);
56
+ if (depth >= MAX_CAUSE_DEPTH) {
57
+ return payload;
58
+ }
59
+ const cause = readErrorCause(error);
60
+ if (cause === undefined) {
61
+ return payload;
62
+ }
63
+ return { ...payload, cause: toErrorPayloadAtDepth(cause, depth + 1) };
64
+ }
65
+
66
+ function toShallowErrorPayload(error: unknown): ErrorPayload {
67
+ if (isCodedError(error)) {
68
+ return {
69
+ code: error.code,
70
+ message: error.message,
71
+ name: error.name,
72
+ details: error.details,
73
+ retryable: errorInfo(error.code).retryable,
74
+ };
75
+ }
76
+ if (error instanceof Error) {
77
+ return makeErrorPayload(CoreErrors.codes.INTERNAL, error.message, { name: error.name });
78
+ }
79
+ return makeErrorPayload(CoreErrors.codes.INTERNAL, String(error));
80
+ }
81
+
82
+ function readErrorCause(error: unknown): unknown {
83
+ if (error === null || typeof error !== 'object') {
84
+ return undefined;
85
+ }
86
+ return (error as { readonly cause?: unknown }).cause;
87
+ }
88
+
89
+ export const toKimiErrorPayload = toErrorPayload;
90
+
91
+ export function fromErrorPayload(payload: ErrorPayload): Error2 {
92
+ return new Error2(payload.code, payload.message, {
93
+ name: payload.name,
94
+ details: payload.details,
95
+ cause: payload.cause === undefined ? undefined : fromErrorPayload(payload.cause),
96
+ });
97
+ }
packages/agent-core-v2/src/_base/errors/unexpectedError.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type UnexpectedErrorHandler = (err: unknown) => void;
2
+
3
+ const defaultHandler: UnexpectedErrorHandler = (err) => {
4
+ console.error('[unexpected]', err);
5
+ };
6
+
7
+ let currentHandler: UnexpectedErrorHandler = defaultHandler;
8
+
9
+ export function setUnexpectedErrorHandler(handler: UnexpectedErrorHandler): void {
10
+ currentHandler = handler;
11
+ }
12
+
13
+ export function resetUnexpectedErrorHandler(): void {
14
+ currentHandler = defaultHandler;
15
+ }
16
+
17
+ export function onUnexpectedError(err: unknown): void {
18
+ try {
19
+ currentHandler(err);
20
+ } catch (handlerErr) {
21
+ console.error('[unexpected] handler threw', handlerErr, 'while reporting', err);
22
+ }
23
+ }
24
+
25
+ export function safelyCallListener(listener: () => void): void {
26
+ try {
27
+ listener();
28
+ } catch (err) {
29
+ onUnexpectedError(err);
30
+ }
31
+ }
packages/agent-core-v2/src/_base/execEnv/bufferedReadable.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Readable } from 'node:stream';
2
+
3
+ export class BufferedReadable extends Readable {
4
+ private readonly _source: Readable;
5
+ private _ended: boolean = false;
6
+
7
+ constructor(source: Readable) {
8
+ super({ highWaterMark: 128 * 1024 });
9
+ this._source = source;
10
+ this._source.on('data', this._onData);
11
+ this._source.on('end', this._onEnd);
12
+ this._source.on('close', this._onClose);
13
+ this._source.on('error', this._onError);
14
+ }
15
+
16
+ override _read(): void {
17
+ if (!this._ended && !this.destroyed) {
18
+ this._source.resume();
19
+ }
20
+ }
21
+
22
+ override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
23
+ this._source.off('data', this._onData);
24
+ this._source.off('end', this._onEnd);
25
+ this._source.off('close', this._onClose);
26
+ this._source.off('error', this._onError);
27
+ this._source.destroy();
28
+ callback(error);
29
+ }
30
+
31
+ private readonly _onData = (chunk: string | Uint8Array): void => {
32
+ if (!this.push(chunk)) {
33
+ this._source.pause();
34
+ }
35
+ };
36
+
37
+ private readonly _onEnd = (): void => {
38
+ this._ended = true;
39
+ this.push(null);
40
+ };
41
+
42
+ private readonly _onClose = (): void => {
43
+ if (!this._ended) {
44
+ this._ended = true;
45
+ this.push(null);
46
+ }
47
+ };
48
+
49
+ private readonly _onError = (error: Error): void => {
50
+ this.destroy(error);
51
+ };
52
+ }
packages/agent-core-v2/src/_base/execEnv/decodeText.ts ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type TextDecodeErrors = 'strict' | 'replace' | 'ignore';
2
+
3
+ export async function* readUtf8Lines(
4
+ source: AsyncIterable<Uint8Array>,
5
+ errors: TextDecodeErrors = 'strict',
6
+ ): AsyncGenerator<string> {
7
+ let pending: Buffer[] = [];
8
+ let offset = 0;
9
+ let pendingOffset = 0;
10
+ for await (const bytes of source) {
11
+ const chunk = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12
+ let start = 0;
13
+ for (let i = 0; i < chunk.length; i++) {
14
+ if (chunk[i] !== 0x0a) continue;
15
+ const piece = chunk.subarray(start, i + 1);
16
+ const lineOffset = pending.length === 0 ? offset + start : pendingOffset;
17
+ const line = pending.length === 0 ? piece : Buffer.concat([...pending, piece]);
18
+ yield decodeTextWithErrors(line, 'utf-8', errors, lineOffset !== 0);
19
+ pending = [];
20
+ start = i + 1;
21
+ }
22
+ if (start < chunk.length) {
23
+ if (pending.length === 0) pendingOffset = offset + start;
24
+ pending.push(Buffer.from(chunk.subarray(start)));
25
+ }
26
+ offset += chunk.length;
27
+ }
28
+ if (pending.length > 0) yield decodeTextWithErrors(Buffer.concat(pending), 'utf-8', errors, pendingOffset !== 0);
29
+ }
30
+
31
+ function isUtf8Continuation(byte: number): boolean {
32
+ return byte >= 0x80 && byte <= 0xbf;
33
+ }
34
+
35
+ function decodeUtf8Ignore(data: Buffer): string {
36
+ let output = '';
37
+ let i = 0;
38
+
39
+ while (i < data.length) {
40
+ const b0 = data[i];
41
+ if (b0 === undefined) break;
42
+
43
+ if (b0 <= 0x7f) {
44
+ output += String.fromCodePoint(b0);
45
+ i += 1;
46
+ continue;
47
+ }
48
+
49
+ if (b0 >= 0xc2 && b0 <= 0xdf) {
50
+ const b1 = data[i + 1];
51
+ if (b1 !== undefined && isUtf8Continuation(b1)) {
52
+ output += String.fromCodePoint(((b0 & 0x1f) << 6) | (b1 & 0x3f));
53
+ i += 2;
54
+ continue;
55
+ }
56
+ i += 1;
57
+ continue;
58
+ }
59
+
60
+ if (b0 >= 0xe0 && b0 <= 0xef) {
61
+ const b1 = data[i + 1];
62
+ const b2 = data[i + 2];
63
+ const validSecond =
64
+ b1 !== undefined &&
65
+ ((b0 === 0xe0 && b1 >= 0xa0 && b1 <= 0xbf) ||
66
+ (b0 >= 0xe1 && b0 <= 0xec && isUtf8Continuation(b1)) ||
67
+ (b0 === 0xed && b1 >= 0x80 && b1 <= 0x9f) ||
68
+ (b0 >= 0xee && b0 <= 0xef && isUtf8Continuation(b1)));
69
+
70
+ if (validSecond && b2 !== undefined && isUtf8Continuation(b2)) {
71
+ output += String.fromCodePoint(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
72
+ i += 3;
73
+ continue;
74
+ }
75
+ i += 1;
76
+ continue;
77
+ }
78
+
79
+ if (b0 >= 0xf0 && b0 <= 0xf4) {
80
+ const b1 = data[i + 1];
81
+ const b2 = data[i + 2];
82
+ const b3 = data[i + 3];
83
+ const validSecond =
84
+ b1 !== undefined &&
85
+ ((b0 === 0xf0 && b1 >= 0x90 && b1 <= 0xbf) ||
86
+ (b0 >= 0xf1 && b0 <= 0xf3 && isUtf8Continuation(b1)) ||
87
+ (b0 === 0xf4 && b1 >= 0x80 && b1 <= 0x8f));
88
+
89
+ if (
90
+ validSecond &&
91
+ b2 !== undefined &&
92
+ b3 !== undefined &&
93
+ isUtf8Continuation(b2) &&
94
+ isUtf8Continuation(b3)
95
+ ) {
96
+ output += String.fromCodePoint(
97
+ ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f),
98
+ );
99
+ i += 4;
100
+ continue;
101
+ }
102
+ i += 1;
103
+ continue;
104
+ }
105
+
106
+ i += 1;
107
+ }
108
+
109
+ return output;
110
+ }
111
+
112
+ function decodeUtf16LeIgnore(data: Buffer): string {
113
+ let output = '';
114
+ let i = 0;
115
+
116
+ while (i + 1 < data.length) {
117
+ const first = data[i];
118
+ const second = data[i + 1];
119
+ if (first === undefined || second === undefined) break;
120
+
121
+ const codeUnit = first | (second << 8);
122
+
123
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
124
+ const lowFirst = data[i + 2];
125
+ const lowSecond = data[i + 3];
126
+ if (lowFirst !== undefined && lowSecond !== undefined) {
127
+ const low = lowFirst | (lowSecond << 8);
128
+ if (low >= 0xdc00 && low <= 0xdfff) {
129
+ const codePoint = 0x10000 + ((codeUnit - 0xd800) << 10) + (low - 0xdc00);
130
+ output += String.fromCodePoint(codePoint);
131
+ i += 4;
132
+ continue;
133
+ }
134
+ }
135
+ i += 2;
136
+ continue;
137
+ }
138
+
139
+ if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
140
+ i += 2;
141
+ continue;
142
+ }
143
+
144
+ output += String.fromCodePoint(codeUnit);
145
+ i += 2;
146
+ }
147
+
148
+ return output;
149
+ }
150
+
151
+ export function decodeTextWithErrors(
152
+ data: Buffer,
153
+ encoding: BufferEncoding,
154
+ errors: TextDecodeErrors = 'strict',
155
+ ignoreBOM: boolean = false,
156
+ ): string {
157
+ let webLabel: string | undefined;
158
+ switch (encoding) {
159
+ case 'utf-8':
160
+ case 'utf8':
161
+ webLabel = 'utf-8';
162
+ break;
163
+ case 'utf16le':
164
+ case 'ucs2':
165
+ case 'ucs-2':
166
+ webLabel = 'utf-16le';
167
+ break;
168
+ default:
169
+ webLabel = undefined;
170
+ }
171
+
172
+ if (webLabel === undefined) {
173
+ return data.toString(encoding);
174
+ }
175
+
176
+ if (errors === 'strict') {
177
+ return new TextDecoder(webLabel, { fatal: true, ignoreBOM }).decode(data);
178
+ }
179
+
180
+ if (errors === 'ignore') {
181
+ return webLabel === 'utf-8' ? decodeUtf8Ignore(data) : decodeUtf16LeIgnore(data);
182
+ }
183
+
184
+ return new TextDecoder(webLabel, { fatal: false, ignoreBOM }).decode(data);
185
+ }
packages/agent-core-v2/src/_base/execEnv/environmentProbe.ts ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { execFile as nodeExecFile } from 'node:child_process';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { access } from 'node:fs/promises';
4
+ import * as nodeOs from 'node:os';
5
+ import * as nodePath from 'node:path';
6
+
7
+ export type OsKind = string;
8
+ export type ShellName = 'bash' | 'sh';
9
+ export type PathClass = 'posix' | 'win32';
10
+
11
+ export class ProbeShellNotFoundError extends Error {
12
+ readonly checked: readonly string[];
13
+
14
+ constructor(message: string, checked: readonly string[]) {
15
+ super(message);
16
+ this.name = 'ProbeShellNotFoundError';
17
+ this.checked = checked;
18
+ }
19
+ }
20
+
21
+ export interface HostEnvironmentInfo {
22
+ readonly osKind: OsKind;
23
+ readonly osArch: string;
24
+ readonly osVersion: string;
25
+ readonly shellName: ShellName;
26
+ readonly shellPath: string;
27
+ readonly pathClass: PathClass;
28
+ readonly homeDir: string;
29
+ }
30
+
31
+ export interface HostEnvironmentProbeDeps {
32
+ readonly platform: string;
33
+ readonly arch: string;
34
+ readonly release: string;
35
+ readonly homeDir: string;
36
+ readonly env: Record<string, string | undefined>;
37
+ readonly isFile: (path: string) => Promise<boolean>;
38
+ readonly execFileText: (
39
+ file: string,
40
+ args: readonly string[],
41
+ timeoutMs: number,
42
+ ) => Promise<string | undefined>;
43
+ }
44
+
45
+ const GIT_EXEC_PATH_TIMEOUT_MS = 5_000;
46
+
47
+ const MINGW_PREFIX_SET: ReadonlySet<string> = new Set([
48
+ 'mingw32',
49
+ 'mingw64',
50
+ 'ucrt64',
51
+ 'clang64',
52
+ 'clangarm64',
53
+ ]);
54
+
55
+ function resolveOsKind(platform: string): OsKind {
56
+ switch (platform) {
57
+ case 'darwin':
58
+ return 'macOS';
59
+ case 'linux':
60
+ return 'Linux';
61
+ case 'win32':
62
+ return 'Windows';
63
+ default:
64
+ return platform;
65
+ }
66
+ }
67
+
68
+ export async function probeHostEnvironment(
69
+ deps: HostEnvironmentProbeDeps,
70
+ ): Promise<HostEnvironmentInfo> {
71
+ const osKind = resolveOsKind(deps.platform);
72
+ const osArch = deps.arch;
73
+ const osVersion = deps.release;
74
+ const pathClass: PathClass = deps.platform === 'win32' ? 'win32' : 'posix';
75
+
76
+ if (deps.platform === 'win32') {
77
+ const shellPath = await locateWindowsGitBash(deps);
78
+ return {
79
+ osKind,
80
+ osArch,
81
+ osVersion,
82
+ shellName: 'bash',
83
+ shellPath,
84
+ pathClass,
85
+ homeDir: deps.homeDir,
86
+ };
87
+ }
88
+
89
+ const candidates: readonly string[] = ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash'];
90
+ let found: string | undefined;
91
+ for (const p of candidates) {
92
+ if (await deps.isFile(p)) {
93
+ found = p;
94
+ break;
95
+ }
96
+ }
97
+ if (found !== undefined) {
98
+ return {
99
+ osKind,
100
+ osArch,
101
+ osVersion,
102
+ shellName: 'bash',
103
+ shellPath: found,
104
+ pathClass,
105
+ homeDir: deps.homeDir,
106
+ };
107
+ }
108
+ return {
109
+ osKind,
110
+ osArch,
111
+ osVersion,
112
+ shellName: 'sh',
113
+ shellPath: '/bin/sh',
114
+ pathClass,
115
+ homeDir: deps.homeDir,
116
+ };
117
+ }
118
+
119
+ async function locateWindowsGitBash(deps: HostEnvironmentProbeDeps): Promise<string> {
120
+ const checked: string[] = [];
121
+
122
+ const override = deps.env['KIMI_SHELL_PATH']?.trim();
123
+ if (override !== undefined && override.length > 0) {
124
+ checked.push(override);
125
+ if (await deps.isFile(override)) {
126
+ return override;
127
+ }
128
+ }
129
+
130
+ const gitExecutables = await findExecutablesOnPath(
131
+ 'git.exe',
132
+ deps.env['PATH'],
133
+ deps.platform,
134
+ deps.isFile,
135
+ );
136
+
137
+ for (const gitExe of gitExecutables) {
138
+ const inferred = gitBashCandidatesFromGitExe(gitExe);
139
+ if (inferred !== undefined) {
140
+ for (const candidate of inferred) {
141
+ checked.push(candidate);
142
+ if (await deps.isFile(candidate)) {
143
+ return candidate;
144
+ }
145
+ }
146
+ }
147
+
148
+ const gitExecPath = await readGitExecPath(deps, gitExe);
149
+ if (gitExecPath === undefined) {
150
+ continue;
151
+ }
152
+ for (const candidate of gitBashCandidatesFromGitExecPath(gitExecPath)) {
153
+ checked.push(candidate);
154
+ if (await deps.isFile(candidate)) {
155
+ return candidate;
156
+ }
157
+ }
158
+ }
159
+
160
+ const candidates: string[] = [
161
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
162
+ 'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
163
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
164
+ 'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
165
+ ];
166
+ const localAppData = deps.env['LOCALAPPDATA']?.trim();
167
+ if (localAppData !== undefined && localAppData.length > 0) {
168
+ candidates.push(`${localAppData}\\Programs\\Git\\bin\\bash.exe`);
169
+ candidates.push(`${localAppData}\\Programs\\Git\\usr\\bin\\bash.exe`);
170
+ }
171
+ for (const candidate of candidates) {
172
+ checked.push(candidate);
173
+ if (await deps.isFile(candidate)) {
174
+ return candidate;
175
+ }
176
+ }
177
+
178
+ throw new ProbeShellNotFoundError(
179
+ 'Git Bash was not found on this Windows host. Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe.',
180
+ checked,
181
+ );
182
+ }
183
+
184
+ async function readGitExecPath(
185
+ deps: HostEnvironmentProbeDeps,
186
+ gitExe: string,
187
+ ): Promise<string | undefined> {
188
+ if (deps.platform === 'win32' && !isAbsoluteWindowsPath(gitExe)) return undefined;
189
+
190
+ const stdout = await deps.execFileText(gitExe, ['--exec-path'], GIT_EXEC_PATH_TIMEOUT_MS);
191
+ if (stdout === undefined) return undefined;
192
+
193
+ for (const line of stdout.split(/\r?\n/)) {
194
+ const execPath = line.trim();
195
+ if (execPath.length > 0) {
196
+ return execPath;
197
+ }
198
+ }
199
+ return undefined;
200
+ }
201
+
202
+ function gitBashCandidatesFromGitExe(gitExe: string): readonly string[] | undefined {
203
+ const normalizedGitExe = nodePath.win32.normalize(normalizeWindowsPath(gitExe));
204
+ const gitDir = nodePath.win32.dirname(normalizedGitExe);
205
+ const gitDirName = nodePath.win32.basename(gitDir).toLowerCase();
206
+ if (gitDirName !== 'cmd' && gitDirName !== 'bin') {
207
+ return undefined;
208
+ }
209
+ return gitBashCandidatesFromGitRoot(nodePath.win32.dirname(gitDir));
210
+ }
211
+
212
+ function gitBashCandidatesFromGitExecPath(execPath: string): readonly string[] {
213
+ const normalized = nodePath.win32.normalize(normalizeWindowsPath(execPath));
214
+ const parts = normalized.split('\\');
215
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
216
+ const segment = parts[i]?.toLowerCase();
217
+ if (segment !== undefined && MINGW_PREFIX_SET.has(segment)) {
218
+ const root = parts.slice(0, i).join('\\');
219
+ if (root.length > 0) {
220
+ return gitBashCandidatesFromGitRoot(root);
221
+ }
222
+ }
223
+ }
224
+
225
+ return gitBashCandidatesFromGitRoot(nodePath.win32.join(normalized, '..', '..'));
226
+ }
227
+
228
+ function gitBashCandidatesFromGitRoot(root: string): readonly string[] {
229
+ return [
230
+ nodePath.win32.normalize(nodePath.win32.join(root, 'bin', 'bash.exe')),
231
+ nodePath.win32.normalize(nodePath.win32.join(root, 'usr', 'bin', 'bash.exe')),
232
+ ];
233
+ }
234
+
235
+ function normalizeWindowsPath(path: string): string {
236
+ return path.replaceAll('/', '\\');
237
+ }
238
+
239
+ function isAbsoluteWindowsPath(path: string): boolean {
240
+ return nodePath.win32.isAbsolute(normalizeWindowsPath(path));
241
+ }
242
+
243
+ function dedupeWindowsPaths(paths: readonly string[]): readonly string[] {
244
+ const deduped: string[] = [];
245
+ const seen = new Set<string>();
246
+ for (const path of paths) {
247
+ const key = normalizeWindowsPath(path).toLowerCase();
248
+ if (seen.has(key)) continue;
249
+ seen.add(key);
250
+ deduped.push(path);
251
+ }
252
+ return deduped;
253
+ }
254
+
255
+ let cachedProbe: Promise<HostEnvironmentInfo> | undefined;
256
+
257
+ export function probeHostEnvironmentFromNode(): Promise<HostEnvironmentInfo> {
258
+ if (cachedProbe !== undefined) return cachedProbe;
259
+ const platform = process.platform;
260
+ const env = process.env as Record<string, string | undefined>;
261
+ const isFile = async (path: string): Promise<boolean> => {
262
+ try {
263
+ await access(path, fsConstants.F_OK);
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ };
269
+ cachedProbe = probeHostEnvironment({
270
+ platform,
271
+ arch: process.arch,
272
+ release: nodeOs.release(),
273
+ homeDir: nodeOs.homedir(),
274
+ env,
275
+ isFile,
276
+ execFileText,
277
+ });
278
+ return cachedProbe;
279
+ }
280
+
281
+ async function findExecutablesOnPath(
282
+ name: string,
283
+ pathEnv: string | undefined,
284
+ platform: string,
285
+ isFile: (p: string) => Promise<boolean>,
286
+ ): Promise<readonly string[]> {
287
+ if (pathEnv === undefined || pathEnv.length === 0) return [];
288
+ const listSep = platform === 'win32' ? ';' : ':';
289
+ const dirSep = platform === 'win32' ? '\\' : '/';
290
+ const paths: string[] = [];
291
+ for (const rawDir of pathEnv.split(listSep)) {
292
+ const dir = rawDir.trim();
293
+ if (dir.length === 0) continue;
294
+ if (platform === 'win32' && !isAbsoluteWindowsPath(dir)) continue;
295
+ const candidate = dir.endsWith(dirSep) ? `${dir}${name}` : `${dir}${dirSep}${name}`;
296
+ if (await isFile(candidate)) {
297
+ paths.push(candidate);
298
+ }
299
+ }
300
+ return platform === 'win32' ? dedupeWindowsPaths(paths) : paths;
301
+ }
302
+
303
+ export async function execFileText(
304
+ file: string,
305
+ args: readonly string[],
306
+ timeoutMs: number,
307
+ ): Promise<string | undefined> {
308
+ return new Promise((resolve) => {
309
+ nodeExecFile(
310
+ file,
311
+ [...args],
312
+ { encoding: 'utf8', timeout: timeoutMs, windowsHide: true },
313
+ (error, stdout) => {
314
+ if (error !== null) {
315
+ resolve(undefined);
316
+ return;
317
+ }
318
+ resolve(stdout);
319
+ },
320
+ );
321
+ });
322
+ }
packages/agent-core-v2/src/_base/execEnv/globPattern.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function globPatternToRegex(pattern: string, caseSensitive: boolean): RegExp {
2
+ let regex = '^';
3
+ for (let i = 0; i < pattern.length; i++) {
4
+ const ch = pattern[i];
5
+ if (ch === undefined) break;
6
+ switch (ch) {
7
+ case '*':
8
+ regex += '[^/]*';
9
+ break;
10
+ case '?':
11
+ regex += '[^/]';
12
+ break;
13
+ case '[': {
14
+ const end = pattern.indexOf(']', i + 1);
15
+ if (end === -1) {
16
+ regex += '\\[';
17
+ } else {
18
+ let charClass = pattern.slice(i + 1, end);
19
+ charClass = charClass.replace(/\\/g, '\\\\');
20
+ if (charClass.startsWith('!')) {
21
+ charClass = '^' + charClass.slice(1);
22
+ } else if (charClass.startsWith('^')) {
23
+ charClass = '\\' + charClass;
24
+ }
25
+ regex += '[' + charClass + ']';
26
+ i = end;
27
+ }
28
+ break;
29
+ }
30
+ case '\\': {
31
+ if (i + 1 < pattern.length) {
32
+ const next = pattern.charAt(i + 1);
33
+ regex += next.replaceAll(/[{}()+.\\[\]^$|]/g, '\\$&');
34
+ i++;
35
+ } else {
36
+ regex += '\\\\';
37
+ }
38
+ break;
39
+ }
40
+ default:
41
+ regex += ch.replaceAll(/[{}()+.\\[\]^$|]/g, '\\$&');
42
+ }
43
+ }
44
+ regex += '$';
45
+ return new RegExp(regex, caseSensitive ? '' : 'i');
46
+ }
packages/agent-core-v2/src/_base/execEnv/loginShellPath.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { userInfo } from 'node:os';
2
+
3
+ import { execFileText } from './environmentProbe';
4
+
5
+ export interface LoginShellPathDeps {
6
+ readonly platform: string;
7
+ readonly env: Record<string, string | undefined>;
8
+ readonly userShell: () => string | undefined;
9
+ readonly execFileText: (
10
+ file: string,
11
+ args: readonly string[],
12
+ timeoutMs: number,
13
+ ) => Promise<string | undefined>;
14
+ }
15
+
16
+ const LOGIN_SHELL_ENV_TIMEOUT_MS = 5_000;
17
+
18
+ export async function probeLoginShellPath(deps: LoginShellPathDeps): Promise<string | undefined> {
19
+ if (deps.platform === 'win32') return undefined;
20
+ const envShell = deps.env['SHELL']?.trim();
21
+ const shell = envShell === undefined || envShell.length === 0 ? deps.userShell() : envShell;
22
+ if (shell === undefined || shell.length === 0) return undefined;
23
+
24
+ const stdout = await deps.execFileText(shell, ['-l', '-c', '/usr/bin/env'], LOGIN_SHELL_ENV_TIMEOUT_MS);
25
+ if (stdout === undefined) return undefined;
26
+
27
+ let path: string | undefined;
28
+ for (const line of stdout.split('\n')) {
29
+ if (line.startsWith('PATH=')) {
30
+ path = line.slice('PATH='.length).trim();
31
+ }
32
+ }
33
+ if (path === undefined || path.length === 0) return undefined;
34
+ return path;
35
+ }
36
+
37
+ export function mergeLoginShellPath(currentPath: string | undefined, loginShellPath: string): string {
38
+ const current = currentPath ?? '';
39
+ const seen = new Set(current.split(':').filter((entry) => entry.length > 0));
40
+ const additions: string[] = [];
41
+ for (const entry of loginShellPath.split(':')) {
42
+ if (!entry.startsWith('/') || seen.has(entry)) continue;
43
+ seen.add(entry);
44
+ additions.push(entry);
45
+ }
46
+ if (additions.length === 0) return current;
47
+ if (currentPath === undefined) return additions.join(':');
48
+ return `${current}:${additions.join(':')}`;
49
+ }
50
+
51
+ export async function applyLoginShellPath(deps: LoginShellPathDeps): Promise<void> {
52
+ const loginShellPath = await probeLoginShellPath(deps);
53
+ if (loginShellPath === undefined) return;
54
+ const currentPath = deps.env['PATH'];
55
+ const merged = mergeLoginShellPath(currentPath, loginShellPath);
56
+ if (merged === (currentPath ?? '')) return;
57
+ deps.env['PATH'] = merged;
58
+ }
59
+
60
+ function userShellFromNode(): string | undefined {
61
+ try {
62
+ const shell = userInfo().shell;
63
+ return shell === null || shell.length === 0 ? undefined : shell;
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ let appliedLoginShellPath: Promise<void> | undefined;
70
+
71
+ export function applyLoginShellPathFromNode(): Promise<void> {
72
+ if (appliedLoginShellPath !== undefined) return appliedLoginShellPath;
73
+ appliedLoginShellPath = applyLoginShellPath({
74
+ platform: process.platform,
75
+ env: process.env as Record<string, string | undefined>,
76
+ userShell: userShellFromNode,
77
+ execFileText,
78
+ });
79
+ return appliedLoginShellPath;
80
+ }
packages/agent-core-v2/src/_base/execEnv/shellPathBridge.ts ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { execFileSync as nodeExecFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+
5
+ import type { HostEnvironmentInfo } from './environmentProbe';
6
+
7
+ export interface ShellPathBridge {
8
+ toShellPath(nativePath: string): string;
9
+ fromShellPath(path: string): string;
10
+ }
11
+
12
+ export type ShellPathBridgeEnv = Pick<HostEnvironmentInfo, 'osKind' | 'shellName' | 'shellPath'>;
13
+
14
+ export interface ShellPathBridgeDeps {
15
+ readonly execFileSync: (file: string, args: readonly string[]) => string;
16
+ readonly isFile: (path: string) => boolean;
17
+ }
18
+
19
+ const CYGPATH_TIMEOUT_MS = 5_000;
20
+
21
+ const DRIVE_COLON_RE = /^\/([a-zA-Z]):(?:[\\/]|$)/;
22
+ const CYGDRIVE_RE = /^\/cygdrive\/([a-zA-Z])(?:\/|$)/;
23
+ const DRIVE_RE = /^\/([a-zA-Z])(?:\/|$)/;
24
+
25
+ const VIRTUAL_FS_PREFIXES: readonly string[] = ['/dev/', '/proc/', '/sys/'];
26
+
27
+ const WIN32_DRIVE_ABSOLUTE_RE = /^[A-Za-z]:[\\/]/;
28
+
29
+ function joinDrive(letter: string, rest: string): string {
30
+ const normalizedRest = rest.replaceAll('\\', '/');
31
+ return normalizedRest === ''
32
+ ? `${letter.toUpperCase()}:/`
33
+ : `${letter.toUpperCase()}:${normalizedRest}`;
34
+ }
35
+
36
+ export function translateShellDrivePath(path: string): string {
37
+ const colonMatch = DRIVE_COLON_RE.exec(path);
38
+ if (colonMatch !== null) {
39
+ return joinDrive(colonMatch[1]!, path.slice(3));
40
+ }
41
+ const cygdriveMatch = CYGDRIVE_RE.exec(path);
42
+ if (cygdriveMatch !== null) {
43
+ return joinDrive(cygdriveMatch[1]!, path.slice(`/cygdrive/${cygdriveMatch[1]!}`.length));
44
+ }
45
+ const driveMatch = DRIVE_RE.exec(path);
46
+ if (driveMatch !== null) {
47
+ return joinDrive(driveMatch[1]!, path.slice(2));
48
+ }
49
+ return path;
50
+ }
51
+
52
+ export function createShellPathBridge(
53
+ env: ShellPathBridgeEnv,
54
+ deps: ShellPathBridgeDeps,
55
+ ): ShellPathBridge {
56
+ const enabled = env.osKind === 'Windows' && env.shellName === 'bash';
57
+
58
+ let cygpathExe: string | null | undefined;
59
+ const segmentCache = new Map<string, string>();
60
+
61
+ function locateCygpath(): string | null {
62
+ if (cygpathExe !== undefined) return cygpathExe;
63
+ const shellDir = nodePath.win32.dirname(env.shellPath);
64
+ const candidates = [nodePath.win32.join(shellDir, 'cygpath.exe')];
65
+ if (nodePath.win32.basename(shellDir).toLowerCase() === 'bin') {
66
+ candidates.push(nodePath.win32.join(shellDir, '..', 'usr', 'bin', 'cygpath.exe'));
67
+ }
68
+ cygpathExe = candidates.find((candidate) => deps.isFile(candidate)) ?? null;
69
+ return cygpathExe;
70
+ }
71
+
72
+ function resolveRootSegment(firstSegment: string): string | null {
73
+ const cached = segmentCache.get(firstSegment);
74
+ if (cached !== undefined) return cached;
75
+
76
+ const exe = locateCygpath();
77
+ if (exe === null) return null;
78
+ let resolved: string;
79
+ try {
80
+ const output = deps.execFileSync(exe, ['-w', '-C', 'UTF8', '--', `/${firstSegment}`]);
81
+ const trimmed = output.replace(/\r?\n$/, '');
82
+ if (!WIN32_DRIVE_ABSOLUTE_RE.test(trimmed) && !trimmed.startsWith('\\\\')) return null;
83
+ resolved = trimmed.replace(/[\\/]$/, '');
84
+ } catch {
85
+ return null;
86
+ }
87
+ segmentCache.set(firstSegment, resolved);
88
+ return resolved;
89
+ }
90
+
91
+ function fromShellPath(path: string): string {
92
+ if (!enabled) return path;
93
+
94
+ if (path.startsWith('//')) return path;
95
+
96
+ if (path.startsWith('/')) {
97
+ const normalized = nodePath.posix.normalize(path);
98
+ const lexical = translateShellDrivePath(normalized);
99
+ if (lexical !== normalized) return lexical;
100
+ if (normalized === '/') return normalized;
101
+ if (VIRTUAL_FS_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return normalized;
102
+ const firstSegment = normalized.slice(1).split('/')[0]!;
103
+ const prefix = resolveRootSegment(firstSegment);
104
+ if (prefix === null) return normalized;
105
+ const remainder = normalized.slice(firstSegment.length + 1);
106
+ const joined = `${prefix}${remainder}`.replaceAll('\\', '/');
107
+ return /^[A-Za-z]:$/.test(joined) ? `${joined}/` : joined;
108
+ }
109
+
110
+ return path;
111
+ }
112
+
113
+ function toShellPath(nativePath: string): string {
114
+ if (!enabled) return nativePath;
115
+
116
+ if (nativePath.startsWith('\\\\')) {
117
+ return nativePath.replaceAll('\\', '/');
118
+ }
119
+
120
+ const driveMatch = /^([A-Za-z]):(?:[\\/]|$)/.exec(nativePath);
121
+ if (driveMatch !== null) {
122
+ const drive = driveMatch[1]!.toLowerCase();
123
+ const rest = nativePath.slice(2).replaceAll('\\', '/');
124
+ return `/${drive}${rest.startsWith('/') ? rest : `/${rest}`}`;
125
+ }
126
+
127
+ return nativePath.replaceAll('\\', '/');
128
+ }
129
+
130
+ return { toShellPath, fromShellPath };
131
+ }
132
+
133
+ const bridgeCache = new Map<string, ShellPathBridge>();
134
+
135
+ export function getShellPathBridge(env: ShellPathBridgeEnv): ShellPathBridge {
136
+ const key = `${env.osKind} ${env.shellName} ${env.shellPath}`;
137
+ const cached = bridgeCache.get(key);
138
+ if (cached !== undefined) return cached;
139
+ const bridge = createShellPathBridge(env, {
140
+ execFileSync: (file, args) =>
141
+ nodeExecFileSync(file, [...args], {
142
+ encoding: 'utf8',
143
+ timeout: CYGPATH_TIMEOUT_MS,
144
+ windowsHide: true,
145
+ }),
146
+ isFile: (path) => existsSync(path),
147
+ });
148
+ bridgeCache.set(key, bridge);
149
+ return bridge;
150
+ }
packages/agent-core-v2/src/_base/lifecycle/disposer.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type TeardownReason = 'scope-close' | 'cascade' | 'unload';
2
+
3
+ export type Disposer = (reason: TeardownReason) => void | Promise<void>;
4
+
5
+ export type EffectResult =
6
+ | void
7
+ | Disposer
8
+ | Promise<Disposer | void>
9
+ | Iterable<Disposer | void>
10
+ | AsyncIterable<Disposer | void>;
11
+
12
+ export type EffectBody = () => EffectResult;
13
+
14
+ export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
15
+ return (
16
+ typeof value === 'object' &&
17
+ value !== null &&
18
+ typeof (value as { then?: unknown }).then === 'function'
19
+ );
20
+ }
21
+
22
+ export function isSyncIterable(value: unknown): value is Iterable<unknown> {
23
+ return (
24
+ typeof value === 'object' &&
25
+ value !== null &&
26
+ typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function'
27
+ );
28
+ }
29
+
30
+ export function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
31
+ return (
32
+ typeof value === 'object' &&
33
+ value !== null &&
34
+ typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] ===
35
+ 'function'
36
+ );
37
+ }
packages/agent-core-v2/src/_base/lifecycle/errors.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class LedgerDisposedError extends Error {
2
+ constructor(
3
+ readonly ledgerLabel: string,
4
+ readonly operation: string,
5
+ readonly ledgerState: 'disposing' | 'disposed',
6
+ ) {
7
+ super(
8
+ `Ledger '${ledgerLabel}' is ${ledgerState}: cannot ${operation} on a ${ledgerState} ledger`,
9
+ );
10
+ this.name = 'LedgerDisposedError';
11
+ }
12
+ }
packages/agent-core-v2/src/_base/lifecycle/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * from './disposer';
2
+ export * from './errors';
3
+ export * from './ledger';
4
+ export * from './lifecycleMachine';
packages/agent-core-v2/src/_base/lifecycle/keyedResource.ts ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface KeyedResourceGeneration {
2
+ readonly owner: string;
3
+ readonly generation: string | number;
4
+ }
5
+
6
+ export interface KeyedResource {
7
+ dispose(): void | Promise<void>;
8
+ abort?(reason?: unknown): void;
9
+ }
10
+
11
+ export interface KeyedResourceLease<Resource> {
12
+ readonly resource: Resource;
13
+ release(): void;
14
+ }
15
+
16
+ interface ResourceEntry<Resource extends KeyedResource> {
17
+ promise: Promise<Resource>;
18
+ resource?: Resource;
19
+ leases: number;
20
+ draining: boolean;
21
+ abortOnDrain: boolean;
22
+ aborted: boolean;
23
+ disposed: boolean;
24
+ drainPromise?: Promise<void>;
25
+ releaseDrain?: () => void;
26
+ }
27
+
28
+ export class KeyedResourceLeasePool<Key, Resource extends KeyedResource> {
29
+ private readonly entries = new Map<Key, ResourceEntry<Resource>>();
30
+ private withdrawn = false;
31
+ private withdrawal?: Promise<void>;
32
+
33
+ constructor(
34
+ readonly identity: KeyedResourceGeneration,
35
+ private readonly create: (key: Key) => Resource | Promise<Resource>,
36
+ ) {}
37
+
38
+ acquire(key: Key): Promise<KeyedResourceLease<Resource>> {
39
+ if (this.withdrawn) return Promise.reject(this.unavailable());
40
+ let entry = this.entries.get(key);
41
+ if (entry === undefined) {
42
+ entry = this.createEntry(key);
43
+ this.entries.set(key, entry);
44
+ }
45
+ if (entry.draining) return Promise.reject(this.unavailable());
46
+ entry.leases += 1;
47
+ return entry.promise.then(
48
+ (resource) => {
49
+ let active = true;
50
+ return {
51
+ resource,
52
+ release: () => {
53
+ if (!active) return;
54
+ active = false;
55
+ entry.leases -= 1;
56
+ if (entry.leases === 0) entry.releaseDrain?.();
57
+ },
58
+ };
59
+ },
60
+ (error: unknown) => {
61
+ entry.leases -= 1;
62
+ if (entry.leases === 0) entry.releaseDrain?.();
63
+ throw error;
64
+ },
65
+ );
66
+ }
67
+
68
+ has(key: Key): boolean {
69
+ return this.entries.has(key);
70
+ }
71
+
72
+ disposeKey(key: Key, reason?: unknown, abort = false): Promise<void> {
73
+ const entry = this.entries.get(key);
74
+ if (entry === undefined) return Promise.resolve();
75
+ this.entries.delete(key);
76
+ return this.drain(entry, reason, abort);
77
+ }
78
+
79
+ withdraw(reason?: unknown): Promise<void> {
80
+ if (this.withdrawal !== undefined) return this.withdrawal;
81
+ this.withdrawn = true;
82
+ const entries = [...this.entries.values()];
83
+ this.entries.clear();
84
+ this.withdrawal = Promise.all(entries.map((entry) => this.drain(entry, reason, false))).then(
85
+ () => undefined,
86
+ );
87
+ return this.withdrawal;
88
+ }
89
+
90
+ private createEntry(key: Key): ResourceEntry<Resource> {
91
+ const entry: ResourceEntry<Resource> = {
92
+ promise: undefined as unknown as Promise<Resource>,
93
+ leases: 0,
94
+ draining: false,
95
+ abortOnDrain: false,
96
+ aborted: false,
97
+ disposed: false,
98
+ };
99
+ entry.promise = Promise.resolve()
100
+ .then(() => this.create(key))
101
+ .then(
102
+ (resource) => {
103
+ entry.resource = resource;
104
+ if (entry.abortOnDrain) this.abort(entry);
105
+ return resource;
106
+ },
107
+ (error: unknown) => {
108
+ if (this.entries.get(key) === entry) this.entries.delete(key);
109
+ throw error;
110
+ },
111
+ );
112
+ return entry;
113
+ }
114
+
115
+ private drain(entry: ResourceEntry<Resource>, reason?: unknown, abort = false): Promise<void> {
116
+ entry.abortOnDrain ||= abort;
117
+ entry.drainPromise ??= (async () => {
118
+ entry.draining = true;
119
+ try {
120
+ await entry.promise;
121
+ } catch {
122
+ return;
123
+ }
124
+ if (entry.abortOnDrain) this.abort(entry, reason);
125
+ if (entry.leases > 0) {
126
+ await new Promise<void>((resolve) => {
127
+ entry.releaseDrain = resolve;
128
+ });
129
+ }
130
+ if (entry.disposed) return;
131
+ entry.disposed = true;
132
+ await entry.resource!.dispose();
133
+ })();
134
+ return entry.drainPromise;
135
+ }
136
+
137
+ private abort(entry: ResourceEntry<Resource>, reason?: unknown): void {
138
+ if (entry.aborted || entry.resource?.abort === undefined) return;
139
+ entry.aborted = true;
140
+ try {
141
+ entry.resource.abort(reason);
142
+ } catch {}
143
+ }
144
+
145
+ private unavailable(): Error {
146
+ return new Error(
147
+ `resource generation ${this.identity.owner}:${String(this.identity.generation)} is withdrawn`,
148
+ );
149
+ }
150
+ }
packages/agent-core-v2/src/_base/lifecycle/ledger.ts ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { onUnexpectedError } from '../errors/unexpectedError';
2
+ import {
3
+ isAsyncIterable,
4
+ isPromiseLike,
5
+ isSyncIterable,
6
+ type Disposer,
7
+ type EffectBody,
8
+ type TeardownReason,
9
+ } from './disposer';
10
+ import { LedgerDisposedError } from './errors';
11
+
12
+ export type LedgerState = 'active' | 'disposing' | 'disposed';
13
+
14
+ export interface LedgerEntryInfo {
15
+ readonly label: string;
16
+ readonly kind: 'disposer' | 'effect' | 'ledger';
17
+ readonly stack?: string;
18
+ readonly children?: readonly LedgerEntryInfo[];
19
+ }
20
+
21
+ export interface LedgerEntry {
22
+ readonly label: string;
23
+ readonly disposed: boolean;
24
+ dispose(reason?: TeardownReason): void | Promise<void>;
25
+ release(): void;
26
+ }
27
+
28
+ interface EntryRecord {
29
+ label: string;
30
+ kind: 'disposer' | 'effect' | 'ledger';
31
+ stack?: string;
32
+ active: boolean;
33
+ run: Disposer;
34
+ ledger?: Ledger;
35
+ }
36
+
37
+ export class Ledger {
38
+ static captureStacks = false;
39
+
40
+ private _state: LedgerState = 'active';
41
+ private readonly _records: EntryRecord[] = [];
42
+ private _teardownPromise: Promise<void> | undefined;
43
+ private _parentEntry: LedgerEntry | undefined;
44
+
45
+ constructor(readonly label: string = 'ledger') {}
46
+
47
+ get state(): LedgerState {
48
+ return this._state;
49
+ }
50
+
51
+ get isActive(): boolean {
52
+ return this._state === 'active';
53
+ }
54
+
55
+ get isDisposed(): boolean {
56
+ return this._state === 'disposed';
57
+ }
58
+
59
+ get size(): number {
60
+ return this._records.reduce((count, record) => count + (record.active ? 1 : 0), 0);
61
+ }
62
+
63
+ register(disposer: Disposer, label: string = 'disposer'): LedgerEntry {
64
+ this._assertActive('register');
65
+ return this._push({ label, kind: 'disposer', active: true, run: disposer });
66
+ }
67
+
68
+ registerFinalizer(disposer: Disposer, label: string = 'finalizer'): LedgerEntry {
69
+ this._assertActive('registerFinalizer');
70
+ return this._push({ label, kind: 'disposer', active: true, run: disposer }, true);
71
+ }
72
+
73
+ effect(body: EffectBody, label: string = 'effect'): LedgerEntry {
74
+ this._assertActive('effect');
75
+ const out = body();
76
+ if (typeof out === 'function') {
77
+ return this._push({ label, kind: 'effect', active: true, run: out });
78
+ }
79
+ if (isPromiseLike(out)) {
80
+ const promise = Promise.resolve(out);
81
+ return this._push({
82
+ label,
83
+ kind: 'effect',
84
+ active: true,
85
+ run: async (reason) => {
86
+ const disposer = await promise;
87
+ if (typeof disposer === 'function') await disposer(reason);
88
+ },
89
+ });
90
+ }
91
+ if (isAsyncIterable(out)) {
92
+ const promise = driveAsyncEffect(out);
93
+ return this._push({
94
+ label,
95
+ kind: 'effect',
96
+ active: true,
97
+ run: async (reason) => {
98
+ const disposer = await promise;
99
+ await disposer(reason);
100
+ },
101
+ });
102
+ }
103
+ if (isSyncIterable(out)) {
104
+ const run = driveSyncEffect(out);
105
+ return this._push({ label, kind: 'effect', active: true, run });
106
+ }
107
+ return this._push({ label, kind: 'effect', active: true, run: () => {} });
108
+ }
109
+
110
+ createChild(label: string = 'ledger'): Ledger {
111
+ this._assertActive('createChild');
112
+ const child = new Ledger(label);
113
+ child._parentEntry = this._push({
114
+ label,
115
+ kind: 'ledger',
116
+ active: true,
117
+ run: (reason) => child.teardown(reason),
118
+ ledger: child,
119
+ });
120
+ return child;
121
+ }
122
+
123
+ teardown(reason: TeardownReason = 'scope-close'): void | Promise<void> {
124
+ if (this._state !== 'active') {
125
+ return this._teardownPromise;
126
+ }
127
+ this._state = 'disposing';
128
+ this._detachFromParent();
129
+ const out = drainRecords(this._records, reason);
130
+ if (isPromiseLike(out)) {
131
+ this._teardownPromise = Promise.resolve(out).then(() => {
132
+ this._state = 'disposed';
133
+ });
134
+ return this._teardownPromise;
135
+ }
136
+ this._state = 'disposed';
137
+ return undefined;
138
+ }
139
+
140
+ clear(reason: TeardownReason = 'scope-close'): void | Promise<void> {
141
+ this._assertActive('clear');
142
+ return drainRecords(this._records, reason);
143
+ }
144
+
145
+ entries(): LedgerEntryInfo[] {
146
+ const infos: LedgerEntryInfo[] = [];
147
+ for (const record of this._records) {
148
+ if (!record.active) continue;
149
+ infos.push({
150
+ label: record.label,
151
+ kind: record.kind,
152
+ stack: record.stack,
153
+ children: record.ledger?.entries(),
154
+ });
155
+ }
156
+ return infos;
157
+ }
158
+
159
+ private _push(record: EntryRecord, front = false): LedgerEntry {
160
+ if (Ledger.captureStacks) {
161
+ record.stack = new Error('Ledger registration').stack;
162
+ }
163
+ if (front) {
164
+ this._records.unshift(record);
165
+ } else {
166
+ this._records.push(record);
167
+ }
168
+ return {
169
+ label: record.label,
170
+ get disposed() {
171
+ return !record.active;
172
+ },
173
+ dispose: (reason: TeardownReason = 'scope-close') => {
174
+ if (!record.active) return undefined;
175
+ record.active = false;
176
+ this._remove(record);
177
+ return runGuarded(record, reason);
178
+ },
179
+ release: () => {
180
+ if (!record.active) return;
181
+ record.active = false;
182
+ this._remove(record);
183
+ },
184
+ };
185
+ }
186
+
187
+ private _remove(record: EntryRecord): void {
188
+ const index = this._records.indexOf(record);
189
+ if (index >= 0) {
190
+ this._records.splice(index, 1);
191
+ }
192
+ }
193
+
194
+ private _detachFromParent(): void {
195
+ this._parentEntry?.release();
196
+ this._parentEntry = undefined;
197
+ }
198
+
199
+ private _assertActive(operation: string): void {
200
+ if (this._state !== 'active') {
201
+ throw new LedgerDisposedError(this.label, operation, this._state);
202
+ }
203
+ }
204
+ }
205
+
206
+ function runGuarded(record: EntryRecord, reason: TeardownReason): void | Promise<void> {
207
+ let out: void | Promise<void>;
208
+ try {
209
+ out = record.run(reason);
210
+ } catch (error) {
211
+ onUnexpectedError(tagged(error, record.label));
212
+ return undefined;
213
+ }
214
+ if (isPromiseLike(out)) {
215
+ return Promise.resolve(out).catch((error: unknown) => {
216
+ onUnexpectedError(tagged(error, record.label));
217
+ });
218
+ }
219
+ return undefined;
220
+ }
221
+
222
+ function tagged(error: unknown, label: string): unknown {
223
+ if (error instanceof Error) {
224
+ error.message = `[ledger:${label}] ${error.message}`;
225
+ return error;
226
+ }
227
+ return new Error(`[ledger:${label}] ${String(error)}`);
228
+ }
229
+
230
+ function drainRecords(records: EntryRecord[], reason: TeardownReason): void | Promise<void> {
231
+ let index = records.length;
232
+ const step = (): void | Promise<void> => {
233
+ while (index > 0) {
234
+ index -= 1;
235
+ const record = records[index]!;
236
+ if (!record.active) continue;
237
+ record.active = false;
238
+ const out = runGuarded(record, reason);
239
+ if (isPromiseLike(out)) {
240
+ return Promise.resolve(out).then(step);
241
+ }
242
+ }
243
+ records.length = 0;
244
+ return undefined;
245
+ };
246
+ return step();
247
+ }
248
+
249
+ function runDisposersReverse(
250
+ disposers: readonly Disposer[],
251
+ reason: TeardownReason,
252
+ ): void | Promise<void> {
253
+ let index = disposers.length;
254
+ const step = (): void | Promise<void> => {
255
+ while (index > 0) {
256
+ index -= 1;
257
+ const disposer = disposers[index]!;
258
+ let out: void | Promise<void>;
259
+ try {
260
+ out = disposer(reason);
261
+ } catch (error) {
262
+ onUnexpectedError(tagged(error, 'effect'));
263
+ continue;
264
+ }
265
+ if (isPromiseLike(out)) {
266
+ return Promise.resolve(out)
267
+ .catch((error: unknown) => { onUnexpectedError(tagged(error, 'effect')); })
268
+ .then(step);
269
+ }
270
+ }
271
+ return undefined;
272
+ };
273
+ return step();
274
+ }
275
+
276
+ function collect(step: IteratorResult<Disposer | void>, disposers: Disposer[]): void {
277
+ if (typeof step.value === 'function') {
278
+ disposers.push(step.value);
279
+ }
280
+ }
281
+
282
+ function driveSyncEffect(iterable: Iterable<Disposer | void>): Disposer {
283
+ const disposers: Disposer[] = [];
284
+ const iterator = iterable[Symbol.iterator]();
285
+ try {
286
+ let step = iterator.next();
287
+ while (!step.done) {
288
+ collect(step, disposers);
289
+ step = iterator.next();
290
+ }
291
+ collect(step, disposers);
292
+ } catch (error) {
293
+ const rollback = runDisposersReverse(disposers, 'unload');
294
+ if (isPromiseLike(rollback)) {
295
+ Promise.resolve(rollback).catch((error: unknown) => { onUnexpectedError(error); });
296
+ }
297
+ throw error;
298
+ }
299
+ return (reason) => runDisposersReverse(disposers, reason);
300
+ }
301
+
302
+ async function driveAsyncEffect(iterable: AsyncIterable<Disposer | void>): Promise<Disposer> {
303
+ const disposers: Disposer[] = [];
304
+ const iterator = iterable[Symbol.asyncIterator]();
305
+ try {
306
+ let step = await iterator.next();
307
+ while (!step.done) {
308
+ collect(step, disposers);
309
+ step = await iterator.next();
310
+ }
311
+ collect(step, disposers);
312
+ } catch (error) {
313
+ await runDisposersReverse(disposers, 'unload');
314
+ throw error;
315
+ }
316
+ return (reason) => runDisposersReverse(disposers, reason);
317
+ }
packages/agent-core-v2/src/_base/lifecycle/lifecycleMachine.ts ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type LifecycleTransitionErrorReason =
2
+ | 'invalid_state'
3
+ | 'transition_conflict'
4
+ | 'missing_commit_state'
5
+ | 'missing_rollback_state'
6
+ | 'already_committed'
7
+ | 'already_rolled_back';
8
+
9
+ export interface LifecycleTransitionErrorOptions<TState extends string> {
10
+ readonly reason: LifecycleTransitionErrorReason;
11
+ readonly operation: string;
12
+ readonly state: TState;
13
+ readonly expected?: TState | readonly TState[];
14
+ readonly activeOperation?: string;
15
+ }
16
+
17
+ export class LifecycleTransitionError<TState extends string = string> extends Error {
18
+ readonly reason: LifecycleTransitionErrorReason;
19
+ readonly operation: string;
20
+ readonly state: TState;
21
+ readonly expected?: TState | readonly TState[];
22
+ readonly activeOperation?: string;
23
+
24
+ constructor(options: LifecycleTransitionErrorOptions<TState>) {
25
+ super(formatTransitionError(options));
26
+ this.name = 'LifecycleTransitionError';
27
+ this.reason = options.reason;
28
+ this.operation = options.operation;
29
+ this.state = options.state;
30
+ this.expected = options.expected;
31
+ this.activeOperation = options.activeOperation;
32
+ }
33
+ }
34
+
35
+ export interface LifecycleSnapshot<TState extends string> {
36
+ readonly state: TState;
37
+ readonly transitioning: boolean;
38
+ readonly operation?: string;
39
+ }
40
+
41
+ export interface LifecycleSwitchOptions<TState extends string> {
42
+ readonly operation: string;
43
+ readonly from: TState | readonly TState[];
44
+ readonly to: TState;
45
+ }
46
+
47
+ export interface LifecycleTransactionOptions<TState extends string> {
48
+ readonly operation: string;
49
+ readonly from: TState | readonly TState[];
50
+ readonly enter: TState;
51
+ readonly commit?: TState;
52
+ readonly rollback?: TState;
53
+ }
54
+
55
+ export interface LifecycleTransaction<TState extends string> {
56
+ defer(callback: LifecycleAction): void;
57
+ rollback(callback: LifecycleAction): void;
58
+ afterCommit(callback: LifecycleAction): void;
59
+ commit(state: TState): void;
60
+ rollbackTo(state: TState): void;
61
+ }
62
+
63
+ export type LifecycleAction = () => void | Promise<void>;
64
+
65
+ export class LifecycleMachine<TState extends string> {
66
+ private _state: TState;
67
+ private activeOperation: string | undefined;
68
+
69
+ constructor(initial: TState) {
70
+ this._state = initial;
71
+ }
72
+
73
+ get state(): TState {
74
+ return this._state;
75
+ }
76
+
77
+ get snapshot(): LifecycleSnapshot<TState> {
78
+ return this.activeOperation === undefined
79
+ ? { state: this._state, transitioning: false }
80
+ : {
81
+ state: this._state,
82
+ transitioning: true,
83
+ operation: this.activeOperation,
84
+ };
85
+ }
86
+
87
+ is(...states: readonly TState[]): boolean {
88
+ return states.includes(this._state);
89
+ }
90
+
91
+ switch(options: LifecycleSwitchOptions<TState>): void {
92
+ this.assertIdle(options.operation);
93
+ this.assertState(options.operation, options.from);
94
+ this._state = options.to;
95
+ }
96
+
97
+ async transaction<TResult>(
98
+ options: LifecycleTransactionOptions<TState>,
99
+ callback: (transaction: LifecycleTransaction<TState>) => TResult | Promise<TResult>,
100
+ ): Promise<TResult> {
101
+ this.assertIdle(options.operation);
102
+ this.assertState(options.operation, options.from);
103
+
104
+ this.activeOperation = options.operation;
105
+ this._state = options.enter;
106
+
107
+ const deferred: LifecycleAction[] = [];
108
+ const rollbacks: LifecycleAction[] = [];
109
+ const afterCommit: LifecycleAction[] = [];
110
+ let commitState = options.commit;
111
+ let rollbackState = options.rollback;
112
+ let commitSelected = false;
113
+ let rollbackSelected = false;
114
+
115
+ const transaction: LifecycleTransaction<TState> = {
116
+ defer: (action) => deferred.push(action),
117
+ rollback: (action) => rollbacks.push(action),
118
+ afterCommit: (action) => afterCommit.push(action),
119
+ commit: (state) => {
120
+ if (commitSelected) {
121
+ throw this.createError(options.operation, 'already_committed');
122
+ }
123
+ commitSelected = true;
124
+ commitState = state;
125
+ },
126
+ rollbackTo: (state) => {
127
+ if (rollbackSelected) {
128
+ throw this.createError(options.operation, 'already_rolled_back');
129
+ }
130
+ rollbackSelected = true;
131
+ rollbackState = state;
132
+ },
133
+ };
134
+
135
+ try {
136
+ let result: TResult;
137
+ try {
138
+ result = await callback(transaction);
139
+ } catch (error) {
140
+ const errors: unknown[] = [
141
+ error,
142
+ ...(await runActions(rollbacks)),
143
+ ...(await runActions(deferred)),
144
+ ];
145
+
146
+ if (rollbackState === undefined) {
147
+ errors.push(this.createError(options.operation, 'missing_rollback_state'));
148
+ } else {
149
+ this._state = rollbackState;
150
+ }
151
+ throw aggregateErrors(errors, `Lifecycle transaction "${options.operation}" failed`);
152
+ }
153
+
154
+ if (commitState === undefined) {
155
+ throw this.createError(options.operation, 'missing_commit_state');
156
+ }
157
+
158
+ const cleanupErrors = await runActions(deferred);
159
+ this._state = commitState;
160
+ const afterCommitErrors = await runActions(afterCommit);
161
+ const errors = [...cleanupErrors, ...afterCommitErrors];
162
+ if (errors.length > 0) {
163
+ throw aggregateErrors(
164
+ errors,
165
+ `Lifecycle transaction "${options.operation}" committed with action failures`,
166
+ );
167
+ }
168
+ return result;
169
+ } finally {
170
+ this.activeOperation = undefined;
171
+ }
172
+ }
173
+
174
+ private assertIdle(operation: string): void {
175
+ if (this.activeOperation === undefined) return;
176
+ throw this.createError(operation, 'transition_conflict', undefined, this.activeOperation);
177
+ }
178
+
179
+ private assertState(operation: string, expected: TState | readonly TState[]): void {
180
+ const states = Array.isArray(expected) ? expected : [expected];
181
+ if (states.includes(this._state)) return;
182
+ throw this.createError(operation, 'invalid_state', expected);
183
+ }
184
+
185
+ private createError(
186
+ operation: string,
187
+ reason: LifecycleTransitionErrorReason,
188
+ expected?: TState | readonly TState[],
189
+ activeOperation?: string,
190
+ ): LifecycleTransitionError<TState> {
191
+ return new LifecycleTransitionError({
192
+ reason,
193
+ operation,
194
+ state: this._state,
195
+ expected,
196
+ activeOperation,
197
+ });
198
+ }
199
+ }
200
+
201
+ async function runActions(actions: readonly LifecycleAction[]): Promise<unknown[]> {
202
+ const errors: unknown[] = [];
203
+ for (let index = actions.length - 1; index >= 0; index -= 1) {
204
+ try {
205
+ await actions[index]!();
206
+ } catch (error) {
207
+ errors.push(error);
208
+ }
209
+ }
210
+ return errors;
211
+ }
212
+
213
+ function aggregateErrors(errors: readonly unknown[], message: string): unknown {
214
+ if (errors.length === 1) return errors[0];
215
+ return new AggregateError(errors, message, { cause: errors[0] });
216
+ }
217
+
218
+ function formatTransitionError<TState extends string>(
219
+ options: LifecycleTransitionErrorOptions<TState>,
220
+ ): string {
221
+ switch (options.reason) {
222
+ case 'invalid_state':
223
+ return `Lifecycle operation "${options.operation}" is not allowed from state "${options.state}"`;
224
+ case 'transition_conflict':
225
+ return `Lifecycle operation "${options.operation}" conflicts with active operation "${options.activeOperation}"`;
226
+ case 'missing_commit_state':
227
+ return `Lifecycle operation "${options.operation}" did not select a commit state`;
228
+ case 'missing_rollback_state':
229
+ return `Lifecycle operation "${options.operation}" did not select a rollback state`;
230
+ case 'already_committed':
231
+ return `Lifecycle operation "${options.operation}" already selected a commit state`;
232
+ case 'already_rolled_back':
233
+ return `Lifecycle operation "${options.operation}" already selected a rollback state`;
234
+ }
235
+ }
packages/agent-core-v2/src/_base/log/fileLog.ts ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { appendFileSync, mkdirSync } from 'node:fs';
2
+ import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
3
+ import { dirname } from 'pathe';
4
+
5
+ import { formatEntry, type FormatOptions } from './formatter';
6
+ import type { ILogWriter, LogEntry } from './log';
7
+
8
+ export const PENDING_MAX = 1000;
9
+ const STDERR_NOTICE_INTERVAL_MS = 30_000;
10
+
11
+ class AsyncSerialQueue {
12
+ private tail: Promise<unknown> = Promise.resolve();
13
+
14
+ run<T>(task: () => Promise<T>): Promise<T> {
15
+ const next = this.tail.then(task, task);
16
+ this.tail = next.catch(() => {});
17
+ return next;
18
+ }
19
+ }
20
+
21
+ export interface RotatingFileWriterOptions {
22
+ readonly path: string;
23
+ readonly maxBytes: number;
24
+ readonly files: number;
25
+ }
26
+
27
+ async function syncDir(dirPath: string): Promise<void> {
28
+ if (process.platform === 'win32') return;
29
+ const dirFh = await open(dirPath, 'r');
30
+ try {
31
+ await dirFh.sync();
32
+ } finally {
33
+ await dirFh.close();
34
+ }
35
+ }
36
+
37
+ export class RotatingFileWriter {
38
+ private readonly queue = new AsyncSerialQueue();
39
+ private pending: string[] = [];
40
+ private dropped = 0;
41
+ private closed = false;
42
+ private lastStderrNotice = 0;
43
+ private currentBytes = -1;
44
+ private directorySynced = false;
45
+
46
+ constructor(private readonly options: RotatingFileWriterOptions) {}
47
+
48
+ enqueue(line: string): void {
49
+ if (this.closed) return;
50
+ if (this.pending.length >= PENDING_MAX) {
51
+ this.pending.shift();
52
+ this.dropped++;
53
+ }
54
+ this.pending.push(line);
55
+ this.scheduleDrain();
56
+ }
57
+
58
+ async flush(): Promise<boolean> {
59
+ return this.queue.run(() => this.drain());
60
+ }
61
+
62
+ async close(): Promise<void> {
63
+ if (this.closed) return;
64
+ this.closed = true;
65
+ try {
66
+ await this.flush();
67
+ } catch {
68
+ }
69
+ }
70
+
71
+ flushSync(): void {
72
+ if (this.closed || this.pending.length === 0) return;
73
+ try {
74
+ mkdirSync(dirname(this.options.path), { recursive: true });
75
+ const body = this.pending.join('') + this.takeDroppedNotice();
76
+ this.pending = [];
77
+ appendFileSync(this.options.path, body);
78
+ } catch (error) {
79
+ this.noteFailure(error);
80
+ }
81
+ }
82
+
83
+ private scheduleDrain(): void {
84
+ if (this.closed) return;
85
+ queueMicrotask(() => {
86
+ if (this.closed || this.pending.length === 0) return;
87
+ this.queue.run(() => this.drain()).catch(() => {});
88
+ });
89
+ }
90
+
91
+ private async drain(): Promise<boolean> {
92
+ if (this.pending.length === 0) return true;
93
+ const droppedLine = this.takeDroppedNotice();
94
+ const lines = droppedLine === '' ? [...this.pending] : [...this.pending, droppedLine];
95
+ this.pending = [];
96
+ try {
97
+ await mkdir(dirname(this.options.path), { recursive: true });
98
+ if (this.currentBytes < 0) {
99
+ this.currentBytes = await this.statSize(this.options.path);
100
+ }
101
+ await this.appendLines(lines);
102
+
103
+ if (!this.directorySynced) {
104
+ await syncDir(dirname(this.options.path));
105
+ this.directorySynced = true;
106
+ }
107
+
108
+ return true;
109
+ } catch (error) {
110
+ this.noteFailure(error);
111
+ this.restorePending(lines);
112
+ return false;
113
+ }
114
+ }
115
+
116
+ private restorePending(lines: readonly string[]): void {
117
+ const restored = [...lines, ...this.pending];
118
+ const overflow = restored.length - PENDING_MAX;
119
+ if (overflow <= 0) {
120
+ this.pending = restored;
121
+ return;
122
+ }
123
+ this.dropped += overflow;
124
+ this.pending = restored.slice(overflow);
125
+ }
126
+
127
+ private async appendLines(lines: readonly string[]): Promise<void> {
128
+ let chunk = '';
129
+ let chunkBytes = 0;
130
+ for (const line of lines) {
131
+ const lineBytes = Buffer.byteLength(line, 'utf-8');
132
+ if (
133
+ chunkBytes > 0 &&
134
+ (chunkBytes + lineBytes > this.options.maxBytes ||
135
+ this.currentBytes + chunkBytes + lineBytes > this.options.maxBytes)
136
+ ) {
137
+ await this.appendChunk(chunk);
138
+ chunk = '';
139
+ chunkBytes = 0;
140
+ }
141
+
142
+ if (
143
+ chunkBytes === 0 &&
144
+ this.currentBytes > 0 &&
145
+ this.currentBytes + lineBytes > this.options.maxBytes
146
+ ) {
147
+ await this.rotate();
148
+ }
149
+
150
+ chunk += line;
151
+ chunkBytes += lineBytes;
152
+ }
153
+ if (chunkBytes > 0) {
154
+ await this.appendChunk(chunk);
155
+ }
156
+ }
157
+
158
+ private async appendChunk(chunk: string): Promise<void> {
159
+ const fh = await open(this.options.path, 'a');
160
+ try {
161
+ await fh.appendFile(chunk, 'utf-8');
162
+ await fh.sync();
163
+ } finally {
164
+ await fh.close();
165
+ }
166
+ this.currentBytes += Buffer.byteLength(chunk, 'utf-8');
167
+ if (this.currentBytes >= this.options.maxBytes) {
168
+ await this.rotate();
169
+ }
170
+ }
171
+
172
+ private async rotate(): Promise<void> {
173
+ const { path, files } = this.options;
174
+ for (let i = files - 2; i >= 1; i--) {
175
+ const from = `${path}.${i}`;
176
+ const to = `${path}.${i + 1}`;
177
+ try {
178
+ await rename(from, to);
179
+ } catch (error) {
180
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
181
+ }
182
+ }
183
+ try {
184
+ await rename(path, `${path}.1`);
185
+ } catch (error) {
186
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
187
+ }
188
+ try {
189
+ await unlink(`${path}.${files}`);
190
+ } catch (error) {
191
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
192
+ }
193
+ this.currentBytes = 0;
194
+ this.directorySynced = false;
195
+ }
196
+
197
+ private async statSize(p: string): Promise<number> {
198
+ try {
199
+ const s = await stat(p);
200
+ return s.size;
201
+ } catch (error) {
202
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 0;
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ private takeDroppedNotice(): string {
208
+ if (this.dropped === 0) return '';
209
+ const line = `... dropped ${this.dropped} entries ...\n`;
210
+ this.dropped = 0;
211
+ return line;
212
+ }
213
+
214
+ private noteFailure(error: unknown): void {
215
+ const now = Date.now();
216
+ if (now - this.lastStderrNotice < STDERR_NOTICE_INTERVAL_MS) return;
217
+ this.lastStderrNotice = now;
218
+ const code = (error as NodeJS.ErrnoException)?.code ?? 'UNKNOWN';
219
+ try {
220
+ process.stderr.write(`[logger] write failed: ${code}\n`);
221
+ } catch {}
222
+ }
223
+ }
224
+
225
+ export interface FileLogWriterOptions extends RotatingFileWriterOptions {
226
+ readonly format?: FormatOptions;
227
+ }
228
+
229
+ export class FileLogWriter implements ILogWriter {
230
+ private readonly sink: RotatingFileWriter;
231
+ private readonly format: FormatOptions;
232
+
233
+ constructor(options: FileLogWriterOptions) {
234
+ this.sink = new RotatingFileWriter(options);
235
+ this.format = options.format ?? {};
236
+ }
237
+
238
+ write(entry: LogEntry): void {
239
+ const { text, dropped } = formatEntry(entry, this.format);
240
+ if (dropped) return;
241
+ this.sink.enqueue(text + '\n');
242
+ }
243
+
244
+ async flush(): Promise<void> {
245
+ await this.sink.flush();
246
+ }
247
+
248
+ close(): Promise<void> {
249
+ return this.sink.close();
250
+ }
251
+
252
+ flushSync(): void {
253
+ this.sink.flushSync();
254
+ }
255
+ }
256
+
257
+ export function createFileLogWriter(options: FileLogWriterOptions): FileLogWriter {
258
+ return new FileLogWriter(options);
259
+ }
260
+
261
+ export class MemoryLogWriter implements ILogWriter {
262
+ readonly entries: LogEntry[] = [];
263
+ write(entry: LogEntry): void {
264
+ this.entries.push(entry);
265
+ }
266
+ }
267
+
268
+ export class ConsoleLogWriter implements ILogWriter {
269
+ write(entry: LogEntry): void {
270
+ const { text } = formatEntry(entry, { ansi: process.stderr.isTTY === true });
271
+ switch (entry.level) {
272
+ case 'error':
273
+ console.error(text);
274
+ break;
275
+ case 'warn':
276
+ console.warn(text);
277
+ break;
278
+ case 'debug':
279
+ console.debug(text);
280
+ break;
281
+ default:
282
+ console.log(text);
283
+ }
284
+ }
285
+ }
packages/agent-core-v2/src/_base/log/formatter.ts ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LogContext, LogEntry, LogEntryError } from './log';
2
+
3
+ export const MSG_MAX_CHARS = 200;
4
+ export const CTX_VALUE_MAX_CHARS = 2048;
5
+ export const STACK_MAX_BYTES = 2048;
6
+ export const ENTRY_MAX_BYTES = 4096;
7
+ export const REDACT_MAX_DEPTH = 10;
8
+
9
+ const REDACTED_KEYS: ReadonlySet<string> = new Set([
10
+ 'authorization',
11
+ 'apikey',
12
+ 'token',
13
+ 'refreshtoken',
14
+ 'accesstoken',
15
+ 'idtoken',
16
+ 'password',
17
+ 'secret',
18
+ 'clientsecret',
19
+ 'apisecret',
20
+ 'cookie',
21
+ 'setcookie',
22
+ 'bearer',
23
+ ]);
24
+
25
+ const SAFE_KEY_RE = /^[\w.-]+$/;
26
+ const ELLIPSIS = '…';
27
+ const TRUNCATED_TAIL = ` …truncated`;
28
+ const REDACTED = '[REDACTED]';
29
+ const RAW_SECRET_PATTERNS: readonly RegExp[] = [
30
+ /\b(authorization\s*[:=]\s*bearer\s+)[^\s"'`]+/gi,
31
+ /\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|secret)\s*[:=]\s*)[^\s"'`]+/gi,
32
+ /\b(cookie\s*[:=]\s*)[^\r\n]+/gi,
33
+ ];
34
+
35
+ const LEVEL_LABEL: Record<Exclude<LogEntry['level'], never>, string> = {
36
+ error: 'ERROR',
37
+ warn: 'WARN ',
38
+ info: 'INFO ',
39
+ debug: 'DEBUG',
40
+ };
41
+
42
+ const ANSI_LEVEL: Record<Exclude<LogEntry['level'], never>, string> = {
43
+ error: '',
44
+ warn: '',
45
+ info: '',
46
+ debug: '',
47
+ };
48
+ const ANSI_RESET = '';
49
+
50
+ function normalizeKey(key: string): string {
51
+ return key.toLowerCase().replaceAll(/[_\-.]/g, '');
52
+ }
53
+
54
+ export function redactCtx(ctx: LogContext): LogContext {
55
+ const seen = new WeakSet<object>();
56
+ const walk = (value: unknown, depth: number): unknown => {
57
+ if (depth > REDACT_MAX_DEPTH) return '[REDACTED:depth]';
58
+ if (value === null || typeof value !== 'object') return value;
59
+ if (seen.has(value)) return '[REDACTED:cycle]';
60
+ seen.add(value);
61
+ if (Array.isArray(value)) {
62
+ return value.map((item) => walk(item, depth + 1));
63
+ }
64
+ const out: Record<string, unknown> = {};
65
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
66
+ out[key] = REDACTED_KEYS.has(normalizeKey(key)) ? REDACTED : walk(raw, depth + 1);
67
+ }
68
+ return out;
69
+ };
70
+ return walk(ctx, 0) as LogContext;
71
+ }
72
+
73
+ export interface FormatOptions {
74
+ readonly ansi?: boolean;
75
+ readonly omitContextKeys?: readonly string[];
76
+ }
77
+
78
+ export interface FormattedEntry {
79
+ readonly text: string;
80
+ readonly dropped: boolean;
81
+ }
82
+
83
+ function truncate(value: string, max: number): string {
84
+ return value.length <= max ? value : value.slice(0, max - 1) + ELLIPSIS;
85
+ }
86
+
87
+ function serializeValue(raw: unknown): string {
88
+ if (typeof raw === 'string') return redactString(raw);
89
+ if (raw === undefined) return 'undefined';
90
+ if (raw === null) return 'null';
91
+ if (
92
+ typeof raw === 'number' ||
93
+ typeof raw === 'boolean' ||
94
+ typeof raw === 'bigint' ||
95
+ typeof raw === 'symbol'
96
+ ) {
97
+ return String(raw);
98
+ }
99
+ try {
100
+ const json = JSON.stringify(raw);
101
+ if (json !== undefined) return json;
102
+ } catch {
103
+ }
104
+ if (typeof raw === 'function') return raw.name === '' ? '[Function]' : `[Function: ${raw.name}]`;
105
+ return Object.prototype.toString.call(raw);
106
+ }
107
+
108
+ function redactString(value: string): string {
109
+ let out = value;
110
+ for (const pattern of RAW_SECRET_PATTERNS) {
111
+ out = out.replace(pattern, `$1${REDACTED}`);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function quote(value: string): string {
117
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`;
118
+ }
119
+
120
+ function formatPair(key: string, raw: unknown): string {
121
+ const limited = truncate(serializeValue(raw), CTX_VALUE_MAX_CHARS);
122
+ const renderedKey = SAFE_KEY_RE.test(key) ? key : quote(key);
123
+ const renderedVal = /[\s="\\]/.test(limited) || limited.length === 0 ? quote(limited) : limited;
124
+ return `${renderedKey}=${renderedVal}`;
125
+ }
126
+
127
+ function clipBytes(text: string, maxBytes: number): string {
128
+ if (Buffer.byteLength(text, 'utf-8') <= maxBytes) return text;
129
+ let lo = 0;
130
+ let hi = text.length;
131
+ while (lo < hi) {
132
+ const mid = (lo + hi + 1) >> 1;
133
+ if (
134
+ Buffer.byteLength(text.slice(0, mid), 'utf-8') <=
135
+ maxBytes - Buffer.byteLength(TRUNCATED_TAIL, 'utf-8')
136
+ ) {
137
+ lo = mid;
138
+ } else {
139
+ hi = mid - 1;
140
+ }
141
+ }
142
+ return text.slice(0, lo) + TRUNCATED_TAIL;
143
+ }
144
+
145
+ function clipStack(stack: string): string {
146
+ if (Buffer.byteLength(stack, 'utf-8') <= STACK_MAX_BYTES) return stack;
147
+ return clipBytes(stack, STACK_MAX_BYTES);
148
+ }
149
+
150
+ function indentStack(stack: string): string {
151
+ return stack
152
+ .split('\n')
153
+ .map((line, i) => (i === 0 ? ` ${line}` : ` ${line.trimStart()}`))
154
+ .join('\n');
155
+ }
156
+
157
+ export function formatEntry(entry: LogEntry, options: FormatOptions = {}): FormattedEntry {
158
+ const ctx = entry.ctx ? redactCtx(entry.ctx) : undefined;
159
+ const omitContextKeys = new Set(options.omitContextKeys ?? []);
160
+ const msg = truncate(entry.msg, MSG_MAX_CHARS);
161
+ const pairs: string[] = [];
162
+ if (ctx) {
163
+ for (const [k, v] of Object.entries(ctx)) {
164
+ if (omitContextKeys.has(k)) continue;
165
+ if (v !== undefined) pairs.push(formatPair(k, v));
166
+ }
167
+ }
168
+
169
+ const time = new Date(entry.t).toISOString();
170
+ const label = LEVEL_LABEL[entry.level];
171
+ const rendered = pairs.length === 0
172
+ ? `${time} ${label} ${msg}`
173
+ : `${time} ${label} ${msg} ${pairs.join(' ')}`;
174
+
175
+ let head = Buffer.byteLength(rendered, 'utf-8') > ENTRY_MAX_BYTES
176
+ ? clipBytes(rendered, ENTRY_MAX_BYTES)
177
+ : rendered;
178
+
179
+ if (options.ansi === true) {
180
+ head = `${ANSI_LEVEL[entry.level]}${head}${ANSI_RESET}`;
181
+ }
182
+
183
+ if (entry.error?.stack) {
184
+ head = `${head}\n${indentStack(clipStack(redactString(entry.error.stack)))}`;
185
+ } else if (entry.error?.message) {
186
+ head = `${head}\n Error: ${redactString(entry.error.message)}`;
187
+ }
188
+
189
+ return { text: head, dropped: false };
190
+ }
191
+
192
+ export function extractError(value: Error): LogEntryError {
193
+ return typeof value.stack === 'string'
194
+ ? { message: value.message, stack: value.stack }
195
+ : { message: value.message };
196
+ }
packages/agent-core-v2/src/_base/log/log.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
2
+
3
+ export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
4
+
5
+ export type LogContext = Record<string, unknown>;
6
+
7
+ export type LogPayload = unknown;
8
+
9
+ export interface LogEntryError {
10
+ readonly message: string;
11
+ readonly stack?: string;
12
+ }
13
+
14
+ export interface LogEntry {
15
+ readonly t: number;
16
+ readonly level: Exclude<LogLevel, 'off'>;
17
+ readonly msg: string;
18
+ readonly ctx?: LogContext;
19
+ readonly error?: LogEntryError;
20
+ }
21
+
22
+ export interface ILogWriter {
23
+ write(entry: LogEntry): void;
24
+ flush?(): Promise<void>;
25
+ close?(): Promise<void>;
26
+ flushSync?(): void;
27
+ }
28
+
29
+ export interface ILogger {
30
+ error(message: string, payload?: LogPayload): void;
31
+ warn(message: string, payload?: LogPayload): void;
32
+ info(message: string, payload?: LogPayload): void;
33
+ debug(message: string, payload?: LogPayload): void;
34
+ child(ctx: LogContext): ILogger;
35
+ }
36
+
37
+ export interface ILogService extends ILogger {
38
+ readonly _serviceBrand: undefined;
39
+
40
+ readonly level: LogLevel;
41
+ setLevel(level: LogLevel): void;
42
+ flush(): Promise<void>;
43
+ }
44
+
45
+ export const ILogService: ServiceIdentifier<ILogService> =
46
+ createDecorator<ILogService>('logService');
47
+
48
+ const LEVEL_ORDER: Record<LogLevel, number> = {
49
+ off: 0,
50
+ error: 1,
51
+ warn: 2,
52
+ info: 3,
53
+ debug: 4,
54
+ };
55
+
56
+ export function levelEnabled(level: LogLevel, configured: LogLevel): boolean {
57
+ if (level === 'off' || configured === 'off') return false;
58
+ return LEVEL_ORDER[level] <= LEVEL_ORDER[configured];
59
+ }
packages/agent-core-v2/src/_base/log/logConfig.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { join } from 'pathe';
2
+
3
+ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
4
+ import type { ScopeSeed } from '#/_base/di/scope';
5
+
6
+ import type { LogLevel } from './log';
7
+
8
+ export const DEFAULT_LOG_LEVEL: LogLevel = 'info';
9
+ export const DEFAULT_GLOBAL_MAX_BYTES = 6 * 1024 * 1024;
10
+ export const DEFAULT_GLOBAL_FILES = 5;
11
+ export const DEFAULT_SESSION_MAX_BYTES = 5 * 1024 * 1024;
12
+ export const DEFAULT_SESSION_FILES = 3;
13
+
14
+ export interface LoggingConfig {
15
+ readonly level: LogLevel;
16
+ readonly globalLogPath: string;
17
+ readonly globalMaxBytes: number;
18
+ readonly globalFiles: number;
19
+ readonly sessionMaxBytes: number;
20
+ readonly sessionFiles: number;
21
+ }
22
+
23
+ export interface ILogOptions extends LoggingConfig {}
24
+
25
+ export const ILogOptions: ServiceIdentifier<ILogOptions> =
26
+ createDecorator<ILogOptions>('logOptions');
27
+
28
+ export interface ResolveLoggingInput {
29
+ readonly homeDir: string;
30
+ readonly env: NodeJS.ProcessEnv;
31
+ }
32
+
33
+ export function resolveGlobalLogPath(homeDir: string): string {
34
+ return join(homeDir, 'logs', 'kimi-code.log');
35
+ }
36
+
37
+ export function resolveSessionLogPath(sessionDir: string): string {
38
+ return join(sessionDir, 'logs', 'kimi-code.log');
39
+ }
40
+
41
+ export function resolveLoggingConfig(input: ResolveLoggingInput): LoggingConfig {
42
+ const env = input.env;
43
+ return {
44
+ level: parseLevel(env['KIMI_LOG_LEVEL']) ?? DEFAULT_LOG_LEVEL,
45
+ globalLogPath: resolveGlobalLogPath(input.homeDir),
46
+ globalMaxBytes: parsePositiveInt(env['KIMI_LOG_GLOBAL_MAX_BYTES']) ?? DEFAULT_GLOBAL_MAX_BYTES,
47
+ globalFiles: parsePositiveInt(env['KIMI_LOG_GLOBAL_FILES']) ?? DEFAULT_GLOBAL_FILES,
48
+ sessionMaxBytes:
49
+ parsePositiveInt(env['KIMI_LOG_SESSION_MAX_BYTES']) ?? DEFAULT_SESSION_MAX_BYTES,
50
+ sessionFiles: parsePositiveInt(env['KIMI_LOG_SESSION_FILES']) ?? DEFAULT_SESSION_FILES,
51
+ };
52
+ }
53
+
54
+ export function logSeed(config: LoggingConfig): ScopeSeed {
55
+ return [
56
+ [ILogOptions as ServiceIdentifier<unknown>, config satisfies ILogOptions],
57
+ ];
58
+ }
59
+
60
+ function parseLevel(value: string | undefined): LogLevel | undefined {
61
+ if (value === undefined) return undefined;
62
+ const v = value.toLowerCase().trim();
63
+ if (v === 'off' || v === 'error' || v === 'warn' || v === 'info' || v === 'debug') return v;
64
+ return undefined;
65
+ }
66
+
67
+ function parsePositiveInt(value: string | undefined): number | undefined {
68
+ if (value === undefined) return undefined;
69
+ const n = Number.parseInt(value, 10);
70
+ if (!Number.isFinite(n) || n <= 0) return undefined;
71
+ return n;
72
+ }
packages/agent-core-v2/src/_base/log/logService.ts ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Service } from '#/_base/di/service';
2
+ import { LifecycleScope } from '#/app/scopes';
3
+ import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
4
+
5
+ import {
6
+ type ILogger,
7
+ type ILogWriter,
8
+ type LogContext,
9
+ type LogEntry,
10
+ type LogEntryError,
11
+ type LogLevel,
12
+ type LogPayload,
13
+ ILogService,
14
+ levelEnabled,
15
+ } from './log';
16
+ import { createFileLogWriter, type FileLogWriter } from './fileLog';
17
+ import { ILogOptions } from './logConfig';
18
+
19
+ const pendingLogCloses = new Set<Promise<void>>();
20
+
21
+ export function trackLogClose(close: Promise<void>): void {
22
+ const tracked = close.then(
23
+ () => undefined,
24
+ () => undefined,
25
+ );
26
+ pendingLogCloses.add(tracked);
27
+ void tracked.finally(() => pendingLogCloses.delete(tracked));
28
+ }
29
+
30
+ export async function drainLogCloses(): Promise<void> {
31
+ while (pendingLogCloses.size > 0) {
32
+ await Promise.all(pendingLogCloses);
33
+ }
34
+ }
35
+
36
+ interface ExtractedPayload {
37
+ readonly ctx?: LogContext;
38
+ readonly error?: LogEntryError;
39
+ }
40
+
41
+ function errorEntry(error: Error): LogEntryError {
42
+ return { message: error.message, stack: error.stack };
43
+ }
44
+
45
+ function stringifyPayload(payload: Exclude<LogPayload, undefined>): string {
46
+ if (typeof payload === 'string') return payload;
47
+ try {
48
+ const json = JSON.stringify(payload);
49
+ return json === undefined ? String(payload) : json;
50
+ } catch {
51
+ return String(payload);
52
+ }
53
+ }
54
+
55
+ function extractPayload(payload: LogPayload): ExtractedPayload | undefined {
56
+ if (payload === undefined) return {};
57
+ if (payload instanceof Error) return { error: errorEntry(payload) };
58
+ if (typeof payload === 'object' && payload !== null) {
59
+ let entries: [string, unknown][];
60
+ try {
61
+ entries = Object.entries(payload as Record<string, unknown>);
62
+ } catch {
63
+ return undefined;
64
+ }
65
+
66
+ let error: LogEntryError | undefined;
67
+ const ctx: LogContext = {};
68
+ for (const [key, value] of entries) {
69
+ if (key === 'error' && value instanceof Error) {
70
+ error = errorEntry(value);
71
+ continue;
72
+ }
73
+ ctx[key] = value;
74
+ }
75
+ return {
76
+ ...(Object.keys(ctx).length > 0 ? { ctx } : {}),
77
+ ...(error !== undefined ? { error } : {}),
78
+ };
79
+ }
80
+
81
+ return { ctx: { reason: stringifyPayload(payload) } };
82
+ }
83
+
84
+ export interface LogLevelState {
85
+ level: LogLevel;
86
+ }
87
+
88
+ export class BoundLogger extends Service implements ILogger {
89
+ constructor(
90
+ protected readonly writer: ILogWriter,
91
+ private readonly levelState: LogLevelState,
92
+ private readonly bound: LogContext = {},
93
+ ) {
94
+ super();
95
+ }
96
+
97
+ child(ctx: LogContext): ILogger {
98
+ return new BoundLogger(this.writer, this.levelState, { ...this.bound, ...ctx });
99
+ }
100
+
101
+ error(message: string, payload?: LogPayload): void {
102
+ this.emit('error', message, payload);
103
+ }
104
+ warn(message: string, payload?: LogPayload): void {
105
+ this.emit('warn', message, payload);
106
+ }
107
+ info(message: string, payload?: LogPayload): void {
108
+ this.emit('info', message, payload);
109
+ }
110
+ debug(message: string, payload?: LogPayload): void {
111
+ this.emit('debug', message, payload);
112
+ }
113
+
114
+ private emit(
115
+ level: Exclude<LogLevel, 'off'>,
116
+ message: string,
117
+ payload?: LogPayload,
118
+ ): void {
119
+ if (!levelEnabled(level, this.levelState.level)) return;
120
+ const extracted = extractPayload(payload);
121
+ if (extracted === undefined) return;
122
+ const payloadCtx = extracted.ctx;
123
+ const error = extracted.error;
124
+ const ctx =
125
+ payloadCtx !== undefined || Object.keys(this.bound).length > 0
126
+ ? { ...payloadCtx, ...this.bound }
127
+ : undefined;
128
+ const entry: LogEntry = {
129
+ t: Date.now(),
130
+ level,
131
+ msg: message,
132
+ ...(ctx !== undefined ? { ctx } : {}),
133
+ ...(error !== undefined ? { error } : {}),
134
+ };
135
+ this.writer.write(entry);
136
+ }
137
+ }
138
+
139
+ export class AppLogService extends BoundLogger implements ILogService {
140
+ declare readonly _serviceBrand: undefined;
141
+ private readonly sink: FileLogWriter;
142
+ private readonly rootLevel: LogLevelState;
143
+
144
+ constructor(@ILogOptions options: ILogOptions) {
145
+ const sink = createFileLogWriter({
146
+ path: options.globalLogPath,
147
+ maxBytes: options.globalMaxBytes,
148
+ files: options.globalFiles,
149
+ });
150
+ const rootLevel: LogLevelState = { level: options.level };
151
+ super(sink, rootLevel);
152
+ this.sink = sink;
153
+ this.rootLevel = rootLevel;
154
+ }
155
+
156
+ get level(): LogLevel {
157
+ return this.rootLevel.level;
158
+ }
159
+
160
+ setLevel(level: LogLevel): void {
161
+ this.rootLevel.level = level;
162
+ }
163
+
164
+ flush(): Promise<void> {
165
+ return this.sink.flush();
166
+ }
167
+
168
+ override dispose(): void {
169
+ this.sink.flushSync();
170
+ trackLogClose(this.sink.close());
171
+ super.dispose();
172
+ }
173
+ }
174
+
175
+ registerScopedService(
176
+ LifecycleScope.App,
177
+ ILogService,
178
+ AppLogService,
179
+ ScopeActivation.OnScopeCreated,
180
+ 'log',
181
+ );
packages/agent-core-v2/src/_base/state/stateRegistry.ts ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Disposable, type IDisposable, toDisposable } from '../di/lifecycle';
2
+ import { BugIndicatingError } from '../errors/errors';
3
+ import { Emitter, type Event } from '../event';
4
+
5
+ export interface StateKey<T> {
6
+ readonly name: string;
7
+ readonly initial: () => T;
8
+ readonly snapshotExcluded?: boolean;
9
+ }
10
+
11
+ export interface StateChange {
12
+ readonly key: string;
13
+ readonly value: unknown;
14
+ }
15
+
16
+ export interface StateInspection {
17
+ readonly scope: string;
18
+ readonly state: Record<string, unknown>;
19
+ readonly parent?: StateInspection;
20
+ }
21
+
22
+ export interface IStateRegistry {
23
+ contributeState<T>(key: StateKey<T>): IDisposable;
24
+ has(key: StateKey<unknown>): boolean;
25
+ get<T>(key: StateKey<T>): T;
26
+ set<T>(key: StateKey<T>, value: T): void;
27
+ onDidChange<T>(key: StateKey<T>): Event<T>;
28
+ readonly onDidChangeAny: Event<StateChange>;
29
+ entries(): readonly [string, unknown][];
30
+ snapshot(): Record<string, unknown>;
31
+ inspect(): StateInspection;
32
+ }
33
+
34
+ export class StateRegistry extends Disposable implements IStateRegistry {
35
+ private readonly values = new Map<string, unknown>();
36
+ private readonly registrations = new Map<string, object>();
37
+ private readonly excludedFromSnapshot = new Set<string>();
38
+ private readonly keyEmitters = new Map<string, Emitter<unknown>>();
39
+ private readonly anyEmitter = this._register(new Emitter<StateChange>());
40
+ readonly onDidChangeAny: Event<StateChange> = this.anyEmitter.event;
41
+
42
+ protected readonly inspectScope: string = 'unknown';
43
+ protected inspectParent?: IStateRegistry;
44
+
45
+ contributeState<T>(key: StateKey<T>): IDisposable {
46
+ const replayable = (key as StateKey<T> & { readonly replayable?: unknown }).replayable;
47
+ if (typeof replayable === 'object' && replayable !== null) {
48
+ throw new BugIndicatingError(
49
+ `replayable state key '${key.name}' must be contributed to the Agent-scope state service`,
50
+ );
51
+ }
52
+ return this.contributeKey(key);
53
+ }
54
+
55
+ protected contributeKey<T>(key: StateKey<T>): IDisposable {
56
+ if (this.values.has(key.name)) {
57
+ throw new BugIndicatingError(`state key '${key.name}' is already registered`);
58
+ }
59
+ const registration = {};
60
+ this.registrations.set(key.name, registration);
61
+ this.values.set(key.name, key.initial());
62
+ if (key.snapshotExcluded === true) {
63
+ this.excludedFromSnapshot.add(key.name);
64
+ }
65
+ return toDisposable(() => {
66
+ if (this.registrations.get(key.name) !== registration) return;
67
+ this.registrations.delete(key.name);
68
+ this.values.delete(key.name);
69
+ this.excludedFromSnapshot.delete(key.name);
70
+ this.keyEmitters.get(key.name)?.dispose();
71
+ this.keyEmitters.delete(key.name);
72
+ });
73
+ }
74
+
75
+ has(key: StateKey<unknown>): boolean {
76
+ return this.values.has(key.name);
77
+ }
78
+
79
+ get<T>(key: StateKey<T>): T {
80
+ if (!this.values.has(key.name)) {
81
+ throw new BugIndicatingError(`state key '${key.name}' is not registered`);
82
+ }
83
+ return this.values.get(key.name) as T;
84
+ }
85
+
86
+ set<T>(key: StateKey<T>, value: T): void {
87
+ if (!this.values.has(key.name)) {
88
+ throw new BugIndicatingError(`state key '${key.name}' is not registered`);
89
+ }
90
+ this.values.set(key.name, value);
91
+ this.keyEmitters.get(key.name)?.fire(value);
92
+ this.anyEmitter.fire({ key: key.name, value });
93
+ }
94
+
95
+ onDidChange<T>(key: StateKey<T>): Event<T> {
96
+ let emitter = this.keyEmitters.get(key.name);
97
+ if (emitter === undefined) {
98
+ emitter = this._register(new Emitter<unknown>());
99
+ this.keyEmitters.set(key.name, emitter);
100
+ }
101
+ return emitter.event as Event<T>;
102
+ }
103
+
104
+ entries(): readonly [string, unknown][] {
105
+ return Array.from(this.values.entries());
106
+ }
107
+
108
+ snapshot(): Record<string, unknown> {
109
+ const out: Record<string, unknown> = {};
110
+ for (const [key, value] of this.values) {
111
+ if (this.excludedFromSnapshot.has(key)) continue;
112
+ out[key] = toJsonSafe(value, new WeakSet());
113
+ }
114
+ return out;
115
+ }
116
+
117
+ inspect(): StateInspection {
118
+ return {
119
+ scope: this.inspectScope,
120
+ state: this.snapshot(),
121
+ parent: this.inspectParent?.inspect(),
122
+ };
123
+ }
124
+ }
125
+
126
+ function toJsonSafe(value: unknown, seen: WeakSet<object>): unknown {
127
+ if (value === undefined || value === null) return null;
128
+ if (typeof value === 'function') return '(function)';
129
+ if (typeof value === 'bigint') return value.toString();
130
+ if (typeof value !== 'object') return value;
131
+ if (seen.has(value)) return '(circular)';
132
+ seen.add(value);
133
+ try {
134
+ if (value instanceof Date) return value.toJSON();
135
+ if (Array.isArray(value)) return value.map((item) => toJsonSafe(item, seen));
136
+ if (value instanceof Map) {
137
+ const entries = [...value.entries()];
138
+ const objectKeys = entries.every(([key]) => ['string', 'number'].includes(typeof key));
139
+ if (objectKeys) {
140
+ return Object.fromEntries(
141
+ entries.map(([key, item]) => [key, toJsonSafe(item, seen)] as const),
142
+ );
143
+ }
144
+ return entries.map(([key, item]) => [toJsonSafe(key, seen), toJsonSafe(item, seen)]);
145
+ }
146
+ if (value instanceof Set) {
147
+ return [...value.values()].map((item) => toJsonSafe(item, seen));
148
+ }
149
+ const proto: unknown = Object.getPrototypeOf(value);
150
+ if (proto !== Object.prototype && proto !== null) {
151
+ const ctor = (value as { constructor?: { name?: string } }).constructor;
152
+ return `(${ctor?.name ?? 'object'})`;
153
+ }
154
+ const out: Record<string, unknown> = {};
155
+ for (const [key, item] of Object.entries(value)) {
156
+ if (typeof item === 'function') continue;
157
+ out[key] = toJsonSafe(item, seen);
158
+ }
159
+ return out;
160
+ } finally {
161
+ seen.delete(value);
162
+ }
163
+ }
packages/agent-core-v2/src/_base/text/encoding.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type UtfTextEncoding = 'utf-8' | 'utf-16le' | 'utf-16be';
2
+
3
+ export interface TextClassification {
4
+ readonly isBinary: boolean;
5
+ readonly encoding: UtfTextEncoding;
6
+ }
7
+
8
+ export const FS_BINARY_NONPRINTABLE_FRACTION = 0.3;
9
+
10
+ export interface TextEncodingDetection {
11
+ readonly encoding: UtfTextEncoding;
12
+ readonly seemsBinary: boolean;
13
+ }
14
+
15
+ export const ENCODING_DETECTION_SAMPLE_BYTES = 512;
16
+
17
+ const MIN_ZERO_BYTES_FOR_UTF16 = 2;
18
+
19
+ const UTF16BE_BOM = [0xfe, 0xff] as const;
20
+ const UTF16LE_BOM = [0xff, 0xfe] as const;
21
+ const UTF8_BOM = [0xef, 0xbb, 0xbf] as const;
22
+
23
+ function sniffTextEncoding(sample: Uint8Array): TextEncodingDetection {
24
+ if (sample.length >= 2) {
25
+ const b0 = sample[0]!;
26
+ const b1 = sample[1]!;
27
+ if (b0 === UTF16BE_BOM[0] && b1 === UTF16BE_BOM[1]) {
28
+ return { encoding: 'utf-16be', seemsBinary: false };
29
+ }
30
+ if (b0 === UTF16LE_BOM[0] && b1 === UTF16LE_BOM[1]) {
31
+ return { encoding: 'utf-16le', seemsBinary: false };
32
+ }
33
+ if (sample.length >= 3 && b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && sample[2] === UTF8_BOM[2]) {
34
+ return { encoding: 'utf-8', seemsBinary: false };
35
+ }
36
+ }
37
+
38
+ let zerosAtOdd = 0;
39
+ let zerosAtEven = 0;
40
+ const limit = Math.min(sample.length, ENCODING_DETECTION_SAMPLE_BYTES);
41
+ for (let i = 0; i < limit; i++) {
42
+ if (sample[i] !== 0) continue;
43
+ if (i % 2 === 1) zerosAtOdd++;
44
+ else zerosAtEven++;
45
+ }
46
+
47
+ if (zerosAtOdd === 0 && zerosAtEven === 0) {
48
+ return { encoding: 'utf-8', seemsBinary: false };
49
+ }
50
+ if (zerosAtEven === 0 && zerosAtOdd >= MIN_ZERO_BYTES_FOR_UTF16) {
51
+ return { encoding: 'utf-16le', seemsBinary: false };
52
+ }
53
+ if (zerosAtOdd === 0 && zerosAtEven >= MIN_ZERO_BYTES_FOR_UTF16) {
54
+ return { encoding: 'utf-16be', seemsBinary: false };
55
+ }
56
+ return { encoding: 'utf-8', seemsBinary: true };
57
+ }
58
+
59
+ export function classifyTextSample(sample: Uint8Array): TextClassification {
60
+ const sniffed = sniffTextEncoding(sample);
61
+ if (sniffed.seemsBinary || sniffed.encoding !== 'utf-8') {
62
+ return { isBinary: sniffed.seemsBinary, encoding: sniffed.encoding };
63
+ }
64
+ if (sample.includes(0)) {
65
+ return { isBinary: true, encoding: 'utf-8' };
66
+ }
67
+ let end = sample.length;
68
+ for (let i = Math.max(0, sample.length - 3); i < sample.length; i++) {
69
+ const b = sample[i]!;
70
+ const expected =
71
+ b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0;
72
+ if (expected === 0 || i + expected <= sample.length) continue;
73
+ let validPrefix = true;
74
+ for (let j = i + 1; j < sample.length; j++) {
75
+ const cb = sample[j]!;
76
+ if (cb < 0x80 || cb > 0xbf) {
77
+ validPrefix = false;
78
+ break;
79
+ }
80
+ }
81
+ if (validPrefix) {
82
+ end = i;
83
+ break;
84
+ }
85
+ }
86
+ let text: string;
87
+ try {
88
+ text = new TextDecoder('utf-8', { fatal: true }).decode(sample.subarray(0, end));
89
+ } catch {
90
+ return { isBinary: true, encoding: 'utf-8' };
91
+ }
92
+ let nonPrintable = 0;
93
+ let total = 0;
94
+ for (const ch of text) {
95
+ const cp = ch.codePointAt(0)!;
96
+ total++;
97
+ if (cp === 9 || cp === 10 || cp === 13) continue;
98
+ if (cp < 32 || (cp >= 0x7f && cp <= 0x9f)) nonPrintable++;
99
+ }
100
+ if (total > 0 && nonPrintable / total > FS_BINARY_NONPRINTABLE_FRACTION) {
101
+ return { isBinary: true, encoding: 'utf-8' };
102
+ }
103
+ return { isBinary: false, encoding: 'utf-8' };
104
+ }
105
+
106
+ export function detectTextEncoding(sample: Uint8Array): TextEncodingDetection {
107
+ const classification = classifyTextSample(sample);
108
+ return { encoding: classification.encoding, seemsBinary: classification.isBinary };
109
+ }
110
+
111
+ export function decodeUtfText(bytes: Uint8Array, encoding: UtfTextEncoding): string {
112
+ return new TextDecoder(encoding, { fatal: false }).decode(bytes);
113
+ }
packages/agent-core-v2/src/_base/text/frontmatter.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { load as loadYaml } from 'js-yaml';
2
+
3
+ export class FrontmatterError extends Error {
4
+ constructor(message: string, cause?: unknown) {
5
+ super(message);
6
+ this.name = 'FrontmatterError';
7
+ if (cause !== undefined) {
8
+ Object.defineProperty(this, 'cause', { value: cause, configurable: true });
9
+ }
10
+ }
11
+ }
12
+
13
+ export interface ParsedFrontmatter {
14
+ readonly data: unknown;
15
+ readonly body: string;
16
+ }
17
+
18
+ const FENCE = '---';
19
+
20
+ export function parseFrontmatter(text: string): ParsedFrontmatter {
21
+ const lines = text.split(/\r?\n/);
22
+ if (lines[0]?.trim() !== FENCE) {
23
+ return { data: null, body: text };
24
+ }
25
+
26
+ const close = lines.findIndex((line, index) => index > 0 && line.trim() === FENCE);
27
+ if (close === -1) {
28
+ throw new FrontmatterError('Missing closing frontmatter fence');
29
+ }
30
+
31
+ const yamlText = lines.slice(1, close).join('\n').trim();
32
+ const body = lines.slice(close + 1).join('\n');
33
+ if (yamlText === '') {
34
+ return { data: {}, body };
35
+ }
36
+
37
+ try {
38
+ return { data: loadYaml(yamlText) ?? {}, body };
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ throw new FrontmatterError(message, error);
42
+ }
43
+ }
packages/agent-core-v2/src/_base/text/line-endings.ts ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type LineEndingStyle = 'lf' | 'crlf' | 'mixed';
2
+
3
+ export interface ModelTextView {
4
+ text: string;
5
+ lineEndingStyle: LineEndingStyle;
6
+ }
7
+
8
+ export function detectLineEndingStyle(text: string): LineEndingStyle {
9
+ let hasCrLf = false;
10
+ let hasLf = false;
11
+ let hasLoneCr = false;
12
+
13
+ for (let i = 0; i < text.length; i++) {
14
+ const code = text.codePointAt(i);
15
+ if (code === 13) {
16
+ if (text.codePointAt(i + 1) === 10) {
17
+ hasCrLf = true;
18
+ i++;
19
+ } else {
20
+ hasLoneCr = true;
21
+ }
22
+ } else if (code === 10) {
23
+ hasLf = true;
24
+ }
25
+ }
26
+
27
+ if (hasLoneCr || (hasCrLf && hasLf)) return 'mixed';
28
+ if (hasCrLf) return 'crlf';
29
+ return 'lf';
30
+ }
31
+
32
+ export function toModelTextView(raw: string): ModelTextView {
33
+ const lineEndingStyle = detectLineEndingStyle(raw);
34
+ if (lineEndingStyle !== 'crlf') {
35
+ return { text: raw, lineEndingStyle };
36
+ }
37
+
38
+ return {
39
+ text: raw.replaceAll('\r\n', '\n'),
40
+ lineEndingStyle,
41
+ };
42
+ }
43
+
44
+ export function materializeModelText(text: string, lineEndingStyle: LineEndingStyle): string {
45
+ if (lineEndingStyle !== 'crlf') return text;
46
+ return text.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n');
47
+ }
48
+
49
+ export function makeCarriageReturnsVisible(text: string): string {
50
+ return text.replaceAll('\r', '\\r');
51
+ }
52
+
53
+ export function splitLinesKeepingTerminator(text: string): string[] {
54
+ if (text.length === 0) return [];
55
+ const lines: string[] = [];
56
+ let start = 0;
57
+ for (let i = 0; i < text.length; i += 1) {
58
+ if (text.codePointAt(i) === 0x0a) {
59
+ lines.push(text.slice(start, i + 1));
60
+ start = i + 1;
61
+ }
62
+ }
63
+ if (start < text.length) {
64
+ lines.push(text.slice(start));
65
+ }
66
+ return lines;
67
+ }
packages/agent-core-v2/src/_base/utils/abort.ts ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function abortError(message = 'Aborted'): Error {
2
+ const error = new Error(message);
3
+ error.name = 'AbortError';
4
+ return error;
5
+ }
6
+
7
+ export function isAbortError(error: unknown): error is Error {
8
+ return error instanceof Error && error.name === 'AbortError';
9
+ }
10
+
11
+ export class UserCancellationError extends Error {
12
+ readonly userCancelled = true;
13
+
14
+ constructor() {
15
+ super('Aborted by the user');
16
+ this.name = 'AbortError';
17
+ }
18
+ }
19
+
20
+ export function userCancellationReason(): UserCancellationError {
21
+ return new UserCancellationError();
22
+ }
23
+
24
+ export function isUserCancellation(value: unknown): value is UserCancellationError {
25
+ return value instanceof UserCancellationError;
26
+ }
27
+
28
+ export function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
29
+ if (signal.aborted) return Promise.reject(abortReason(signal));
30
+ return new Promise<T>((resolve, reject) => {
31
+ const onAbort = () => {
32
+ reject(abortReason(signal));
33
+ };
34
+ signal.addEventListener('abort', onAbort, { once: true });
35
+ promise.then(resolve, reject).finally(() => {
36
+ signal.removeEventListener('abort', onAbort);
37
+ });
38
+ });
39
+ }
40
+
41
+ export function linkAbortSignal(source: AbortSignal, target: AbortController): () => void {
42
+ const onAbort = () => {
43
+ target.abort(source.reason);
44
+ };
45
+ if (source.aborted) {
46
+ onAbort();
47
+ return () => {};
48
+ }
49
+ source.addEventListener('abort', onAbort, { once: true });
50
+ return () => {
51
+ source.removeEventListener('abort', onAbort);
52
+ };
53
+ }
54
+
55
+ function abortReason(signal: AbortSignal): Error {
56
+ if (signal.reason instanceof Error && !isDefaultAbortReason(signal.reason)) {
57
+ return signal.reason;
58
+ }
59
+ return abortError();
60
+ }
61
+
62
+ function isDefaultAbortReason(reason: Error): boolean {
63
+ return reason.name === 'AbortError' && reason.message === 'This operation was aborted';
64
+ }
65
+
66
+ export interface DeadlineAbortSignal {
67
+ readonly signal: AbortSignal;
68
+ readonly timedOut: () => boolean;
69
+ readonly clear: () => void;
70
+ }
71
+
72
+ export function createDeadlineAbortSignal(
73
+ source: AbortSignal,
74
+ timeoutMs: number,
75
+ ): DeadlineAbortSignal {
76
+ const controller = new AbortController();
77
+ const unlinkAbortSignal = linkAbortSignal(source, controller);
78
+ let didTimeout = false;
79
+ let timeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
80
+ didTimeout = true;
81
+ controller.abort(abortError());
82
+ }, timeoutMs);
83
+
84
+ return {
85
+ signal: controller.signal,
86
+ timedOut: () => didTimeout,
87
+ clear: () => {
88
+ if (timeout !== undefined) clearTimeout(timeout);
89
+ timeout = undefined;
90
+ unlinkAbortSignal();
91
+ },
92
+ };
93
+ }
packages/agent-core-v2/src/_base/utils/canonical-args.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function canonicalTelemetryArgs(args: unknown): string {
2
+ const json = JSON.stringify(sortJsonValue(args));
3
+ return json ?? String(args);
4
+ }
5
+
6
+ function sortJsonValue(value: unknown): unknown {
7
+ if (Array.isArray(value)) {
8
+ return value.map(sortJsonValue);
9
+ }
10
+ if (!isPlainRecord(value)) {
11
+ return value;
12
+ }
13
+ const out: Record<string, unknown> = {};
14
+ for (const key of Object.keys(value).toSorted()) {
15
+ out[key] = sortJsonValue(value[key]);
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export function isPlainRecord(value: unknown): value is Record<string, unknown> {
21
+ if (value === null || typeof value !== 'object') return false;
22
+ const proto = Object.getPrototypeOf(value);
23
+ return proto === Object.prototype || proto === null;
24
+ }
packages/agent-core-v2/src/_base/utils/env.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ const TRUE_BOOLEAN_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
2
+ const FALSE_BOOLEAN_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
3
+
4
+ export function parseBooleanEnv(value: string | undefined): boolean | undefined {
5
+ const normalized = value?.trim().toLowerCase();
6
+ if (normalized === undefined || normalized.length === 0) return undefined;
7
+ if (TRUE_BOOLEAN_ENV_VALUES.has(normalized)) return true;
8
+ if (FALSE_BOOLEAN_ENV_VALUES.has(normalized)) return false;
9
+ return undefined;
10
+ }
packages/agent-core-v2/src/_base/utils/fileMeta.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { extname } from 'node:path';
2
+
3
+ import { classifyTextSample } from '#/_base/text/encoding';
4
+
5
+ export { FS_BINARY_NONPRINTABLE_FRACTION } from '#/_base/text/encoding';
6
+
7
+ export const FS_BINARY_SAMPLE_BYTES = 4096;
8
+
9
+ export interface FileMetaStat {
10
+ readonly size: number;
11
+ readonly mtimeMs?: number;
12
+ readonly ino?: number;
13
+ }
14
+
15
+ export function detectBinary(buf: Uint8Array): boolean {
16
+ return classifyTextSample(buf).isBinary;
17
+ }
18
+
19
+ export function countLines(text: string): number {
20
+ if (text.length === 0) return 0;
21
+ let n = 1;
22
+ for (let i = 0; i < text.length; i++) {
23
+ if (text.charCodeAt(i) === 10) n++;
24
+ }
25
+ if (text.charCodeAt(text.length - 1) === 10) n--;
26
+ return Math.max(0, n);
27
+ }
28
+
29
+ export function buildEtag(st: FileMetaStat): string {
30
+ const mtime = Math.floor(st.mtimeMs ?? 0);
31
+ const ino = st.ino ?? 0;
32
+ return [mtime.toString(36), st.size.toString(36), ino.toString(36)].join('-');
33
+ }
34
+
35
+ const EXT_TO_MIME: Readonly<Record<string, string>> = {
36
+ '.ts': 'text/typescript',
37
+ '.tsx': 'text/typescript',
38
+ '.js': 'text/javascript',
39
+ '.jsx': 'text/javascript',
40
+ '.mjs': 'text/javascript',
41
+ '.cjs': 'text/javascript',
42
+ '.json': 'application/json',
43
+ '.md': 'text/markdown',
44
+ '.html': 'text/html',
45
+ '.css': 'text/css',
46
+ '.svg': 'image/svg+xml',
47
+ '.png': 'image/png',
48
+ '.jpg': 'image/jpeg',
49
+ '.jpeg': 'image/jpeg',
50
+ '.gif': 'image/gif',
51
+ '.pdf': 'application/pdf',
52
+ '.yaml': 'text/yaml',
53
+ '.yml': 'text/yaml',
54
+ '.toml': 'application/toml',
55
+ '.sh': 'text/x-shellscript',
56
+ '.py': 'text/x-python',
57
+ '.rs': 'text/rust',
58
+ '.go': 'text/x-go',
59
+ };
60
+
61
+ export function guessMime(path: string, isBinary: boolean): string {
62
+ const ext = extname(path).toLowerCase();
63
+ const mapped = EXT_TO_MIME[ext];
64
+ if (mapped !== undefined) return mapped;
65
+ return isBinary ? 'application/octet-stream' : 'text/plain';
66
+ }
67
+
68
+ const APPLICATION_TEXT_ALIASES: Readonly<Record<string, string>> = {
69
+ 'application/javascript': 'text/javascript',
70
+ 'application/x-javascript': 'text/javascript',
71
+ 'application/ecmascript': 'text/javascript',
72
+ 'application/yaml': 'text/yaml',
73
+ 'application/x-yaml': 'text/yaml',
74
+ 'application/sql': 'text/plain',
75
+ 'application/graphql': 'text/plain',
76
+ 'application/x-www-form-urlencoded': 'text/plain',
77
+ };
78
+
79
+ export function textExtensionForMime(mimeType: string): string | undefined {
80
+ const mime = mimeType.split(';')[0]!.trim().toLowerCase();
81
+ if (mime === 'application/json' || mime.endsWith('+json')) return '.json';
82
+ if (mime === 'application/xml' || mime.endsWith('+xml')) return '.xml';
83
+ if (mime.endsWith('+yaml')) return '.yaml';
84
+ if (mime === 'application/toml') return '.toml';
85
+ if (mime === 'text/csv') return '.csv';
86
+ const textMime = APPLICATION_TEXT_ALIASES[mime] ?? mime;
87
+ if (!textMime.startsWith('text/')) return undefined;
88
+ return Object.entries(EXT_TO_MIME).find(([, value]) => value === textMime)?.[0] ?? '.txt';
89
+ }
90
+
91
+ const EXT_TO_LANGUAGE: Readonly<Record<string, string>> = {
92
+ '.ts': 'typescript',
93
+ '.tsx': 'typescriptreact',
94
+ '.js': 'javascript',
95
+ '.jsx': 'javascriptreact',
96
+ '.mjs': 'javascript',
97
+ '.cjs': 'javascript',
98
+ '.json': 'json',
99
+ '.md': 'markdown',
100
+ '.html': 'html',
101
+ '.css': 'css',
102
+ '.yaml': 'yaml',
103
+ '.yml': 'yaml',
104
+ '.toml': 'toml',
105
+ '.sh': 'shellscript',
106
+ '.py': 'python',
107
+ '.rs': 'rust',
108
+ '.go': 'go',
109
+ };
110
+
111
+ export function guessLanguageId(path: string): string | undefined {
112
+ return EXT_TO_LANGUAGE[extname(path).toLowerCase()];
113
+ }
packages/agent-core-v2/src/_base/utils/fs.ts ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomBytes } from 'node:crypto';
2
+ import { closeSync, fsyncSync, openSync } from 'node:fs';
3
+ import * as nodeFs from 'node:fs';
4
+ import { open, rename, unlink } from 'node:fs/promises';
5
+ import { dirname } from 'pathe';
6
+
7
+ export async function syncDir(dirPath: string): Promise<void> {
8
+ if (process.platform === 'win32') return;
9
+ const dirFh = await open(dirPath, 'r');
10
+ try {
11
+ await dirFh.sync();
12
+ } finally {
13
+ await dirFh.close();
14
+ }
15
+ }
16
+
17
+ export function syncDirSync(dirPath: string): void {
18
+ if (process.platform === 'win32') return;
19
+ const fd = openSync(dirPath, 'r');
20
+ try {
21
+ fsyncSync(fd);
22
+ } finally {
23
+ closeSync(fd);
24
+ }
25
+ }
26
+
27
+ export async function writeFileAtomicDurable(
28
+ filePath: string,
29
+ content: string | Uint8Array,
30
+ ): Promise<void> {
31
+ const tmpPath = filePath + '.tmp';
32
+ let renamed = false;
33
+ try {
34
+ const fh = await open(tmpPath, 'w');
35
+ try {
36
+ await fh.writeFile(content);
37
+ await fh.sync();
38
+ } finally {
39
+ await fh.close();
40
+ }
41
+ if (process.platform === 'win32') {
42
+ try {
43
+ await unlink(filePath);
44
+ } catch (error) {
45
+ const code = (error as NodeJS.ErrnoException).code;
46
+ if (code !== 'ENOENT') throw error;
47
+ }
48
+ }
49
+ await rename(tmpPath, filePath);
50
+ renamed = true;
51
+ await syncDir(dirname(filePath));
52
+ } finally {
53
+ if (!renamed) {
54
+ try {
55
+ await unlink(tmpPath);
56
+ } catch {
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ function syncFd(fd: number): Promise<void> {
63
+ return new Promise<void>((resolve, reject) => {
64
+ nodeFs.fsync(fd, (err) => {
65
+ if (err) {
66
+ reject(err);
67
+ return;
68
+ }
69
+ resolve();
70
+ });
71
+ });
72
+ }
73
+
74
+ export async function atomicWrite(
75
+ filePath: string,
76
+ content: string | Uint8Array,
77
+ _syncOverride?: (fd: number) => Promise<void>,
78
+ mode?: number,
79
+ signal?: AbortSignal,
80
+ ): Promise<void> {
81
+ signal?.throwIfAborted();
82
+ const hex = randomBytes(4).toString('hex');
83
+ const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`;
84
+ let renamed = false;
85
+ try {
86
+ const fh = await open(tmpPath, 'w', mode);
87
+ try {
88
+ signal?.throwIfAborted();
89
+ await fh.writeFile(content);
90
+ signal?.throwIfAborted();
91
+ await (_syncOverride ?? syncFd)(fh.fd);
92
+ } finally {
93
+ await fh.close();
94
+ }
95
+ if (process.platform === 'win32') {
96
+ try {
97
+ await unlink(filePath);
98
+ } catch (error) {
99
+ const code = (error as NodeJS.ErrnoException).code;
100
+ if (code !== 'ENOENT') throw error;
101
+ }
102
+ }
103
+ signal?.throwIfAborted();
104
+ await rename(tmpPath, filePath);
105
+ renamed = true;
106
+ } finally {
107
+ if (!renamed) {
108
+ try {
109
+ await unlink(tmpPath);
110
+ } catch {
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ export async function atomicWriteStream(
117
+ filePath: string,
118
+ source: AsyncIterable<Uint8Array>,
119
+ mode?: number,
120
+ signal?: AbortSignal,
121
+ ): Promise<void> {
122
+ signal?.throwIfAborted();
123
+ const hex = randomBytes(4).toString('hex');
124
+ const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`;
125
+ let renamed = false;
126
+ const destroyable = source as AsyncIterable<Uint8Array> & {
127
+ destroy?(error?: Error): void;
128
+ };
129
+ const onAbort = (): void => {
130
+ const reason = signal?.reason instanceof Error ? signal.reason : undefined;
131
+ destroyable.destroy?.(reason);
132
+ };
133
+ signal?.addEventListener('abort', onAbort, { once: true });
134
+ try {
135
+ const fh = await open(tmpPath, 'w', mode);
136
+ try {
137
+ for await (const chunk of source) {
138
+ signal?.throwIfAborted();
139
+ if (chunk.byteLength > 0) {
140
+ await fh.writeFile(chunk);
141
+ }
142
+ }
143
+ signal?.throwIfAborted();
144
+ await fh.sync();
145
+ } finally {
146
+ await fh.close();
147
+ }
148
+ if (process.platform === 'win32') {
149
+ try {
150
+ await unlink(filePath);
151
+ } catch (error) {
152
+ const code = (error as NodeJS.ErrnoException).code;
153
+ if (code !== 'ENOENT') throw error;
154
+ }
155
+ }
156
+ signal?.throwIfAborted();
157
+ await rename(tmpPath, filePath);
158
+ renamed = true;
159
+ } finally {
160
+ signal?.removeEventListener('abort', onAbort);
161
+ if (!renamed) {
162
+ try {
163
+ await unlink(tmpPath);
164
+ } catch {
165
+ }
166
+ }
167
+ }
168
+ }
packages/agent-core-v2/src/_base/utils/hero-slug.ts ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomInt } from 'node:crypto';
2
+
3
+ export const HERO_NAMES = [
4
+ 'iron-man',
5
+ 'spider-man',
6
+ 'captain-america',
7
+ 'thor',
8
+ 'hulk',
9
+ 'black-widow',
10
+ 'hawkeye',
11
+ 'black-panther',
12
+ 'doctor-strange',
13
+ 'scarlet-witch',
14
+ 'vision',
15
+ 'falcon',
16
+ 'war-machine',
17
+ 'ant-man',
18
+ 'wasp',
19
+ 'captain-marvel',
20
+ 'gamora',
21
+ 'star-lord',
22
+ 'groot',
23
+ 'rocket',
24
+ 'drax',
25
+ 'mantis',
26
+ 'nebula',
27
+ 'shang-chi',
28
+ 'moon-knight',
29
+ 'ms-marvel',
30
+ 'she-hulk',
31
+ 'echo',
32
+ 'wolverine',
33
+ 'cyclops',
34
+ 'storm',
35
+ 'jean-grey',
36
+ 'rogue',
37
+ 'beast',
38
+ 'nightcrawler',
39
+ 'colossus',
40
+ 'shadowcat',
41
+ 'jubilee',
42
+ 'cable',
43
+ 'deadpool',
44
+ 'bishop',
45
+ 'magik',
46
+ 'iceman',
47
+ 'archangel',
48
+ 'psylocke',
49
+ 'dazzler',
50
+ 'forge',
51
+ 'havok',
52
+ 'polaris',
53
+ 'emma-frost',
54
+ 'namor',
55
+ 'silver-surfer',
56
+ 'adam-warlock',
57
+ 'nova',
58
+ 'quasar',
59
+ 'sentry',
60
+ 'blue-marvel',
61
+ 'spectrum',
62
+ 'squirrel-girl',
63
+ 'cloak',
64
+ 'dagger',
65
+ 'punisher',
66
+ 'elektra',
67
+ 'luke-cage',
68
+ 'iron-fist',
69
+ 'jessica-jones',
70
+ 'daredevil',
71
+ 'blade',
72
+ 'ghost-rider',
73
+ 'morbius',
74
+ 'venom',
75
+ 'carnage',
76
+ 'silk',
77
+ 'spider-gwen',
78
+ 'miles-morales',
79
+ 'america-chavez',
80
+ 'kate-bishop',
81
+ 'yelena-belova',
82
+ 'white-tiger',
83
+ 'moon-girl',
84
+ 'devil-dinosaur',
85
+ 'amadeus-cho',
86
+ 'riri-williams',
87
+ 'kamala-khan',
88
+ 'sam-alexander',
89
+ 'nova-prime',
90
+ 'medusa',
91
+ 'black-bolt',
92
+ 'crystal',
93
+ 'karnak',
94
+ 'gorgon',
95
+ 'lockjaw',
96
+ 'quake',
97
+ 'mockingbird',
98
+ 'bobbi-morse',
99
+ 'maria-hill',
100
+ 'nick-fury',
101
+ 'phil-coulson',
102
+ 'winter-soldier',
103
+ 'us-agent',
104
+ 'patriot',
105
+ 'speed',
106
+ 'wiccan',
107
+ 'hulkling',
108
+ 'stature',
109
+ 'yellowjacket',
110
+ 'tigra',
111
+ 'hellcat',
112
+ 'valkyrie',
113
+ 'sif',
114
+ 'beta-ray-bill',
115
+ 'hercules',
116
+ 'wonder-man',
117
+ 'taskmaster',
118
+ 'domino',
119
+ 'cannonball',
120
+ 'sunspot',
121
+ 'wolfsbane',
122
+ 'warpath',
123
+ 'multiple-man',
124
+ 'banshee',
125
+ 'siryn',
126
+ 'monet',
127
+ 'rictor',
128
+ 'shatterstar',
129
+ 'longshot',
130
+ 'daken',
131
+ 'x-23',
132
+ 'fantomex',
133
+ 'batman',
134
+ 'superman',
135
+ 'wonder-woman',
136
+ 'flash',
137
+ 'aquaman',
138
+ 'green-lantern',
139
+ 'martian-manhunter',
140
+ 'cyborg',
141
+ 'hawkgirl',
142
+ 'green-arrow',
143
+ 'black-canary',
144
+ 'zatanna',
145
+ 'constantine',
146
+ 'shazam',
147
+ 'blue-beetle',
148
+ 'booster-gold',
149
+ 'firestorm',
150
+ 'atom',
151
+ 'hawkman',
152
+ 'plastic-man',
153
+ 'red-tornado',
154
+ 'starfire',
155
+ 'raven',
156
+ 'beast-boy',
157
+ 'robin',
158
+ 'nightwing',
159
+ 'batgirl',
160
+ 'batwoman',
161
+ 'red-hood',
162
+ 'signal',
163
+ 'orphan',
164
+ 'spoiler',
165
+ 'catwoman',
166
+ 'huntress',
167
+ 'supergirl',
168
+ 'superboy',
169
+ 'power-girl',
170
+ 'steel',
171
+ 'stargirl',
172
+ 'wildcat',
173
+ 'doctor-fate',
174
+ 'mister-terrific',
175
+ 'hourman',
176
+ 'sandman',
177
+ 'spectre',
178
+ 'phantom-stranger',
179
+ 'swamp-thing',
180
+ 'animal-man',
181
+ 'deadman',
182
+ 'vixen',
183
+ 'black-lightning',
184
+ 'static',
185
+ 'icon',
186
+ 'rocket-dc',
187
+ 'captain-atom',
188
+ 'fire',
189
+ 'ice',
190
+ 'elongated-man',
191
+ 'metamorpho',
192
+ 'black-hawk',
193
+ 'crimson-avenger',
194
+ 'doctor-mid-nite',
195
+ 'jakeem-thunder',
196
+ 'mister-miracle',
197
+ 'big-barda',
198
+ 'orion',
199
+ 'lightray',
200
+ 'forager',
201
+ 'killer-frost',
202
+ 'jessica-cruz',
203
+ 'simon-baz',
204
+ 'john-stewart',
205
+ 'guy-gardner',
206
+ 'kyle-rayner',
207
+ 'hal-jordan',
208
+ 'wally-west',
209
+ 'barry-allen',
210
+ 'jay-garrick',
211
+ 'impulse',
212
+ 'kid-flash',
213
+ 'donna-troy',
214
+ 'tempest',
215
+ 'aqualad',
216
+ 'miss-martian',
217
+ 'terra',
218
+ 'jericho',
219
+ 'ravager',
220
+ 'red-star',
221
+ 'pantha',
222
+ 'argent',
223
+ 'damage',
224
+ 'jade',
225
+ 'obsidian',
226
+ 'cyclone',
227
+ 'atom-smasher',
228
+ 'maxima',
229
+ 'starman',
230
+ 'liberty-belle',
231
+ 'dove',
232
+ 'hawk',
233
+ 'blue-devil',
234
+ 'creeper',
235
+ 'ragman',
236
+ 'thunder',
237
+ ] as const satisfies readonly [string, ...string[]];
238
+
239
+ const MAX_ATTEMPTS = 20;
240
+
241
+ function pickHero(): string {
242
+ return HERO_NAMES[randomInt(HERO_NAMES.length)]!;
243
+ }
244
+
245
+ function assembleSlug(): string {
246
+ return `${pickHero()}-${pickHero()}-${pickHero()}`;
247
+ }
248
+
249
+ export function generateHeroSlug(id: string, existing: Set<string>): string {
250
+ let slug = '';
251
+ let collided = true;
252
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
253
+ slug = assembleSlug();
254
+ if (!existing.has(slug)) {
255
+ collided = false;
256
+ break;
257
+ }
258
+ }
259
+ if (collided) {
260
+ slug = `${slug}-${id.slice(0, 8)}`;
261
+ }
262
+ return slug;
263
+ }