@telorun/kernel 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +100 -0
  2. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  3. package/dist/controllers/module/import-controller.js +18 -9
  4. package/dist/controllers/module/import-controller.js.map +1 -1
  5. package/dist/evaluation-context.d.ts +76 -38
  6. package/dist/evaluation-context.d.ts.map +1 -1
  7. package/dist/evaluation-context.js +254 -89
  8. package/dist/evaluation-context.js.map +1 -1
  9. package/dist/execution-context.d.ts +1 -1
  10. package/dist/execution-context.d.ts.map +1 -1
  11. package/dist/execution-context.js +1 -1
  12. package/dist/execution-context.js.map +1 -1
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/kernel.d.ts +7 -1
  18. package/dist/kernel.d.ts.map +1 -1
  19. package/dist/kernel.js +54 -11
  20. package/dist/kernel.js.map +1 -1
  21. package/dist/manifest-adapters/local-file-adapter.d.ts +1 -1
  22. package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
  23. package/dist/manifest-adapters/local-file-adapter.js +2 -1
  24. package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
  25. package/dist/manifest-adapters/manifest-adapter.d.ts +1 -1
  26. package/dist/module-context.d.ts +28 -5
  27. package/dist/module-context.d.ts.map +1 -1
  28. package/dist/module-context.js +107 -4
  29. package/dist/module-context.js.map +1 -1
  30. package/dist/resource-context.d.ts +6 -6
  31. package/dist/resource-context.d.ts.map +1 -1
  32. package/dist/resource-context.js +5 -4
  33. package/dist/resource-context.js.map +1 -1
  34. package/dist/schema-valiator.d.ts +1 -0
  35. package/dist/schema-valiator.d.ts.map +1 -1
  36. package/dist/schema-valiator.js +11 -1
  37. package/dist/schema-valiator.js.map +1 -1
  38. package/dist/schema-validator.d.ts +15 -0
  39. package/dist/schema-validator.d.ts.map +1 -0
  40. package/dist/schema-validator.js +127 -0
  41. package/dist/schema-validator.js.map +1 -0
  42. package/package.json +21 -10
  43. package/src/controllers/module/import-controller.ts +27 -11
  44. package/src/evaluation-context.ts +490 -0
  45. package/src/execution-context.ts +21 -0
  46. package/src/index.ts +3 -0
  47. package/src/kernel.ts +70 -13
  48. package/src/manifest-adapters/local-file-adapter.ts +2 -2
  49. package/src/manifest-adapters/manifest-adapter.ts +1 -1
  50. package/src/module-context.ts +211 -0
  51. package/src/resource-context.ts +8 -7
  52. package/src/{schema-valiator.ts → schema-validator.ts} +13 -1
