@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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { formatContext } from './format.js';
|
|
3
|
+
import type { EnrichedContext, BudgetWarning, Skill, MemoryEntry, Identity } from './types.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function makeContext(overrides?: Partial<EnrichedContext>): EnrichedContext {
|
|
10
|
+
return {
|
|
11
|
+
prompt: 'test prompt',
|
|
12
|
+
labels: [],
|
|
13
|
+
identities: [],
|
|
14
|
+
memories: [],
|
|
15
|
+
skills: [],
|
|
16
|
+
tools: [],
|
|
17
|
+
routing: { model: 'default', reason: 'test' },
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeSkill(name: string, contentLength: number): Skill {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
description: `Skill ${name}`,
|
|
26
|
+
labels: [],
|
|
27
|
+
content: 'x'.repeat(contentLength),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeMemory(content: string): MemoryEntry {
|
|
32
|
+
return {
|
|
33
|
+
content,
|
|
34
|
+
type: 'fact',
|
|
35
|
+
source: 'test',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeIdentity(content: string, priority = 10): Identity {
|
|
40
|
+
return {
|
|
41
|
+
type: 'agents',
|
|
42
|
+
content,
|
|
43
|
+
source: 'test',
|
|
44
|
+
priority,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Tests
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe('formatContext budget', () => {
|
|
53
|
+
it('no budget — returns full content', () => {
|
|
54
|
+
const ctx = makeContext({
|
|
55
|
+
skills: [
|
|
56
|
+
makeSkill('skill-a', 100),
|
|
57
|
+
makeSkill('skill-b', 100),
|
|
58
|
+
makeSkill('skill-c', 100),
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = formatContext(ctx);
|
|
63
|
+
|
|
64
|
+
expect(result).toContain('skill-a');
|
|
65
|
+
expect(result).toContain('skill-b');
|
|
66
|
+
expect(result).toContain('skill-c');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('under budget — no trimming', () => {
|
|
70
|
+
const ctx = makeContext({
|
|
71
|
+
skills: [makeSkill('small-skill', 50)],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const onBudgetExceeded = vi.fn();
|
|
75
|
+
const result = formatContext(ctx, { tokenBudget: 10000, onBudgetExceeded });
|
|
76
|
+
|
|
77
|
+
expect(result).toContain('small-skill');
|
|
78
|
+
expect(onBudgetExceeded).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('over budget — drops skills by reverse order', () => {
|
|
82
|
+
// 5 skills × 1000 chars = ~5000 chars = ~1250 tokens for skills alone
|
|
83
|
+
const ctx = makeContext({
|
|
84
|
+
skills: [
|
|
85
|
+
makeSkill('skill-1', 1000),
|
|
86
|
+
makeSkill('skill-2', 1000),
|
|
87
|
+
makeSkill('skill-3', 1000),
|
|
88
|
+
makeSkill('skill-4', 1000),
|
|
89
|
+
makeSkill('skill-5', 1000),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const actions: string[] = [];
|
|
94
|
+
const onBudgetExceeded = vi.fn((w: BudgetWarning) => actions.push(...w.actions));
|
|
95
|
+
|
|
96
|
+
formatContext(ctx, { tokenBudget: 500, onBudgetExceeded });
|
|
97
|
+
|
|
98
|
+
expect(onBudgetExceeded).toHaveBeenCalled();
|
|
99
|
+
const droppedActions = actions.filter(a => a.startsWith('dropped skill:'));
|
|
100
|
+
expect(droppedActions.length).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('over budget — truncates skills first before dropping', () => {
|
|
104
|
+
// 2 skills × 5000 chars = ~10000 chars = ~2500 tokens
|
|
105
|
+
// New order: strategy 1 (truncate to 2000) fires FIRST
|
|
106
|
+
// After truncation: ~4100 chars = ~1025 tokens — still over 800
|
|
107
|
+
// Strategy 3 then drops big-skill-b to bring it under budget
|
|
108
|
+
const ctx = makeContext({
|
|
109
|
+
skills: [
|
|
110
|
+
makeSkill('big-skill-a', 5000),
|
|
111
|
+
makeSkill('big-skill-b', 5000),
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const actions: string[] = [];
|
|
116
|
+
const onBudgetExceeded = vi.fn((w: BudgetWarning) => actions.push(...w.actions));
|
|
117
|
+
|
|
118
|
+
formatContext(ctx, { tokenBudget: 800, onBudgetExceeded });
|
|
119
|
+
|
|
120
|
+
expect(onBudgetExceeded).toHaveBeenCalled();
|
|
121
|
+
// Truncation must appear before any drop action
|
|
122
|
+
const truncateIdx = actions.findIndex(a => a.includes('truncated'));
|
|
123
|
+
const dropIdx = actions.findIndex(a => a.startsWith('dropped skill:'));
|
|
124
|
+
expect(truncateIdx).toBeGreaterThanOrEqual(0);
|
|
125
|
+
// If a drop also happened, truncation came first
|
|
126
|
+
if (dropIdx !== -1) {
|
|
127
|
+
expect(truncateIdx).toBeLessThan(dropIdx);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('over budget — drops memories after skills trimmed', () => {
|
|
132
|
+
// 1 tiny skill + 10 memories × 200 chars = ~2000 chars = ~500 tokens
|
|
133
|
+
// Budget low enough to trigger memory trimming after skills are already minimal
|
|
134
|
+
const memories = Array.from({ length: 10 }, (_, i) =>
|
|
135
|
+
makeMemory('m'.repeat(200) + ` entry-${i}`),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const ctx = makeContext({
|
|
139
|
+
skills: [makeSkill('tiny', 10)],
|
|
140
|
+
memories,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const actions: string[] = [];
|
|
144
|
+
const onBudgetExceeded = vi.fn((w: BudgetWarning) => actions.push(...w.actions));
|
|
145
|
+
|
|
146
|
+
// ~500 token budget: memories alone exceed this, and there's only 1 skill
|
|
147
|
+
// (can't drop it, strategy 1 requires > 1 skill), truncation won't help much
|
|
148
|
+
// strategy 3 (drop old memories) should fire
|
|
149
|
+
formatContext(ctx, { tokenBudget: 100, onBudgetExceeded });
|
|
150
|
+
|
|
151
|
+
expect(onBudgetExceeded).toHaveBeenCalled();
|
|
152
|
+
const memoryActions = actions.filter(a => a.includes('memories'));
|
|
153
|
+
expect(memoryActions.length).toBeGreaterThan(0);
|
|
154
|
+
expect(memoryActions[0]).toMatch(/dropped \d+ oldest memories/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('onBudgetExceeded callback receives correct BudgetWarning', () => {
|
|
158
|
+
const ctx = makeContext({
|
|
159
|
+
skills: [
|
|
160
|
+
makeSkill('alpha', 2000),
|
|
161
|
+
makeSkill('beta', 2000),
|
|
162
|
+
makeSkill('gamma', 2000),
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let capturedWarning: BudgetWarning | undefined;
|
|
167
|
+
const onBudgetExceeded = vi.fn((w: BudgetWarning) => {
|
|
168
|
+
capturedWarning = w;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
formatContext(ctx, { tokenBudget: 200, onBudgetExceeded });
|
|
172
|
+
|
|
173
|
+
expect(onBudgetExceeded).toHaveBeenCalledOnce();
|
|
174
|
+
expect(capturedWarning).toBeDefined();
|
|
175
|
+
expect(capturedWarning!.budget).toBe(200);
|
|
176
|
+
expect(typeof capturedWarning!.actual).toBe('number');
|
|
177
|
+
expect(Array.isArray(capturedWarning!.actions)).toBe(true);
|
|
178
|
+
expect(capturedWarning!.actions.length).toBeGreaterThan(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('budget trimming preserves identity sections', () => {
|
|
182
|
+
const ctx = makeContext({
|
|
183
|
+
identities: [makeIdentity('You are Kiwi. This is your identity.')],
|
|
184
|
+
skills: [
|
|
185
|
+
makeSkill('skill-x', 3000),
|
|
186
|
+
makeSkill('skill-y', 3000),
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const onBudgetExceeded = vi.fn();
|
|
191
|
+
// Very low budget — forces trimming, but identities must survive
|
|
192
|
+
const result = formatContext(ctx, { tokenBudget: 100, onBudgetExceeded });
|
|
193
|
+
|
|
194
|
+
expect(result).toContain('You are Kiwi. This is your identity.');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('identity-only overflow calls onBudgetExceeded with non-trimmable message', () => {
|
|
198
|
+
// Identity content alone exceeds budget — no skills or memories to trim
|
|
199
|
+
const bigIdentity = 'You are Kiwi. '.repeat(500); // ~7000 chars = ~1750 tokens
|
|
200
|
+
const ctx = makeContext({
|
|
201
|
+
identities: [makeIdentity(bigIdentity)],
|
|
202
|
+
skills: [],
|
|
203
|
+
memories: [],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let capturedWarning: BudgetWarning | undefined;
|
|
207
|
+
const onBudgetExceeded = vi.fn((w: BudgetWarning) => {
|
|
208
|
+
capturedWarning = w;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
formatContext(ctx, { tokenBudget: 100, onBudgetExceeded });
|
|
212
|
+
|
|
213
|
+
expect(onBudgetExceeded).toHaveBeenCalledOnce();
|
|
214
|
+
expect(capturedWarning).toBeDefined();
|
|
215
|
+
expect(capturedWarning!.actions).toHaveLength(1);
|
|
216
|
+
expect(capturedWarning!.actions[0]).toContain('non-trimmable');
|
|
217
|
+
});
|
|
218
|
+
});
|
package/src/format.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { EnrichedContext, MemoryEntry, Skill, Tool, RoutingResult } from './types.js';
|
|
1
|
+
import type { EnrichedContext, MemoryEntry, Skill, Task, Tool, RoutingResult, BudgetWarning } from './types.js';
|
|
2
2
|
import type { Logger } from './logger.js';
|
|
3
|
+
import type { Capability } from './capabilities.js';
|
|
3
4
|
|
|
4
5
|
export interface FormatOptions {
|
|
5
6
|
/** Skip identity sections (useful when already injected at session start) */
|
|
@@ -8,8 +9,14 @@ export interface FormatOptions {
|
|
|
8
9
|
maxSkillLength?: number;
|
|
9
10
|
/** Max number of skills to include (already sorted by match relevance). Default: unlimited */
|
|
10
11
|
maxSkills?: number;
|
|
12
|
+
/** Max estimated tokens (chars / 4). When exceeded, content is trimmed. Default: no limit */
|
|
13
|
+
tokenBudget?: number;
|
|
14
|
+
/** Called when budget was exceeded and trimming occurred. Diagnostic signal. */
|
|
15
|
+
onBudgetExceeded?: (warning: BudgetWarning) => void;
|
|
11
16
|
/** Optional logger for tracing */
|
|
12
17
|
logger?: Logger;
|
|
18
|
+
/** Skill disclosure mode. 'progressive' shows name+description only, 'full' shows entire content */
|
|
19
|
+
skillDisclosure?: 'full' | 'progressive';
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
@@ -20,9 +27,12 @@ export function formatContext(ctx: EnrichedContext, options?: FormatOptions): st
|
|
|
20
27
|
const start = performance.now();
|
|
21
28
|
const sections: string[] = [];
|
|
22
29
|
|
|
23
|
-
// Identity sections (sorted by priority,
|
|
30
|
+
// Identity sections (sorted by priority DESC, then by type alphabetically for stability)
|
|
24
31
|
if (!options?.skipIdentities) {
|
|
25
|
-
const sortedIdentities = [...ctx.identities].sort((a, b) =>
|
|
32
|
+
const sortedIdentities = [...ctx.identities].sort((a, b) => {
|
|
33
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
34
|
+
return a.type.localeCompare(b.type);
|
|
35
|
+
});
|
|
26
36
|
for (const identity of sortedIdentities) {
|
|
27
37
|
if (identity.content) {
|
|
28
38
|
sections.push(identity.content);
|
|
@@ -30,23 +40,35 @@ export function formatContext(ctx: EnrichedContext, options?: FormatOptions): st
|
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
// Memory section
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
// Memory section (sorted by date DESC for stability)
|
|
44
|
+
const sortedMemories = [...ctx.memories].sort((a, b) => {
|
|
45
|
+
if (!a.date && !b.date) return 0;
|
|
46
|
+
if (!a.date) return 1;
|
|
47
|
+
if (!b.date) return -1;
|
|
48
|
+
return b.date.localeCompare(a.date);
|
|
49
|
+
});
|
|
50
|
+
if (sortedMemories.length > 0 || ctx.memoryDescription) {
|
|
51
|
+
sections.push(formatMemory(sortedMemories, ctx.memoryDescription));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Recent tasks section
|
|
55
|
+
if (ctx.recentTasks && ctx.recentTasks.length > 0) {
|
|
56
|
+
sections.push(formatTasks(ctx.recentTasks));
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
// Skills section
|
|
39
|
-
let loadedSkills = ctx.skills.filter(s => s.content);
|
|
59
|
+
// Skills section (sorted by name alphabetically for determinism)
|
|
60
|
+
let loadedSkills = [...ctx.skills].sort((a, b) => a.name.localeCompare(b.name)).filter(s => s.content);
|
|
40
61
|
if (options?.maxSkills != null) {
|
|
41
62
|
loadedSkills = loadedSkills.slice(0, options.maxSkills);
|
|
42
63
|
}
|
|
43
64
|
if (loadedSkills.length > 0) {
|
|
44
|
-
sections.push(formatSkills(loadedSkills, options?.maxSkillLength));
|
|
65
|
+
sections.push(formatSkills(loadedSkills, options?.maxSkillLength, options?.skillDisclosure));
|
|
45
66
|
}
|
|
46
67
|
|
|
47
|
-
// Tools section
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
// Tools section (sorted by name alphabetically for determinism)
|
|
69
|
+
const sortedTools = [...ctx.tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
70
|
+
if (sortedTools.length > 0) {
|
|
71
|
+
sections.push(formatTools(sortedTools));
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
// Routing hint
|
|
@@ -54,6 +76,75 @@ export function formatContext(ctx: EnrichedContext, options?: FormatOptions): st
|
|
|
54
76
|
sections.push(formatRouting(ctx.routing));
|
|
55
77
|
}
|
|
56
78
|
|
|
79
|
+
// Budget enforcement — trim if over token budget
|
|
80
|
+
if (options?.tokenBudget != null) {
|
|
81
|
+
const estimate = () => Math.round(sections.join('\n\n---\n\n').length / 4);
|
|
82
|
+
let current = estimate();
|
|
83
|
+
|
|
84
|
+
if (current > options.tokenBudget) {
|
|
85
|
+
const actions: string[] = [];
|
|
86
|
+
const skillSectionIdx = sections.findIndex(s => s.startsWith('## Active Skills'));
|
|
87
|
+
const memorySectionIdx = sections.findIndex(s => s.startsWith('## Relevant Memory'));
|
|
88
|
+
|
|
89
|
+
// Strategy 1: Truncate skill content to 2000 chars
|
|
90
|
+
if (current > options.tokenBudget && skillSectionIdx !== -1 && options?.maxSkillLength == null) {
|
|
91
|
+
const longSkills = loadedSkills.filter(s => (s.content?.length ?? 0) > 2000).length;
|
|
92
|
+
if (longSkills > 0) {
|
|
93
|
+
sections[skillSectionIdx] = formatSkills(loadedSkills, 2000, options?.skillDisclosure);
|
|
94
|
+
actions.push(`truncated ${longSkills} skills to 2000 chars`);
|
|
95
|
+
current = estimate();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Strategy 2: Drop oldest memories (keep most recent 5)
|
|
100
|
+
if (current > options.tokenBudget && memorySectionIdx !== -1 && sortedMemories.length > 5) {
|
|
101
|
+
const kept = sortedMemories.slice(0, 5);
|
|
102
|
+
const dropped = sortedMemories.length - 5;
|
|
103
|
+
sections[memorySectionIdx] = formatMemory(kept, ctx.memoryDescription);
|
|
104
|
+
actions.push(`dropped ${dropped} oldest memories`);
|
|
105
|
+
current = estimate();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Strategy 3: Drop skills by reverse order
|
|
109
|
+
if (current > options.tokenBudget && skillSectionIdx !== -1 && loadedSkills.length > 1) {
|
|
110
|
+
while (loadedSkills.length > 1 && current > options.tokenBudget) {
|
|
111
|
+
const dropped = loadedSkills.pop()!;
|
|
112
|
+
actions.push(`dropped skill: ${dropped.name}`);
|
|
113
|
+
sections[skillSectionIdx] = formatSkills(loadedSkills, options?.maxSkillLength ?? 2000, options?.skillDisclosure);
|
|
114
|
+
current = estimate();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (actions.length > 0) {
|
|
119
|
+
const warning: BudgetWarning = { budget: options.tokenBudget, actual: current, actions };
|
|
120
|
+
options.onBudgetExceeded?.(warning);
|
|
121
|
+
options?.logger?.log({
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
level: current > options.tokenBudget ? 'warn' : 'info',
|
|
124
|
+
phase: 'format',
|
|
125
|
+
event: 'budget_trimmed',
|
|
126
|
+
data: warning as unknown as Record<string, unknown>,
|
|
127
|
+
durationMs: Math.round(performance.now() - start),
|
|
128
|
+
});
|
|
129
|
+
} else if (current > options.tokenBudget) {
|
|
130
|
+
const warning: BudgetWarning = {
|
|
131
|
+
budget: options.tokenBudget,
|
|
132
|
+
actual: current,
|
|
133
|
+
actions: ['budget exceeded by non-trimmable content (identities)'],
|
|
134
|
+
};
|
|
135
|
+
options.onBudgetExceeded?.(warning);
|
|
136
|
+
options?.logger?.log({
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
level: 'warn',
|
|
139
|
+
phase: 'format',
|
|
140
|
+
event: 'budget_exceeded_no_action',
|
|
141
|
+
data: warning as unknown as Record<string, unknown>,
|
|
142
|
+
durationMs: Math.round(performance.now() - start),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
57
148
|
const result = sections.join('\n\n---\n\n');
|
|
58
149
|
|
|
59
150
|
options?.logger?.log({
|
|
@@ -75,20 +166,35 @@ export function formatContext(ctx: EnrichedContext, options?: FormatOptions): st
|
|
|
75
166
|
return result;
|
|
76
167
|
}
|
|
77
168
|
|
|
78
|
-
function formatMemory(memories: MemoryEntry[]): string {
|
|
169
|
+
function formatMemory(memories: MemoryEntry[], description?: string): string {
|
|
79
170
|
const lines = ['## Relevant Memory\n'];
|
|
80
|
-
|
|
171
|
+
if (description) {
|
|
172
|
+
lines.push(`> ${description}\n`);
|
|
173
|
+
}
|
|
174
|
+
for (const m of memories.slice(0, 10)) {
|
|
81
175
|
const date = m.date ? ` (${m.date})` : '';
|
|
82
176
|
lines.push(`- ${m.content}${date}`);
|
|
83
177
|
}
|
|
84
178
|
return lines.join('\n');
|
|
85
179
|
}
|
|
86
180
|
|
|
87
|
-
function
|
|
181
|
+
function formatTasks(tasks: Task[]): string {
|
|
182
|
+
const lines = ['## Current Tasks (In Progress)\n'];
|
|
183
|
+
for (const t of tasks.slice(0, 10)) {
|
|
184
|
+
const project = t.project ? ` [${t.project}]` : '';
|
|
185
|
+
const priority = t.priority ? ` (${t.priority})` : '';
|
|
186
|
+
lines.push(`- ${t.text}${project}${priority}`);
|
|
187
|
+
}
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatSkills(skills: Skill[], maxLength?: number, disclosure?: 'full' | 'progressive'): string {
|
|
88
192
|
const lines = ['## Active Skills\n'];
|
|
89
193
|
for (const s of skills) {
|
|
90
194
|
lines.push(`### ${s.name}`);
|
|
91
|
-
if (
|
|
195
|
+
if (disclosure === 'progressive') {
|
|
196
|
+
lines.push(s.description);
|
|
197
|
+
} else if (s.content) {
|
|
92
198
|
if (maxLength != null && s.content.length > maxLength) {
|
|
93
199
|
lines.push(s.content.slice(0, maxLength) + '\n\n[...truncated]');
|
|
94
200
|
} else {
|
|
@@ -108,6 +214,24 @@ function formatTools(tools: Tool[]): string {
|
|
|
108
214
|
return lines.join('\n');
|
|
109
215
|
}
|
|
110
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Format tool hints from capabilities that have promptSnippet or promptGuidelines.
|
|
219
|
+
* Returns empty string if no capabilities have hints.
|
|
220
|
+
*/
|
|
221
|
+
export function formatToolHints(capabilities: Capability[]): string {
|
|
222
|
+
const withHints = capabilities.filter(c => c.promptSnippet || c.promptGuidelines);
|
|
223
|
+
if (withHints.length === 0) return '';
|
|
224
|
+
|
|
225
|
+
const lines = ['## Tool Hints\n'];
|
|
226
|
+
for (const cap of withHints) {
|
|
227
|
+
lines.push(`### ${cap.name}`);
|
|
228
|
+
if (cap.promptSnippet) lines.push(cap.promptSnippet);
|
|
229
|
+
if (cap.promptGuidelines) lines.push(`\n> ${cap.promptGuidelines}`);
|
|
230
|
+
lines.push('');
|
|
231
|
+
}
|
|
232
|
+
return lines.join('\n');
|
|
233
|
+
}
|
|
234
|
+
|
|
111
235
|
function formatRouting(routing: RoutingResult): string {
|
|
112
236
|
return `## Model Routing\n\nRecommended model: **${routing.model}** (${routing.reason})`;
|
|
113
237
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @dot-ai/core
|
|
2
|
+
* @dot-ai/core v6 — Headless Agent framework.
|
|
3
3
|
*
|
|
4
|
-
* dot-ai =
|
|
5
|
-
*
|
|
4
|
+
* dot-ai = extensions (event-driven plugins) + adapters (agent integration).
|
|
5
|
+
* Everything is an extension. Core orchestrates events, adapters map to agent runtimes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// ── Types ──
|
|
@@ -19,39 +19,57 @@ export type {
|
|
|
19
19
|
TaskFilter,
|
|
20
20
|
DotAiConfig,
|
|
21
21
|
DebugConfig,
|
|
22
|
-
ProviderConfig,
|
|
23
22
|
WorkspaceConfig,
|
|
23
|
+
BudgetWarning,
|
|
24
|
+
PromptTemplate,
|
|
25
|
+
ExtensionsConfig,
|
|
26
|
+
PromptsConfig,
|
|
24
27
|
} from './types.js';
|
|
25
28
|
|
|
26
|
-
// ──
|
|
29
|
+
// ── Extension Types ──
|
|
27
30
|
export type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
Section,
|
|
32
|
+
ResourceEntry,
|
|
33
|
+
ResourcesDiscoverResult,
|
|
34
|
+
LabelExtractEvent,
|
|
35
|
+
ContextEnrichEvent,
|
|
36
|
+
ContextEnrichResult,
|
|
37
|
+
CollectedSections,
|
|
38
|
+
RouteEvent,
|
|
39
|
+
RouteResult,
|
|
40
|
+
InputEvent,
|
|
41
|
+
InputResult,
|
|
42
|
+
CommandParameter,
|
|
43
|
+
CommandResult,
|
|
44
|
+
CommandDefinition,
|
|
45
|
+
ToolDefinition,
|
|
46
|
+
ExtensionContext,
|
|
47
|
+
ExtensionEvent,
|
|
48
|
+
ExtensionTier,
|
|
49
|
+
ExtensionEventName,
|
|
50
|
+
LoadedExtension,
|
|
51
|
+
ExtensionDiagnostic,
|
|
52
|
+
ToolCallEvent, ToolCallResult,
|
|
53
|
+
ToolResultEvent,
|
|
54
|
+
AgentEndEvent,
|
|
55
|
+
Message,
|
|
56
|
+
ContextInjectEvent, ContextInjectResult,
|
|
57
|
+
ContextModifyEvent, ContextModifyResult,
|
|
58
|
+
} from './extension-types.js';
|
|
59
|
+
export { EVENT_TIERS, ADAPTER_CAPABILITIES, TOOL_STRATEGY } from './extension-types.js';
|
|
40
60
|
|
|
41
|
-
// ──
|
|
42
|
-
export {
|
|
43
|
-
export type { ResolvedConfig } from './config.js';
|
|
61
|
+
// ── Extension Runner ──
|
|
62
|
+
export { ExtensionRunner, EventBus } from './extension-runner.js';
|
|
44
63
|
|
|
45
|
-
// ──
|
|
46
|
-
export {
|
|
47
|
-
export type { FormatOptions } from './format.js';
|
|
64
|
+
// ── Extension API ──
|
|
65
|
+
export type { ExtensionAPI, ExtensionContextV6 } from './extension-api.js';
|
|
48
66
|
|
|
49
|
-
// ──
|
|
50
|
-
export
|
|
51
|
-
export { NoopLogger, JsonFileLogger, StderrLogger } from './logger.js';
|
|
67
|
+
// ── Extension Loader ──
|
|
68
|
+
export { discoverExtensions, createV6CollectorAPI } from './extension-loader.js';
|
|
52
69
|
|
|
53
|
-
// ──
|
|
54
|
-
export {
|
|
70
|
+
// ── Runtime ──
|
|
71
|
+
export { DotAiRuntime } from './runtime.js';
|
|
72
|
+
export type { RuntimeOptions, ProcessResult, RuntimeDiagnostics } from './runtime.js';
|
|
55
73
|
|
|
56
74
|
// ── Labels ──
|
|
57
75
|
export { extractLabels, buildVocabulary } from './labels.js';
|
|
@@ -59,5 +77,25 @@ export { extractLabels, buildVocabulary } from './labels.js';
|
|
|
59
77
|
// ── Nodes ──
|
|
60
78
|
export { discoverNodes, parseScanDirs } from './nodes.js';
|
|
61
79
|
|
|
62
|
-
// ──
|
|
63
|
-
export {
|
|
80
|
+
// ── Format ──
|
|
81
|
+
export { formatContext, formatToolHints } from './format.js';
|
|
82
|
+
export type { FormatOptions } from './format.js';
|
|
83
|
+
|
|
84
|
+
// ── Capabilities ──
|
|
85
|
+
export { toolDefinitionToCapability } from './capabilities.js';
|
|
86
|
+
export type { Capability, CapabilityResult } from './capabilities.js';
|
|
87
|
+
|
|
88
|
+
// ── Logger ──
|
|
89
|
+
export type { LogLevel, LogEntry, Logger } from './logger.js';
|
|
90
|
+
export { NoopLogger, JsonFileLogger, StderrLogger } from './logger.js';
|
|
91
|
+
|
|
92
|
+
// ── Config ──
|
|
93
|
+
export { loadConfig, migrateConfig } from './config.js';
|
|
94
|
+
|
|
95
|
+
// ── Package Manager ──
|
|
96
|
+
export { install, remove, listPackages, resolvePackages } from './package-manager.js';
|
|
97
|
+
export type { PackageInfo } from './package-manager.js';
|
|
98
|
+
|
|
99
|
+
// ── Boot Cache ──
|
|
100
|
+
export { computeChecksum, loadBootCache, writeBootCache, clearBootCache } from './boot-cache.js';
|
|
101
|
+
export type { BootCacheData } from './boot-cache.js';
|
package/src/logger.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
|
5
5
|
export interface LogEntry {
|
|
6
6
|
timestamp: string;
|
|
7
7
|
level: LogLevel;
|
|
8
|
-
phase: 'boot' | 'enrich' | 'learn' | 'format';
|
|
8
|
+
phase: 'boot' | 'enrich' | 'learn' | 'format' | 'runtime';
|
|
9
9
|
event: string;
|
|
10
10
|
data?: Record<string, unknown>;
|
|
11
11
|
durationMs?: number;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface PackageInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
dotAi?: {
|
|
9
|
+
extensions?: string[];
|
|
10
|
+
skills?: string[];
|
|
11
|
+
providers?: string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Install a dot-ai package from npm or git.
|
|
17
|
+
*/
|
|
18
|
+
export async function install(
|
|
19
|
+
source: string,
|
|
20
|
+
targetDir: string,
|
|
21
|
+
): Promise<PackageInfo> {
|
|
22
|
+
const installDir = join(targetDir, '.ai', 'packages');
|
|
23
|
+
await mkdir(installDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
execSync(`npm install --prefix "${installDir}" "${source}"`, {
|
|
26
|
+
stdio: 'pipe',
|
|
27
|
+
timeout: 60000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Read installed package info
|
|
31
|
+
const name = source.startsWith('@') || !source.includes('/')
|
|
32
|
+
? source.replace(/@[^/]*$/, '')
|
|
33
|
+
: source;
|
|
34
|
+
return readPackageInfo(installDir, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Remove an installed package.
|
|
39
|
+
*/
|
|
40
|
+
export async function remove(
|
|
41
|
+
name: string,
|
|
42
|
+
targetDir: string,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const installDir = join(targetDir, '.ai', 'packages');
|
|
45
|
+
execSync(`npm uninstall --prefix "${installDir}" "${name}"`, {
|
|
46
|
+
stdio: 'pipe',
|
|
47
|
+
timeout: 60000,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* List installed dot-ai packages.
|
|
53
|
+
*/
|
|
54
|
+
export async function listPackages(
|
|
55
|
+
targetDir: string,
|
|
56
|
+
): Promise<PackageInfo[]> {
|
|
57
|
+
const installDir = join(targetDir, '.ai', 'packages');
|
|
58
|
+
const pkgJsonPath = join(installDir, 'package.json');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(pkgJsonPath, 'utf-8');
|
|
62
|
+
const pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
63
|
+
const deps = pkg.dependencies as Record<string, string> | undefined;
|
|
64
|
+
if (!deps) return [];
|
|
65
|
+
|
|
66
|
+
const packages: PackageInfo[] = [];
|
|
67
|
+
for (const name of Object.keys(deps)) {
|
|
68
|
+
try {
|
|
69
|
+
const info = await readPackageInfo(installDir, name);
|
|
70
|
+
packages.push(info);
|
|
71
|
+
} catch {
|
|
72
|
+
packages.push({ name, version: deps[name] });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return packages;
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve dot-ai manifest from installed packages.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolvePackages(
|
|
85
|
+
targetDir: string,
|
|
86
|
+
): Promise<{ extensions: string[]; skills: string[]; providers: string[] }> {
|
|
87
|
+
const packages = await listPackages(targetDir);
|
|
88
|
+
const result = { extensions: [] as string[], skills: [] as string[], providers: [] as string[] };
|
|
89
|
+
|
|
90
|
+
const installDir = join(targetDir, '.ai', 'packages');
|
|
91
|
+
for (const pkg of packages) {
|
|
92
|
+
if (!pkg.dotAi) continue;
|
|
93
|
+
const pkgDir = join(installDir, 'node_modules', pkg.name);
|
|
94
|
+
|
|
95
|
+
if (pkg.dotAi.extensions) {
|
|
96
|
+
result.extensions.push(...pkg.dotAi.extensions.map(e => join(pkgDir, e)));
|
|
97
|
+
}
|
|
98
|
+
if (pkg.dotAi.skills) {
|
|
99
|
+
result.skills.push(...pkg.dotAi.skills.map(s => join(pkgDir, s)));
|
|
100
|
+
}
|
|
101
|
+
if (pkg.dotAi.providers) {
|
|
102
|
+
result.providers.push(...pkg.dotAi.providers.map(p => join(pkgDir, p)));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readPackageInfo(installDir: string, name: string): Promise<PackageInfo> {
|
|
110
|
+
const pkgPath = join(installDir, 'node_modules', name, 'package.json');
|
|
111
|
+
const raw = await readFile(pkgPath, 'utf-8');
|
|
112
|
+
const pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
name: pkg.name as string,
|
|
116
|
+
version: pkg.version as string,
|
|
117
|
+
dotAi: pkg['dot-ai'] as PackageInfo['dotAi'],
|
|
118
|
+
};
|
|
119
|
+
}
|