@dot-ai/core 0.5.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/boot-cache.d.ts +40 -0
  2. package/dist/boot-cache.d.ts.map +1 -0
  3. package/dist/boot-cache.js +72 -0
  4. package/dist/boot-cache.js.map +1 -0
  5. package/dist/capabilities.d.ts +35 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +17 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/config.d.ts +7 -23
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +131 -108
  12. package/dist/config.js.map +1 -1
  13. package/dist/extension-api.d.ts +65 -0
  14. package/dist/extension-api.d.ts.map +1 -0
  15. package/dist/extension-api.js +2 -0
  16. package/dist/extension-api.js.map +1 -0
  17. package/dist/extension-loader.d.ts +19 -0
  18. package/dist/extension-loader.d.ts.map +1 -0
  19. package/dist/extension-loader.js +113 -0
  20. package/dist/extension-loader.js.map +1 -0
  21. package/dist/extension-runner.d.ts +62 -0
  22. package/dist/extension-runner.d.ts.map +1 -0
  23. package/dist/extension-runner.js +260 -0
  24. package/dist/extension-runner.js.map +1 -0
  25. package/dist/extension-types.d.ts +312 -0
  26. package/dist/extension-types.d.ts.map +1 -0
  27. package/dist/extension-types.js +89 -0
  28. package/dist/extension-types.js.map +1 -0
  29. package/dist/format.d.ts +13 -1
  30. package/dist/format.d.ts.map +1 -1
  31. package/dist/format.js +131 -15
  32. package/dist/format.js.map +1 -1
  33. package/dist/format.spec.d.ts +2 -0
  34. package/dist/format.spec.d.ts.map +1 -0
  35. package/dist/format.spec.js +140 -0
  36. package/dist/format.spec.js.map +1 -0
  37. package/dist/index.d.ts +21 -14
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +21 -14
  40. package/dist/index.js.map +1 -1
  41. package/dist/logger.d.ts +1 -1
  42. package/dist/logger.d.ts.map +1 -1
  43. package/dist/package-manager.d.ts +30 -0
  44. package/dist/package-manager.d.ts.map +1 -0
  45. package/dist/package-manager.js +91 -0
  46. package/dist/package-manager.js.map +1 -0
  47. package/dist/runtime.d.ts +119 -0
  48. package/dist/runtime.d.ts.map +1 -0
  49. package/dist/runtime.js +441 -0
  50. package/dist/runtime.js.map +1 -0
  51. package/dist/types.d.ts +29 -10
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +4 -1
  54. package/src/__tests__/capabilities.test.ts +72 -0
  55. package/src/__tests__/config.test.ts +22 -120
  56. package/src/__tests__/extension-loader.test.ts +84 -0
  57. package/src/__tests__/extension-runner.test.ts +228 -0
  58. package/src/__tests__/fixtures/extensions/ctx-aware.js +26 -0
  59. package/src/__tests__/fixtures/extensions/security-gate.js +20 -0
  60. package/src/__tests__/fixtures/extensions/session-analytics.js +28 -0
  61. package/src/__tests__/fixtures/extensions/smart-context.js +10 -0
  62. package/src/__tests__/format.test.ts +207 -2
  63. package/src/__tests__/runtime.test.ts +141 -0
  64. package/src/boot-cache.ts +104 -0
  65. package/src/capabilities.ts +49 -0
  66. package/src/config.ts +131 -133
  67. package/src/extension-api.ts +99 -0
  68. package/src/extension-loader.ts +127 -0
  69. package/src/extension-runner.ts +297 -0
  70. package/src/extension-types.ts +416 -0
  71. package/src/format.spec.ts +175 -0
  72. package/src/format.test.ts +218 -0
  73. package/src/format.ts +140 -16
  74. package/src/index.ts +68 -30
  75. package/src/logger.ts +1 -1
  76. package/src/package-manager.ts +119 -0
  77. package/src/runtime.ts +562 -0
  78. package/src/types.ts +36 -14
  79. package/tsconfig.json +1 -1
  80. package/tsconfig.tsbuildinfo +1 -1
  81. package/.ai/memory/2026-03-04.md +0 -2
  82. package/.ai/tasks.json +0 -7
  83. package/dist/__tests__/config.test.d.ts +0 -2
  84. package/dist/__tests__/config.test.d.ts.map +0 -1
  85. package/dist/__tests__/config.test.js +0 -128
  86. package/dist/__tests__/config.test.js.map +0 -1
  87. package/dist/__tests__/e2e.test.d.ts +0 -2
  88. package/dist/__tests__/e2e.test.d.ts.map +0 -1
  89. package/dist/__tests__/e2e.test.js +0 -211
  90. package/dist/__tests__/e2e.test.js.map +0 -1
  91. package/dist/__tests__/engine.test.d.ts +0 -2
  92. package/dist/__tests__/engine.test.d.ts.map +0 -1
  93. package/dist/__tests__/engine.test.js +0 -271
  94. package/dist/__tests__/engine.test.js.map +0 -1
  95. package/dist/__tests__/format.test.d.ts +0 -2
  96. package/dist/__tests__/format.test.d.ts.map +0 -1
  97. package/dist/__tests__/format.test.js +0 -200
  98. package/dist/__tests__/format.test.js.map +0 -1
  99. package/dist/__tests__/labels.test.d.ts +0 -2
  100. package/dist/__tests__/labels.test.d.ts.map +0 -1
  101. package/dist/__tests__/labels.test.js +0 -82
  102. package/dist/__tests__/labels.test.js.map +0 -1
  103. package/dist/__tests__/loader.test.d.ts +0 -2
  104. package/dist/__tests__/loader.test.d.ts.map +0 -1
  105. package/dist/__tests__/loader.test.js +0 -161
  106. package/dist/__tests__/loader.test.js.map +0 -1
  107. package/dist/__tests__/logger.test.d.ts +0 -2
  108. package/dist/__tests__/logger.test.d.ts.map +0 -1
  109. package/dist/__tests__/logger.test.js +0 -95
  110. package/dist/__tests__/logger.test.js.map +0 -1
  111. package/dist/__tests__/nodes.test.d.ts +0 -2
  112. package/dist/__tests__/nodes.test.d.ts.map +0 -1
  113. package/dist/__tests__/nodes.test.js +0 -83
  114. package/dist/__tests__/nodes.test.js.map +0 -1
  115. package/dist/contracts.d.ts +0 -56
  116. package/dist/contracts.d.ts.map +0 -1
  117. package/dist/contracts.js +0 -2
  118. package/dist/contracts.js.map +0 -1
  119. package/dist/engine.d.ts +0 -38
  120. package/dist/engine.d.ts.map +0 -1
  121. package/dist/engine.js +0 -88
  122. package/dist/engine.js.map +0 -1
  123. package/dist/loader.d.ts +0 -26
  124. package/dist/loader.d.ts.map +0 -1
  125. package/dist/loader.js +0 -120
  126. package/dist/loader.js.map +0 -1
  127. package/src/__tests__/e2e.test.ts +0 -257
  128. package/src/__tests__/engine.test.ts +0 -305
  129. package/src/__tests__/loader.test.ts +0 -191
  130. package/src/contracts.ts +0 -71
  131. package/src/engine.ts +0 -145
  132. package/src/loader.ts +0 -152