@@ -0,0 +1,490 @@
1
+ import {
2
+ isCompiledValue,
3
+ resourceKey,
4
+ type EvaluationContext as IEvaluationContext,
5
+ type EmitEvent,
6
+ type InstanceFactory,
7
+ type LifecycleState,
8
+ type PreInitHook,
9
+ type ResourceInstance,
10
+ type ResourceManifest,
11
+ type RuntimeDiagnostic,
12
+ type ScopeContext,
13
+ type ScopeHandle,
14
+ } from "@telorun/sdk";
15
+ import { RuntimeError } from "@telorun/sdk";
16
+
17
+ export { resourceKey };
18
+
19
+ /**
20
+ * Base class for all evaluation contexts. Owns template
21
+ * expansion, secrets redaction, and the generic resource lifecycle tree.
22
+ *
23
+ * Every EvaluationContext node can:
24
+ * - Hold its own resource instances (resourceInstances)
25
+ * - Queue resources for initialization (pendingResources)
26
+ * - Spawn child contexts (spawnChild) forming a lifecycle tree
27
+ * - Run a multi-pass initialization loop (initializeResources)
28
+ * - Cascade teardown depth-first through the tree (teardownResources)
29
+ */
30
+ export class EvaluationContext implements IEvaluationContext {
31
+ readonly id = Math.random().toString(16).slice(2, 8);
32
+ protected _context: Record<string, unknown>;
33
+ protected _secretValues: Set<string>;
34
+ protected _createInstance: InstanceFactory;
35
+ readonly emit: EmitEvent;
36
+
37
+ /** Position in the lifecycle tree. */
38
+ parent: IEvaluationContext | undefined = undefined;
39
+ readonly children: IEvaluationContext[] = [];
40
+
41
+ /** Current lifecycle state of this context node. */
42
+ state: LifecycleState = "Pending";
43
+
44
+ /** Resource instances owned by this context node, keyed by resourceKey(). */
45
+ readonly resourceInstances = new Map<
46
+ string,
47
+ { resource: ResourceManifest; instance: ResourceInstance }
48
+ >();
49
+
50
+ /** Resources that have been created but not yet initialized (between phases). */
51
+ protected readonly createdInstances = new Map<
52
+ string,
53
+ { resource: ResourceManifest; instance: ResourceInstance; ctx: any }
54
+ >();
55
+
56
+ /** Resources queued for initialization on this context node. */
57
+ private pendingResources: ResourceManifest[] = [];
58
+
59
+ /**
60
+ * Optional hook called between create() and init() for each resource.
61
+ * Set by the kernel to inject live instances into reference fields.
62
+ */
63
+ preInitHook?: PreInitHook;
64
+
65
+ constructor(
66
+ readonly source: string,
67
+ context: Record<string, unknown>,
68
+ createInstance: InstanceFactory = async () => null,
69
+ secretValues: Set<string>,
70
+ emit: EmitEvent,
71
+ ) {
72
+ this._context = context;
73
+ this._createInstance = createInstance;
74
+ this._secretValues = secretValues ?? new Set();
75
+ this.emit = emit;
76
+ }
77
+
78
+ get createInstance(): InstanceFactory {
79
+ return this._createInstance;
80
+ }
81
+
82
+ /** Called after init() when a resource snapshot is available. Overridden by ModuleContext. */
83
+ protected onResourceSnapshotted(_name: string, _snap: Record<string, unknown>): void {}
84
+
85
+ get context(): Record<string, unknown> {
86
+ return this._context;
87
+ }
88
+
89
+ get secretValues(): Set<string> {
90
+ return this._secretValues;
91
+ }
92
+
93
+ /**
94
+ * Reorder pending resources to match the given name sequence (topo order from Phase 4).
95
+ * Resources not present in `names` are left at the end in their original order.
96
+ * Call before initializeResources() so the create/init sub-phases run in dependency order,
97
+ * guaranteeing that Phase 5 injection always finds initialized dependencies.
98
+ */
99
+ setInitOrder(names: string[]): void {
100
+ const rank = new Map(names.map((n, i) => [n, i]));
101
+ this.pendingResources.sort((a, b) => {
102
+ const ra = rank.get(a.metadata.name as string) ?? Infinity;
103
+ const rb = rank.get(b.metadata.name as string) ?? Infinity;
104
+ return ra - rb;
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Queue a resource manifest for initialization on this context.
110
+ */
111
+ hasManifest(name: string): boolean {
112
+ return (
113
+ this.resourceInstances.has(name) ||
114
+ this.createdInstances.has(name) ||
115
+ this.pendingResources.some((r) => r.metadata.name === name)
116
+ );
117
+ }
118
+
119
+ registerManifest(resource: ResourceManifest): void {
120
+ if (!resource.metadata) {
121
+ resource.metadata = { name: `__unnamed_${Math.random().toString(16).slice(2, 8)}` };
122
+ }
123
+ const name = resource.metadata.name;
124
+ if (this.hasManifest(name)) {
125
+ throw new RuntimeError("ERR_DUPLICATE_RESOURCE", `Resource '${name}' is already registered`);
126
+ }
127
+ this.pendingResources.push(resource);
128
+ }
129
+
130
+ /**
131
+ * Attach a child context to this node. The child's parent is set to this
132
+ * context and the child is registered under the given name.
133
+ */
134
+ spawnChild<T extends IEvaluationContext>(child: T): T {
135
+ child.parent = this;
136
+ this.children.push(child);
137
+ // Propagate injection hook so all child contexts (module imports, scopes) participate
138
+ // in Phase 5 injection. createScopeHandle overrides this with an extended version.
139
+ if (this.preInitHook && !child.preInitHook) {
140
+ child.preInitHook = this.preInitHook;
141
+ }
142
+ return child;
143
+ }
144
+
145
+ /**
146
+ * Interleaved create/init loop.
147
+ *
148
+ * Each pass has two sub-phases run back-to-back:
149
+ * 1. Create sub-phase: call controller.create() for each pending resource that
150
+ * hasn't been created yet. Successful results go into createdInstances.
151
+ * 2. Init sub-phase: call instance.init(ctx) for each created-but-not-inited
152
+ * resource. Successful results go into resourceInstances.
153
+ *
154
+ * Interleaving is necessary because some resources' create() depends on effects
155
+ * produced by other resources' init() (e.g. Kernel.Import.init() runs
156
+ * child.initializeResources() which registers controllers needed by sibling
157
+ * resources' create()). Running both sub-phases each pass lets those effects
158
+ * propagate before the next create attempt.
159
+ *
160
+ * Each resource is created at most once and inited at most once.
161
+ * ERR_VISIBILITY_DENIED is fatal and re-thrown immediately.
162
+ * All other errors are tracked and retried until no progress is made.
163
+ */
164
+ async initializeResources(): Promise<void> {
165
+ const MAX_PASSES = 10;
166
+ const errors = new Map<string, string>();
167
+
168
+ let pass = 1;
169
+ do {
170
+ let progress = false;
171
+
172
+ // Create sub-phase
173
+ for (const resource of [...this.pendingResources]) {
174
+ const name = resource.metadata.name;
175
+ if (this.createdInstances.has(name)) continue;
176
+ try {
177
+ // const expanded = this.expand(resource) as ResourceManifest;
178
+ // FIXME: Cannot expand it for all resources, needs to be selective
179
+ const created = await this._createInstance(this, resource);
180
+ if (created) {
181
+ this.createdInstances.set(name, {
182
+ resource,
183
+ instance: created.instance,
184
+ ctx: created.ctx,
185
+ });
186
+ const idx = this.pendingResources.findIndex((m) => m.metadata.name === name);
187
+ if (idx >= 0) this.pendingResources.splice(idx, 1);
188
+ errors.delete(name);
189
+ progress = true;
190
+ }
191
+ } catch (error) {
192
+ if (error instanceof RuntimeError && error.code === "ERR_VISIBILITY_DENIED") throw error;
193
+ errors.set(name, error instanceof Error ? error.message : String(error));
194
+ }
195
+ }
196
+
197
+ // Init sub-phase
198
+ for (const [name, { resource, instance, ctx }] of [...this.createdInstances]) {
199
+ if (this.resourceInstances.has(name)) continue;
200
+ try {
201
+ if (this.preInitHook) {
202
+ this.preInitHook(resource, (n) => this.resourceInstances.get(n)?.instance);
203
+ }
204
+ if (instance.init) await instance.init(ctx);
205
+ if (instance.snapshot) {
206
+ const snap = await Promise.resolve(instance.snapshot()).catch(() => ({}));
207
+ this.onResourceSnapshotted(name, (snap as Record<string, unknown>) ?? {});
208
+ }
209
+ this.resourceInstances.set(name, { resource, instance });
210
+ this.createdInstances.delete(name);
211
+ errors.delete(name);
212
+ progress = true;
213
+ } catch (error) {
214
+ if (error instanceof RuntimeError && error.code === "ERR_VISIBILITY_DENIED") throw error;
215
+ errors.set(name, error instanceof Error ? error.message : String(error));
216
+ }
217
+ }
218
+
219
+ pass++;
220
+ if (!progress) break;
221
+ } while (pass <= MAX_PASSES);
222
+
223
+ if (this.pendingResources.length > 0 || this.createdInstances.size > 0) {
224
+ const diagnostics: RuntimeDiagnostic[] = [
225
+ ...this.pendingResources.map((r) => ({
226
+ resource: r.metadata.name,
227
+ message: errors.get(r.metadata.name) ?? "Unknown error",
228
+ })),
229
+ ...[...this.createdInstances.keys()].map((name) => ({
230
+ resource: name,
231
+ message: errors.get(name) ?? "Unknown error",
232
+ })),
233
+ ];
234
+ const details = diagnostics
235
+ .map((d) => ` ${d.resource}: ${d.message}`)
236
+ .join("\n");
237
+ throw new RuntimeError(
238
+ "ERR_RESOURCE_INITIALIZATION_FAILED",
239
+ `Unable to process resources:\n${details}`,
240
+ diagnostics,
241
+ );
242
+ }
243
+
244
+ this.state = "Initialized";
245
+ }
246
+
247
+ withManifests<T>(manifests: any[], fn: () => T): T {
248
+ const child = this.spawnChild(
249
+ new EvaluationContext(
250
+ this.source,
251
+ this._context,
252
+ this._createInstance,
253
+ this._secretValues,
254
+ this.emit,
255
+ ),
256
+ );
257
+ try {
258
+ for (const manifest of manifests) {
259
+ child.registerManifest(manifest);
260
+ }
261
+ return fn();
262
+ } finally {
263
+ // Tear down child context and its resources immediately after fn() completes.
264
+ // Note that this does NOT emit Kernel-level events (e.g. Teardown events) —
265
+ // they remain the Kernel's responsibility.
266
+ child.teardownResources();
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Returns a ScopeHandle that initializes `manifests` in a fresh child context each time
272
+ * `run()` is called, executes the callback with a ScopeContext, and tears down when done.
273
+ *
274
+ * The child inherits the parent's preInitHook (if any), extended so that `getInstance`
275
+ * also checks the parent's already-initialized singleton instances. This lets scoped
276
+ * resources hold x-telo-ref slots pointing to outer resources — those deps are already
277
+ * live when the scope opens.
278
+ */
279
+ createScopeHandle(manifests: ResourceManifest[]): ScopeHandle {
280
+ const parent = this;
281
+ return {
282
+ async run<T>(fn: (scope: ScopeContext) => Promise<T>): Promise<T> {
283
+ const child = parent.spawnChild(
284
+ new EvaluationContext(
285
+ parent.source,
286
+ parent._context,
287
+ parent._createInstance,
288
+ parent._secretValues,
289
+ parent.emit,
290
+ ),
291
+ );
292
+
293
+ // Propagate injection hook: extend getInstance to also resolve parent singleton instances.
294
+ if (parent.preInitHook) {
295
+ const parentHook = parent.preInitHook;
296
+ child.preInitHook = (resource, childGetInstance) => {
297
+ parentHook(
298
+ resource,
299
+ (name) => childGetInstance(name) ?? parent.resourceInstances.get(name)?.instance,
300
+ );
301
+ };
302
+ }
303
+
304
+ try {
305
+ for (const manifest of manifests) {
306
+ child.registerManifest(manifest);
307
+ }
308
+ await child.initializeResources();
309
+ const scope: ScopeContext = {
310
+ getInstance(name: string): ResourceInstance {
311
+ const childEntry = child.resourceInstances.get(name);
312
+ if (childEntry) return childEntry.instance;
313
+ const parentEntry = parent.resourceInstances.get(name);
314
+ if (parentEntry) return parentEntry.instance;
315
+ throw new RuntimeError(
316
+ "ERR_SCOPE_RESOURCE_NOT_FOUND",
317
+ `Resource '${name}' not found in scope or outer context. Available scoped: ${[...child.resourceInstances.keys()].join(", ")}`,
318
+ );
319
+ },
320
+ };
321
+ return await fn(scope);
322
+ } finally {
323
+ await child.teardownResources();
324
+ const idx = parent.children.indexOf(child);
325
+ if (idx >= 0) parent.children.splice(idx, 1);
326
+ }
327
+ },
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Cascade teardown depth-first through the tree:
333
+ * 1. Tear down child contexts in reverse registration order.
334
+ * 2. Tear down own resource instances in reverse registration order,
335
+ * emitting a Teardown event for each via the injected emit callback.
336
+ */
337
+ async teardownResources(): Promise<void> {
338
+ this.state = "Draining";
339
+ for (const child of [...this.children].reverse()) {
340
+ await child.teardownResources();
341
+ }
342
+ const entries = [...this.resourceInstances.entries()].reverse();
343
+ for (const [key, { resource, instance }] of entries) {
344
+ if (instance.teardown) await instance.teardown();
345
+ await this.emit(`${resource.kind}.${resource.metadata.name}.Teardown`, {
346
+ resource: { kind: resource.kind, name: resource.metadata.name },
347
+ });
348
+ this.resourceInstances.delete(key);
349
+ }
350
+ this.state = "Teardown";
351
+ }
352
+
353
+ transientChild(context: Record<string, any>): EvaluationContext {
354
+ return new EvaluationContext(
355
+ this.source,
356
+ { ...this.context, ...context },
357
+ this._createInstance,
358
+ this._secretValues,
359
+ this.emit,
360
+ );
361
+ }
362
+
363
+ /**
364
+ * Invoke a resource by kind and name within this context's resourceInstances.
365
+ * Emits a scoped Invoked event via the injected emit callback after invocation.
366
+ */
367
+ async invoke<TInputs>(kind: string, name: string, inputs: TInputs): Promise<any> {
368
+ const entry = this.resourceInstances.get(name);
369
+
370
+ if (entry) {
371
+ if (typeof entry.instance.invoke !== "function") {
372
+ throw new RuntimeError(
373
+ "ERR_RESOURCE_NOT_INVOKABLE",
374
+ `Resource ${kind}.${name} does not have an invoke method`,
375
+ );
376
+ }
377
+ const outputs = await entry.instance.invoke(inputs as any);
378
+ await this.emit(`${kind}.${name}.Invoked`, { outputs });
379
+ return outputs;
380
+ }
381
+
382
+ throw new RuntimeError(
383
+ "ERR_RESOURCE_NOT_FOUND",
384
+ `Resource not found for invocation: ${kind}.${name}. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`,
385
+ );
386
+ }
387
+
388
+ async run(name: string): Promise<void> {
389
+ const entry = this.resourceInstances.get(name);
390
+ if (entry && typeof entry.instance.run === "function") {
391
+ return entry.instance.run();
392
+ }
393
+ throw new RuntimeError(
394
+ "ERR_RESOURCE_NOT_RUNNABLE",
395
+ `Resource ${name} is not runnable or not found. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`,
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Expand a value that may contain precompiled ${{ }} templates.
401
+ * Works recursively over CompiledValues, arrays, and objects.
402
+ */
403
+ expand(value: unknown): unknown {
404
+ if (isCompiledValue(value)) {
405
+ return value.call(this._context);
406
+ }
407
+ if (Array.isArray(value)) {
408
+ return value.map((entry) => this.expand(entry));
409
+ }
410
+ if (value !== null && typeof value === "object") {
411
+ const resolved: Record<string, unknown> = {};
412
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
413
+ resolved[key] = this.expand(entry);
414
+ }
415
+ return resolved;
416
+ }
417
+ return value;
418
+ }
419
+
420
+ /**
421
+ * Expand a value using this context merged with additional properties.
422
+ * Equivalent to merge(extraContext).expand(value) without allocating a context object.
423
+ */
424
+ expandWith(value: unknown, extraContext: Record<string, unknown>): unknown {
425
+ const saved = this._context;
426
+ this._context = Object.assign(Object.create(null), saved, extraContext) as Record<
427
+ string,
428
+ unknown
429
+ >;
430
+ try {
431
+ return this.expand(value);
432
+ } finally {
433
+ this._context = saved;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Expand specific dot-paths within an object. '**' expands the entire object.
439
+ * Paths listed in excludePaths are left untouched (runtime takes precedence).
440
+ * Always throws if an expression cannot be resolved.
441
+ */
442
+ expandPaths(
443
+ value: Record<string, unknown>,
444
+ paths: string[],
445
+ excludePaths: string[] = [],
446
+ ): Record<string, unknown> {
447
+ if (paths.includes("**")) {
448
+ const result: Record<string, unknown> = {};
449
+ for (const [key, v] of Object.entries(value)) {
450
+ result[key] = isExcluded(key, excludePaths) ? v : this.expand(v);
451
+ }
452
+ return result;
453
+ }
454
+ const result = { ...value };
455
+ for (const path of paths) {
456
+ if (isExcluded(path, excludePaths)) continue;
457
+ const parts = path.split(".");
458
+ const current = getNestedValue(result, parts);
459
+ if (current !== undefined) {
460
+ setNestedValue(result, parts, this.expand(current));
461
+ }
462
+ }
463
+ return result;
464
+ }
465
+ }
466
+
467
+ function isExcluded(path: string, excludePaths: string[]): boolean {
468
+ return excludePaths.some(
469
+ (ep) => ep === path || ep === "**" || path.startsWith(ep + ".") || ep.startsWith(path + "."),
470
+ );
471
+ }
472
+
473
+ function getNestedValue(obj: Record<string, unknown>, parts: string[]): unknown {
474
+ let current: unknown = obj;
475
+ for (const part of parts) {
476
+ if (current === null || typeof current !== "object") return undefined;
477
+ current = (current as Record<string, unknown>)[part];
478
+ }
479
+ return current;
480
+ }
481
+
482
+ function setNestedValue(obj: Record<string, unknown>, parts: string[], value: unknown): void {
483
+ let current: Record<string, unknown> = obj;
484
+ for (let i = 0; i < parts.length - 1; i++) {
485
+ const next = current[parts[i]];
486
+ if (next === null || typeof next !== "object") return;
487
+ current = next as Record<string, unknown>;
488
+ }
489
+ current[parts[parts.length - 1]] = value;
490
+ }
@@ -0,0 +1,21 @@
1
+ import { EvaluationContext } from "./evaluation-context.js";
2
+ import type { ModuleContext } from "./module-context.js";
3
+
4
+ /**
5
+ * The ephemeral, per-trigger context layer. Merges a ModuleContext with
6
+ * arbitrary execution-time properties (e.g. { request, inputs } for HTTP;
7
+ * any shape is valid — determined by the trigger type).
8
+ *
9
+ * Execution props overlay the module namespaces on key conflict.
10
+ */
11
+ export class ExecutionContext extends EvaluationContext {
12
+ constructor(moduleCtx: ModuleContext, execProps: Record<string, unknown>) {
13
+ super(
14
+ moduleCtx.source,
15
+ Object.assign(Object.create(null), moduleCtx.context, execProps) as Record<string, unknown>,
16
+ moduleCtx.createInstance,
17
+ moduleCtx.secretValues,
18
+ moduleCtx.emit,
19
+ );
20
+ }
21
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { ControllerRegistry } from "./controller-registry.js";
2
+ export { EvaluationContext } from "./evaluation-context.js";
2
3
  export { EventStream } from "./event-stream.js";
4
+ export { ExecutionContext } from "./execution-context.js";
3
5
  export { Kernel, type KernelOptions } from "./kernel.js";
6
+ export { ModuleContext } from "./module-context.js";
4
7
  export { ManifestRegistry as Registry } from "./registry.js";
5
8
  export { ResourceURI } from "./resource-uri.js";
6
9
  export type { RuntimeDiagnostic } from "@telorun/sdk";
package/src/kernel.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { AnalysisRegistry, Loader, StaticAnalyzer } from "@telorun/analyzer";
1
+ import { AnalysisRegistry, DEFAULT_MANIFEST_FILENAME, Loader, StaticAnalyzer } from "@telorun/analyzer";
2
2
  import {
3
3
  ControllerContext,
4
4
  Kernel as IKernel,
5
- ModuleContext,
6
5
  ResourceContext,
7
6
  ResourceDefinition,
8
7
  ResourceInstance,
@@ -10,8 +9,11 @@ import {
10
9
  RuntimeError,
11
10
  RuntimeEvent,
12
11
  isCompiledValue,
12
+ type EvaluationContext as IEvaluationContext,
13
+ type ModuleContext as IModuleContext,
13
14
  type ParsedArgs,
14
15
  } from "@telorun/sdk";
16
+ import { ModuleContext } from "./module-context.js";
15
17
  import * as path from "path";
16
18
  import { parseArgs } from "util";
17
19
  import { ControllerRegistry } from "./controller-registry.js";
@@ -19,7 +21,7 @@ import { EventStream } from "./event-stream.js";
19
21
  import { EventBus } from "./events.js";
20
22
  import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
21
23
  import { ResourceContextImpl } from "./resource-context.js";
22
- import { SchemaValidator } from "./schema-valiator.js";
24
+ import { SchemaValidator } from "./schema-validator.js";
23
25
 
24
26
  export interface KernelOptions {
25
27
  stdin?: NodeJS.ReadableStream;
@@ -216,7 +218,7 @@ export class Kernel implements IKernel {
216
218
  * @deprecated Use loadFromConfig instead
217
219
  */
218
220
  async loadDirectory(dirPath: string): Promise<void> {
219
- const configYamlPath = path.join(dirPath, "module.yaml");
221
+ const configYamlPath = path.join(dirPath, DEFAULT_MANIFEST_FILENAME);
220
222
 
221
223
  await this.loadFromConfig(configYamlPath);
222
224
  }
@@ -361,8 +363,21 @@ export class Kernel implements IKernel {
361
363
  };
362
364
  }
363
365
 
366
+ /**
367
+ * Walk up the parent chain from a given evaluation context to find the nearest
368
+ * ModuleContext ancestor. Falls back to rootContext if none found.
369
+ */
370
+ private findModuleContext(ctx: IEvaluationContext): IModuleContext {
371
+ let current: IEvaluationContext | undefined = ctx;
372
+ while (current) {
373
+ if (current instanceof ModuleContext) return current;
374
+ current = current.parent;
375
+ }
376
+ return this.rootContext;
377
+ }
378
+
364
379
  private createResourceContext(
365
- moduleContext: ModuleContext,
380
+ moduleContext: IModuleContext,
366
381
  resource: ResourceManifest,
367
382
  args?: ParsedArgs,
368
383
  ): ResourceContext {
@@ -417,7 +432,7 @@ export class Kernel implements IKernel {
417
432
  * is not yet registered (retry signal).
418
433
  */
419
434
  private async _createInstance(
420
- evalContext: ModuleContext,
435
+ evalContext: IEvaluationContext,
421
436
  resource: ResourceManifest,
422
437
  ): Promise<{ instance: ResourceInstance; ctx: ResourceContext } | null> {
423
438
  const kind = resource.kind;
@@ -483,7 +498,8 @@ export class Kernel implements IKernel {
483
498
  : resource;
484
499
 
485
500
  const parsedArgs = this.parseArgsForController(controller);
486
- const ctx = this.createResourceContext(evalContext, processedResource, parsedArgs);
501
+ const moduleCtx = this.findModuleContext(evalContext);
502
+ const ctx = this.createResourceContext(moduleCtx, processedResource, parsedArgs);
487
503
  const instance = await controller.create(processedResource, ctx);
488
504
  if (!instance) return null;
489
505
 
@@ -578,20 +594,61 @@ function placeholderForSchema(schema: Record<string, unknown>): unknown {
578
594
  }
579
595
  }
580
596
 
597
+ /** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
598
+ function resolveSchemaRef(
599
+ schema: Record<string, unknown>,
600
+ root: Record<string, unknown>,
601
+ ): Record<string, unknown> {
602
+ if (schema.$ref && typeof schema.$ref === "string" && (schema.$ref as string).startsWith("#/$defs/")) {
603
+ const defName = (schema.$ref as string).slice("#/$defs/".length);
604
+ const defs = root.$defs as Record<string, Record<string, unknown>> | undefined;
605
+ const resolved = defs?.[defName];
606
+ if (resolved) return resolved;
607
+ }
608
+ return schema;
609
+ }
610
+
611
+ /** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
612
+ function collectSchemaProperties(
613
+ schema: Record<string, unknown>,
614
+ ): Record<string, Record<string, unknown>> {
615
+ const props: Record<string, Record<string, unknown>> = {
616
+ ...((schema.properties ?? {}) as Record<string, Record<string, unknown>>),
617
+ };
618
+ for (const sub of (schema.oneOf ?? schema.anyOf ?? []) as Record<string, unknown>[]) {
619
+ if (sub && typeof sub === "object" && sub.properties) {
620
+ for (const [k, v] of Object.entries(sub.properties as Record<string, Record<string, unknown>>)) {
621
+ if (!(k in props)) props[k] = v;
622
+ }
623
+ }
624
+ }
625
+ return props;
626
+ }
627
+
581
628
  /** Replaces CompiledValue wrappers with schema-appropriate placeholders for schema validation.
582
629
  * Template strings were compiled from YAML at load time; this restores a shape
583
630
  * that AJV can validate without evaluating expressions. */
584
- function stripCompiledValues(v: unknown, schema: Record<string, unknown> = {}): unknown {
585
- if (isCompiledValue(v)) return placeholderForSchema(schema);
631
+ function stripCompiledValues(
632
+ v: unknown,
633
+ schema: Record<string, unknown> = {},
634
+ rootSchema?: Record<string, unknown>,
635
+ ): unknown {
636
+ const root = rootSchema ?? schema;
637
+ const resolved = resolveSchemaRef(schema, root);
638
+
639
+ if (isCompiledValue(v)) return placeholderForSchema(resolved);
586
640
  if (Array.isArray(v)) {
587
- const itemSchema = (schema.items ?? {}) as Record<string, unknown>;
588
- return v.map((item) => stripCompiledValues(item, itemSchema));
641
+ const itemSchema = resolveSchemaRef(
642
+ (resolved.items ?? {}) as Record<string, unknown>,
643
+ root,
644
+ );
645
+ return v.map((item) => stripCompiledValues(item, itemSchema, root));
589
646
  }
590
647
  if (v !== null && typeof v === "object") {
591
- const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
648
+ const props = collectSchemaProperties(resolved);
592
649
  const out: Record<string, unknown> = {};
593
650
  for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
594
- out[k] = stripCompiledValues(val, props[k] ?? {});
651
+ out[k] = stripCompiledValues(val, props[k] ?? {}, root);
595
652
  }
596
653
  return out;
597
654
  }
@@ -1,4 +1,4 @@
1
- import type { ManifestAdapter } from "@telorun/analyzer";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "@telorun/analyzer";
2
2
  import * as fs from "fs/promises";
3
3
  import * as path from "path";
4
4
 
@@ -19,7 +19,7 @@ export class LocalFileAdapter implements ManifestAdapter {
19
19
  : pathOrUrl;
20
20
  const resolvedPath = path.resolve(normalizedPath);
21
21
  const stat = await fs.stat(resolvedPath);
22
- const filePath = stat.isDirectory() ? path.join(resolvedPath, "module.yaml") : resolvedPath;
22
+ const filePath = stat.isDirectory() ? path.join(resolvedPath, DEFAULT_MANIFEST_FILENAME) : resolvedPath;
23
23
  const text = await fs.readFile(filePath, "utf-8");
24
24
  return { text, source: `file://${filePath}` };
25
25
  }
@@ -17,7 +17,7 @@ export interface ManifestAdapter {
17
17
  /**
18
18
  * Read a single manifest entry point.
19
19
  * - File path or URL → read that file/URL.
20
- * - Directory path → find and read `module.yaml` within it.
20
+ * - Directory path → find and read `telo.yaml` within it.
21
21
  */
22
22
  read(pathOrUrl: string): Promise<ManifestSourceData>;
23
23
  /**