@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.
- package/dist/boot-cache.d.ts +40 -0
- package/dist/boot-cache.d.ts.map +1 -0
- package/dist/boot-cache.js +72 -0
- package/dist/boot-cache.js.map +1 -0
- package/dist/capabilities.d.ts +35 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +17 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +7 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +131 -108
- package/dist/config.js.map +1 -1
- package/dist/extension-api.d.ts +65 -0
- package/dist/extension-api.d.ts.map +1 -0
- package/dist/extension-api.js +2 -0
- package/dist/extension-api.js.map +1 -0
- package/dist/extension-loader.d.ts +19 -0
- package/dist/extension-loader.d.ts.map +1 -0
- package/dist/extension-loader.js +113 -0
- package/dist/extension-loader.js.map +1 -0
- package/dist/extension-runner.d.ts +62 -0
- package/dist/extension-runner.d.ts.map +1 -0
- package/dist/extension-runner.js +260 -0
- package/dist/extension-runner.js.map +1 -0
- package/dist/extension-types.d.ts +312 -0
- package/dist/extension-types.d.ts.map +1 -0
- package/dist/extension-types.js +89 -0
- package/dist/extension-types.js.map +1 -0
- package/dist/format.d.ts +13 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +131 -15
- package/dist/format.js.map +1 -1
- package/dist/format.spec.d.ts +2 -0
- package/dist/format.spec.d.ts.map +1 -0
- package/dist/format.spec.js +140 -0
- package/dist/format.spec.js.map +1 -0
- package/dist/index.d.ts +21 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -14
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/package-manager.d.ts +30 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +91 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/runtime.d.ts +119 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +441 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +29 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/capabilities.test.ts +72 -0
- package/src/__tests__/config.test.ts +22 -120
- package/src/__tests__/extension-loader.test.ts +84 -0
- package/src/__tests__/extension-runner.test.ts +228 -0
- package/src/__tests__/fixtures/extensions/ctx-aware.js +26 -0
- package/src/__tests__/fixtures/extensions/security-gate.js +20 -0
- package/src/__tests__/fixtures/extensions/session-analytics.js +28 -0
- package/src/__tests__/fixtures/extensions/smart-context.js +10 -0
- package/src/__tests__/format.test.ts +207 -2
- package/src/__tests__/runtime.test.ts +141 -0
- package/src/boot-cache.ts +104 -0
- package/src/capabilities.ts +49 -0
- package/src/config.ts +131 -133
- package/src/extension-api.ts +99 -0
- package/src/extension-loader.ts +127 -0
- package/src/extension-runner.ts +297 -0
- package/src/extension-types.ts +416 -0
- package/src/format.spec.ts +175 -0
- package/src/format.test.ts +218 -0
- package/src/format.ts +140 -16
- package/src/index.ts +68 -30
- package/src/logger.ts +1 -1
- package/src/package-manager.ts +119 -0
- package/src/runtime.ts +562 -0
- package/src/types.ts +36 -14
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/.ai/memory/2026-03-04.md +0 -2
- package/.ai/tasks.json +0 -7
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -128
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/e2e.test.d.ts +0 -2
- package/dist/__tests__/e2e.test.d.ts.map +0 -1
- package/dist/__tests__/e2e.test.js +0 -211
- package/dist/__tests__/e2e.test.js.map +0 -1
- package/dist/__tests__/engine.test.d.ts +0 -2
- package/dist/__tests__/engine.test.d.ts.map +0 -1
- package/dist/__tests__/engine.test.js +0 -271
- package/dist/__tests__/engine.test.js.map +0 -1
- package/dist/__tests__/format.test.d.ts +0 -2
- package/dist/__tests__/format.test.d.ts.map +0 -1
- package/dist/__tests__/format.test.js +0 -200
- package/dist/__tests__/format.test.js.map +0 -1
- package/dist/__tests__/labels.test.d.ts +0 -2
- package/dist/__tests__/labels.test.d.ts.map +0 -1
- package/dist/__tests__/labels.test.js +0 -82
- package/dist/__tests__/labels.test.js.map +0 -1
- package/dist/__tests__/loader.test.d.ts +0 -2
- package/dist/__tests__/loader.test.d.ts.map +0 -1
- package/dist/__tests__/loader.test.js +0 -161
- package/dist/__tests__/loader.test.js.map +0 -1
- package/dist/__tests__/logger.test.d.ts +0 -2
- package/dist/__tests__/logger.test.d.ts.map +0 -1
- package/dist/__tests__/logger.test.js +0 -95
- package/dist/__tests__/logger.test.js.map +0 -1
- package/dist/__tests__/nodes.test.d.ts +0 -2
- package/dist/__tests__/nodes.test.d.ts.map +0 -1
- package/dist/__tests__/nodes.test.js +0 -83
- package/dist/__tests__/nodes.test.js.map +0 -1
- package/dist/contracts.d.ts +0 -56
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -2
- package/dist/contracts.js.map +0 -1
- package/dist/engine.d.ts +0 -38
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -88
- package/dist/engine.js.map +0 -1
- package/dist/loader.d.ts +0 -26
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -120
- package/dist/loader.js.map +0 -1
- package/src/__tests__/e2e.test.ts +0 -257
- package/src/__tests__/engine.test.ts +0 -305
- package/src/__tests__/loader.test.ts +0 -191
- package/src/contracts.ts +0 -71
- package/src/engine.ts +0 -145
- 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
|
|
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; //
|
|
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; //
|
|
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
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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