package/src/runtime.ts ADDED
@@ -0,0 +1,562 @@
1
+ import { loadConfig } from './config.js';
2
+ import { computeChecksum, loadBootCache, writeBootCache } from './boot-cache.js';
3
+ import type { FormatOptions } from './format.js';
4
+ import { toolDefinitionToCapability } from './capabilities.js';
5
+ import type { Capability } from './capabilities.js';
6
+ import { discoverExtensions, createV6CollectorAPI } from './extension-loader.js';
7
+ import { ExtensionRunner, EventBus } from './extension-runner.js';
8
+ import type {
9
+ ToolCallEvent, ToolCallResult, ExtensionDiagnostic,
10
+ ContextInjectEvent, ContextInjectResult,
11
+ Section, ResourcesDiscoverResult, ContextEnrichEvent,
12
+ RouteEvent, RouteResult,
13
+ CommandDefinition,
14
+ } from './extension-types.js';
15
+ import type { ExtensionContextV6 } from './extension-api.js';
16
+ import type { EnrichedContext, ExtensionsConfig, Label, BudgetWarning } from './types.js';
17
+ import type { Logger } from './logger.js';
18
+ import { NoopLogger } from './logger.js';
19
+ import { extractLabels } from './labels.js';
20
+
21
+ export interface RuntimeOptions {
22
+ /** Workspace root directory (contains .ai/) */
23
+ workspaceRoot: string;
24
+ /** Optional logger */
25
+ logger?: Logger;
26
+ /** Skip identity sections in formatted output (useful when adapter injects them separately) */
27
+ skipIdentities?: boolean;
28
+ /** Max skills in formatted output */
29
+ maxSkills?: number;
30
+ /** Max chars per skill */
31
+ maxSkillLength?: number;
32
+ /** Token budget for formatted output */
33
+ tokenBudget?: number;
34
+ /** Extension configuration */
35
+ extensions?: ExtensionsConfig;
36
+ }
37
+
38
+ export interface ProcessResult {
39
+ /** The formatted context string ready for injection */
40
+ formatted: string;
41
+ /** The enriched context (for adapters that need raw data) */
42
+ enriched: EnrichedContext;
43
+ /** Interactive capabilities built from extensions */
44
+ capabilities: Capability[];
45
+ /** Routing result from route event */
46
+ routing?: RouteResult | null;
47
+ /** Matched labels */
48
+ labels?: Label[];
49
+ /** Sections collected from extensions */
50
+ sections?: Section[];
51
+ }
52
+
53
+ export interface RuntimeDiagnostics {
54
+ extensions: ExtensionDiagnostic[];
55
+ usedTiers: string[];
56
+ capabilityCount: number;
57
+ vocabularySize: number;
58
+ }
59
+
60
+ /**
61
+ * DotAiRuntime — encapsulates the full extension-based pipeline lifecycle.
62
+ * Boot once, process many prompts.
63
+ */
64
+ export class DotAiRuntime {
65
+ private caps: Capability[] = [];
66
+ private booted = false;
67
+ private readonly options: RuntimeOptions;
68
+ private readonly logger: Logger;
69
+ private _runner: ExtensionRunner | null = null;
70
+ private _eventBus: EventBus | null = null;
71
+ private vocabulary: string[] = [];
72
+
73
+ constructor(options: RuntimeOptions) {
74
+ this.options = options;
75
+ this.logger = options.logger ?? new NoopLogger();
76
+ }
77
+
78
+ // ── Context Builder ──
79
+
80
+ private buildCtx(labels: Label[] = []): ExtensionContextV6 {
81
+ return {
82
+ workspaceRoot: this.options.workspaceRoot,
83
+ events: this._eventBus ?? { on: () => {}, off: () => {}, emit: () => {} },
84
+ labels,
85
+ };
86
+ }
87
+
88
+ // ══════════════════════════════════════════════════════════════════════════════
89
+ // Boot
90
+ // ══════════════════════════════════════════════════════════════════════════════
91
+
92
+ /**
93
+ * Boot the runtime — loads config, discovers extensions, fires lifecycle events.
94
+ * Call once per session. Safe to call multiple times (idempotent).
95
+ */
96
+ async boot(): Promise<void> {
97
+ if (this.booted) return;
98
+
99
+ const start = performance.now();
100
+ const rawConfig = await loadConfig(this.options.workspaceRoot);
101
+
102
+ // Create event bus
103
+ this._eventBus = new EventBus();
104
+
105
+ // Discover extensions
106
+ const extConfig = this.options.extensions ?? rawConfig.extensions;
107
+ const extPaths = await discoverExtensions(this.options.workspaceRoot, extConfig);
108
+
109
+ // Compute checksum for cache invalidation
110
+ const checksum = await computeChecksum(this.options.workspaceRoot, extPaths);
111
+
112
+ // Try loading from cache
113
+ const cached = await loadBootCache(this.options.workspaceRoot, checksum);
114
+
115
+ // Load extensions via v6 collector API (always needed for handlers)
116
+ const loaded = await this.loadExtensions(extPaths);
117
+ this._runner = new ExtensionRunner(loaded, this.logger);
118
+
119
+ if (cached) {
120
+ // Cache hit — use cached vocabulary, skip resources_discover
121
+ this.vocabulary = cached.vocabulary;
122
+ this.logger.log({
123
+ timestamp: new Date().toISOString(),
124
+ level: 'info',
125
+ phase: 'boot',
126
+ event: 'boot_cache_hit',
127
+ data: { checksum, vocabularySize: cached.vocabulary.length },
128
+ });
129
+ } else {
130
+ // Cache miss — fire resources_discover → build vocabulary
131
+ const ctx = this.buildCtx();
132
+ const discoverResults = await this._runner.fire<ResourcesDiscoverResult>(
133
+ 'resources_discover', undefined, ctx,
134
+ );
135
+
136
+ // Build vocabulary from all extension-contributed labels
137
+ const allLabels = new Set<string>();
138
+ for (const result of discoverResults) {
139
+ if (result.labels && Array.isArray(result.labels)) {
140
+ for (const label of result.labels) {
141
+ allLabels.add(label);
142
+ }
143
+ }
144
+ }
145
+ this.vocabulary = Array.from(allLabels);
146
+
147
+ // Write cache
148
+ await writeBootCache(this.options.workspaceRoot, {
149
+ version: 1,
150
+ checksum,
151
+ vocabulary: this.vocabulary,
152
+ extensionPaths: extPaths,
153
+ tools: this._runner.tools.map(t => ({ name: t.name, description: t.description ?? '' })),
154
+ createdAt: new Date().toISOString(),
155
+ });
156
+ }
157
+
158
+ // Build capabilities from extension-registered tools
159
+ this.caps = this._runner.tools.map(toolDefinitionToCapability);
160
+
161
+ this.logger.log({
162
+ timestamp: new Date().toISOString(),
163
+ level: 'info',
164
+ phase: 'boot',
165
+ event: 'boot_complete',
166
+ data: {
167
+ extensionCount: loaded.length,
168
+ vocabularySize: this.vocabulary.length,
169
+ toolCount: this._runner.tools.length,
170
+ commandCount: this._runner.commands.length,
171
+ cacheHit: !!cached,
172
+ },
173
+ durationMs: Math.round(performance.now() - start),
174
+ });
175
+
176
+ // Fire session_start (always, regardless of cache)
177
+ const ctx = this.buildCtx();
178
+ await this._runner.fire('session_start', undefined, ctx);
179
+
180
+ this.booted = true;
181
+ }
182
+
183
+ /**
184
+ * Load extensions using jiti for TypeScript support, falls back to dynamic import.
185
+ */
186
+ private async loadExtensions(extensionPaths: string[]) {
187
+ if (extensionPaths.length === 0) return [];
188
+
189
+ let jitiImport: ((id: string) => unknown) | undefined;
190
+ try {
191
+ const { createJiti } = await import('jiti');
192
+ jitiImport = createJiti(import.meta.url, { interopDefault: true });
193
+ } catch {
194
+ this.logger.log({
195
+ timestamp: new Date().toISOString(),
196
+ level: 'warn',
197
+ phase: 'boot',
198
+ event: 'jiti_not_available',
199
+ data: { message: 'jiti not installed, falling back to dynamic import' },
200
+ });
201
+ }
202
+
203
+ const loaded: Array<import('./extension-types.js').LoadedExtension> = [];
204
+
205
+ for (const extPath of extensionPaths) {
206
+ try {
207
+ let mod: Record<string, unknown>;
208
+ if (jitiImport) {
209
+ mod = jitiImport(extPath) as Record<string, unknown>;
210
+ } else {
211
+ mod = await import(extPath) as Record<string, unknown>;
212
+ }
213
+
214
+ const factory = (typeof mod.default === 'function' ? mod.default : mod) as
215
+ ((api: unknown) => void | Promise<void>) | undefined;
216
+
217
+ if (typeof factory !== 'function') {
218
+ this.logger.log({
219
+ timestamp: new Date().toISOString(),
220
+ level: 'warn',
221
+ phase: 'boot',
222
+ event: 'extension_no_factory',
223
+ data: { path: extPath },
224
+ });
225
+ continue;
226
+ }
227
+
228
+ // TODO: resolve per-extension config from settings.json `with:` block
229
+ const extConfig: Record<string, unknown> = {};
230
+
231
+ const { api, extension } = createV6CollectorAPI(extPath, extConfig, this._eventBus!, this.options.workspaceRoot);
232
+ await factory(api);
233
+ loaded.push(extension);
234
+
235
+ this.logger.log({
236
+ timestamp: new Date().toISOString(),
237
+ level: 'info',
238
+ phase: 'boot',
239
+ event: 'extension_loaded',
240
+ data: {
241
+ path: extPath,
242
+ handlers: Object.fromEntries(
243
+ Array.from(extension.handlers.entries()).map(([k, v]) => [k, v.length]),
244
+ ),
245
+ tools: Array.from(extension.tools.keys()),
246
+ commands: Array.from(extension.commands.keys()),
247
+ },
248
+ });
249
+ } catch (err) {
250
+ this.logger.log({
251
+ timestamp: new Date().toISOString(),
252
+ level: 'warn',
253
+ phase: 'boot',
254
+ event: 'extension_load_error',
255
+ data: {
256
+ path: extPath,
257
+ error: err instanceof Error ? err.message : String(err),
258
+ },
259
+ });
260
+ }
261
+ }
262
+
263
+ return loaded;
264
+ }
265
+
266
+ // ══════════════════════════════════════════════════════════════════════════════
267
+ // Process Prompt
268
+ // ══════════════════════════════════════════════════════════════════════════════
269
+
270
+ /**
271
+ * Process a prompt through the pipeline:
272
+ * 1. Extract labels (core regex + label_extract chain-transform)
273
+ * 2. Route (first-result)
274
+ * 3. Context enrich (collect-sections)
275
+ * 4. Assemble sections by priority
276
+ * 5. Apply token budget trimming
277
+ */
278
+ async processPrompt(prompt: string, formatOverrides?: Partial<FormatOptions>): Promise<ProcessResult> {
279
+ if (!this.booted) {
280
+ await this.boot();
281
+ }
282
+
283
+ const start = performance.now();
284
+
285
+ // 1. Extract labels from prompt using vocabulary
286
+ let labels = extractLabels(prompt, this.vocabulary);
287
+
288
+ // Chain-transform via label_extract event
289
+ if (this._runner) {
290
+ const ctx = this.buildCtx(labels);
291
+ const enrichedLabels = await this._runner.fireChainTransform<Label[]>(
292
+ 'label_extract', labels, ctx,
293
+ );
294
+ if (enrichedLabels && Array.isArray(enrichedLabels)) {
295
+ labels = enrichedLabels;
296
+ }
297
+ }
298
+
299
+ this.logger.log({
300
+ timestamp: new Date().toISOString(),
301
+ level: 'info',
302
+ phase: 'enrich',
303
+ event: 'labels_extracted',
304
+ data: { labels: labels.map(l => l.name), vocabularySize: this.vocabulary.length },
305
+ durationMs: Math.round(performance.now() - start),
306
+ });
307
+
308
+ // 2. Route (first-result)
309
+ let routing: RouteResult | null = null;
310
+ if (this._runner) {
311
+ const routeEvent: RouteEvent = { labels };
312
+ const ctx = this.buildCtx(labels);
313
+ routing = await this._runner.fireFirstResult<RouteResult>('route', routeEvent, ctx);
314
+ }
315
+
316
+ // 3. Context enrich (collect-sections)
317
+ let sections: Section[] = [];
318
+ if (this._runner) {
319
+ const enrichEvent: ContextEnrichEvent = { prompt, labels };
320
+ const ctx = this.buildCtx(labels);
321
+ const collected = await this._runner.fireCollectSections('context_enrich', enrichEvent, ctx);
322
+ sections = collected.sections;
323
+
324
+ // Also fire legacy context_inject for backward compat
325
+ const injectEvent: ContextInjectEvent = { prompt, labels };
326
+ const injectResults = await this._runner.fire<ContextInjectResult>(
327
+ 'context_inject', injectEvent, ctx,
328
+ );
329
+ for (const result of injectResults) {
330
+ if (result.inject) {
331
+ sections.push({
332
+ id: `legacy-inject-${sections.length}`,
333
+ title: 'Extension Context',
334
+ content: result.inject,
335
+ priority: 20,
336
+ source: 'legacy',
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ // 4. Sort sections by priority DESC
343
+ sections.sort((a, b) => b.priority - a.priority);
344
+
345
+ // 5. Apply token budget trimming
346
+ const tokenBudget = formatOverrides?.tokenBudget ?? this.options.tokenBudget;
347
+ if (tokenBudget) {
348
+ sections = this.trimSections(sections, tokenBudget);
349
+ }
350
+
351
+ // 6. Assemble sections into formatted markdown
352
+ const formatted = this.assembleSections(sections);
353
+
354
+ this.logger.log({
355
+ timestamp: new Date().toISOString(),
356
+ level: 'info',
357
+ phase: 'format',
358
+ event: 'format_complete',
359
+ data: {
360
+ sectionCount: sections.length,
361
+ outputChars: formatted.length,
362
+ estimatedTokens: Math.round(formatted.length / 4),
363
+ routing: routing?.model ?? 'default',
364
+ labels: labels.map(l => l.name),
365
+ },
366
+ durationMs: Math.round(performance.now() - start),
367
+ });
368
+
369
+ // Build enriched context for adapters that need it
370
+ const enriched = this.buildEnrichedFromSections(prompt, labels, sections, routing);
371
+
372
+ return {
373
+ formatted,
374
+ enriched,
375
+ capabilities: this.caps,
376
+ routing,
377
+ labels,
378
+ sections,
379
+ };
380
+ }
381
+
382
+ // ══════════════════════════════════════════════════════════════════════════════
383
+ // Learn
384
+ // ══════════════════════════════════════════════════════════════════════════════
385
+
386
+ /**
387
+ * Learn from agent response — fires agent_end event.
388
+ * Memory extensions handle storage in their handlers.
389
+ */
390
+ async learn(response: string): Promise<void> {
391
+ if (this._runner) {
392
+ await this._runner.fire('agent_end', { response }, this.buildCtx());
393
+ }
394
+ }
395
+
396
+ // ══════════════════════════════════════════════════════════════════════════════
397
+ // Event Firing
398
+ // ══════════════════════════════════════════════════════════════════════════════
399
+
400
+ /**
401
+ * Fire an event (for adapters to call on agent-native events).
402
+ */
403
+ async fire<T>(event: string, data?: unknown): Promise<T[]> {
404
+ if (!this._runner) return [];
405
+ return this._runner.fire<T>(event, data, this.buildCtx());
406
+ }
407
+
408
+ /**
409
+ * Fire a tool_call event and return block result if any.
410
+ */
411
+ async fireToolCall(event: ToolCallEvent): Promise<ToolCallResult | null> {
412
+ if (!this._runner) return null;
413
+ return this._runner.fireUntilBlocked('tool_call', event, this.buildCtx());
414
+ }
415
+
416
+ /**
417
+ * Shutdown: fire session_end, flush logger.
418
+ */
419
+ async shutdown(): Promise<void> {
420
+ if (this._runner) {
421
+ await this._runner.fire('session_end', undefined, this.buildCtx());
422
+ }
423
+ await this.logger.flush();
424
+ }
425
+
426
+ // ══════════════════════════════════════════════════════════════════════════════
427
+ // Accessors
428
+ // ══════════════════════════════════════════════════════════════════════════════
429
+
430
+ /** Get the interactive capabilities (for registering as tools) */
431
+ get capabilities(): Capability[] {
432
+ return this.caps;
433
+ }
434
+
435
+ /** Check if runtime has been booted */
436
+ get isBooted(): boolean {
437
+ return this.booted;
438
+ }
439
+
440
+ /** Flush logger buffers — call before process exit in CLI hooks */
441
+ async flush(): Promise<void> {
442
+ await this.logger.flush();
443
+ }
444
+
445
+ /** Get the extension runner */
446
+ get runner(): ExtensionRunner | null {
447
+ return this._runner;
448
+ }
449
+
450
+ /** Get registered commands from extensions */
451
+ get commands(): CommandDefinition[] {
452
+ return this._runner?.commands ?? [];
453
+ }
454
+
455
+ /** Get diagnostics including extensions */
456
+ get diagnostics(): RuntimeDiagnostics {
457
+ return {
458
+ extensions: this._runner?.diagnostics ?? [],
459
+ usedTiers: this._runner ? Array.from(this._runner.usedTiers) : [],
460
+ capabilityCount: this.caps.length,
461
+ vocabularySize: this.vocabulary.length,
462
+ };
463
+ }
464
+
465
+ // ══════════════════════════════════════════════════════════════════════════════
466
+ // Helpers
467
+ // ══════════════════════════════════════════════════════════════════════════════
468
+
469
+ /**
470
+ * Assemble sorted sections into a single markdown string.
471
+ */
472
+ private assembleSections(sections: Section[]): string {
473
+ if (sections.length === 0) return '';
474
+
475
+ return sections
476
+ .map(s => {
477
+ if (s.title) {
478
+ return `## ${s.title}\n\n${s.content}`;
479
+ }
480
+ return s.content;
481
+ })
482
+ .join('\n\n---\n\n');
483
+ }
484
+
485
+ /**
486
+ * Trim sections to fit within a token budget.
487
+ * Respects trimStrategy: 'never' sections are never removed.
488
+ */
489
+ private trimSections(sections: Section[], budget: number): Section[] {
490
+ const estimateTokens = (secs: Section[]) =>
491
+ Math.round(secs.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n---\n\n').length / 4);
492
+
493
+ let current = estimateTokens(sections);
494
+ if (current <= budget) return sections;
495
+
496
+ const result = [...sections];
497
+ const actions: string[] = [];
498
+
499
+ // Strategy 1: Truncate 'truncate' sections
500
+ for (let i = result.length - 1; i >= 0; i--) {
501
+ if (current <= budget) break;
502
+ const s = result[i];
503
+ if (s.trimStrategy === 'truncate' && s.content.length > 2000) {
504
+ result[i] = { ...s, content: s.content.slice(0, 2000) + '\n\n[...truncated]' };
505
+ actions.push(`truncated section: ${s.id}`);
506
+ current = estimateTokens(result);
507
+ }
508
+ }
509
+
510
+ // Strategy 2: Drop non-'never' sections (lowest priority first)
511
+ for (let i = result.length - 1; i >= 0; i--) {
512
+ if (current <= budget) break;
513
+ const s = result[i];
514
+ if (s.trimStrategy !== 'never') {
515
+ actions.push(`dropped section: ${s.id} (priority ${s.priority})`);
516
+ result.splice(i, 1);
517
+ current = estimateTokens(result);
518
+ }
519
+ }
520
+
521
+ if (actions.length > 0) {
522
+ const warning: BudgetWarning = { budget, actual: current, actions };
523
+ this.logger.log({
524
+ timestamp: new Date().toISOString(),
525
+ level: current > budget ? 'warn' : 'info',
526
+ phase: 'format',
527
+ event: 'budget_trimmed',
528
+ data: warning as unknown as Record<string, unknown>,
529
+ });
530
+ }
531
+
532
+ return result;
533
+ }
534
+
535
+ /**
536
+ * Build a backward-compatible EnrichedContext from pipeline output.
537
+ * Adapters that log enriched fields still work.
538
+ */
539
+ private buildEnrichedFromSections(
540
+ prompt: string,
541
+ labels: Label[],
542
+ sections: Section[],
543
+ routing: RouteResult | null,
544
+ ): EnrichedContext {
545
+ return {
546
+ prompt,
547
+ labels,
548
+ identities: sections
549
+ .filter(s => s.source === 'identity' || s.priority >= 80)
550
+ .map(s => ({
551
+ type: s.id ?? s.source,
552
+ content: s.content,
553
+ source: s.source,
554
+ priority: s.priority,
555
+ })),
556
+ memories: [],
557
+ skills: [],
558
+ tools: [],
559
+ routing: routing ?? { model: 'default', reason: 'no routing extensions' },
560
+ };
561
+ }
562
+ }
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export interface Label {
6
6
  name: string;
7
- source: string; // which provider/step produced this
7
+ source: string; // which extension/step produced this
8
8
  }
9
9
 
10
10
  /**
@@ -31,7 +31,7 @@ export interface Skill {
31
31
  description: string;
32
32
  labels: string[];
33
33
  triggers?: string[]; // "always", "auto", pattern strings
34
- path?: string; // provider decides if this exists
34
+ path?: string; // extension decides if this exists
35
35
  content?: string; // lazy loaded
36
36
  dependsOn?: string[];
37
37
  requiresTools?: string[];
@@ -42,7 +42,7 @@ export interface Skill {
42
42
  export interface Identity {
43
43
  type: string; // "agents", "soul", "user", "identity"
44
44
  content: string;
45
- source: string; // provider name
45
+ source: string; // extension name
46
46
  priority: number; // for ordering in prompt
47
47
  node?: string; // which context node (null = root)
48
48
  }
@@ -73,7 +73,7 @@ export interface RoutingResult {
73
73
  }
74
74
 
75
75
  /**
76
- * The output of the enrich() call.
76
+ * The output of the enrich pipeline.
77
77
  * This is what adapters consume to inject into the agent.
78
78
  */
79
79
  export interface EnrichedContext {
@@ -81,6 +81,8 @@ export interface EnrichedContext {
81
81
  labels: Label[];
82
82
  identities: Identity[];
83
83
  memories: MemoryEntry[];
84
+ memoryDescription?: string;
85
+ recentTasks?: Task[];
84
86
  skills: Skill[];
85
87
  tools: Tool[];
86
88
  routing: RoutingResult;
@@ -96,7 +98,7 @@ export interface TaskFilter {
96
98
  }
97
99
 
98
100
  /**
99
- * Configuration from dot-ai.yml
101
+ * Configuration types
100
102
  */
101
103
  export interface DebugConfig {
102
104
  logPath?: string;
@@ -107,17 +109,37 @@ export interface WorkspaceConfig {
107
109
  }
108
110
 
109
111
  export interface DotAiConfig {
110
- memory?: ProviderConfig;
111
- skills?: ProviderConfig;
112
- identity?: ProviderConfig;
113
- routing?: ProviderConfig;
114
- tasks?: ProviderConfig;
115
- tools?: ProviderConfig;
116
112
  debug?: DebugConfig;
117
113
  workspace?: WorkspaceConfig;
114
+ extensions?: ExtensionsConfig;
115
+ prompts?: PromptsConfig;
118
116
  }
119
117
 
120
- export interface ProviderConfig {
121
- use: string; // "@dot-ai/provider-file-memory", "@dot-ai/cockpit-memory", etc.
122
- with?: Record<string, unknown>; // provider-specific options
118
+ /** Prompt template type */
119
+ export interface PromptTemplate {
120
+ name: string;
121
+ content: string;
122
+ args?: string[];
123
+ description?: string;
124
+ }
125
+
126
+ /** Extensions config section */
127
+ export interface ExtensionsConfig {
128
+ paths?: string[];
129
+ packages?: string[];
130
+ }
131
+
132
+ /** Prompts config section */
133
+ export interface PromptsConfig {
134
+ use?: string;
135
+ with?: Record<string, unknown>;
136
+ }
137
+
138
+ /**
139
+ * Warning emitted when context exceeds the token budget.
140
+ */
141
+ export interface BudgetWarning {
142
+ budget: number;
143
+ actual: number;
144
+ actions: string[];
123
145
  }
package/tsconfig.json CHANGED
@@ -19,5 +19,5 @@
19
19
  "composite": true
20
20
  },
21
21
  "include": ["src"],
22
- "exclude": ["node_modules", "dist"]
22
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
23
23
  }