@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
@@ -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, highest first)
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) => b.priority - a.priority);
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
- if (ctx.memories.length > 0) {
35
- sections.push(formatMemory(ctx.memories));
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
- if (ctx.tools.length > 0) {
49
- sections.push(formatTools(ctx.tools));
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
- for (const m of memories.slice(0, 10)) { // Limit to 10 most relevant
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 formatSkills(skills: Skill[], maxLength?: number): string {
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 (s.content) {
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 v4Contracts and types for the dot-ai convention.
2
+ * @dot-ai/core v6Headless Agent framework.
3
3
  *
4
- * dot-ai = contracts (interfaces) + providers (pluggable implementations) + adapters (agent integration).
5
- * Core defines WHAT, providers define HOW, adapters define WHERE.
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
- // ── Contracts ──
29
+ // ── Extension Types ──
27
30
  export type {
28
- MemoryProvider,
29
- SkillProvider,
30
- IdentityProvider,
31
- RoutingProvider,
32
- TaskProvider,
33
- ToolProvider,
34
- ProviderFactory,
35
- } from './contracts.js';
36
-
37
- // ── Engine ──
38
- export { boot, enrich, learn } from './engine.js';
39
- export type { Providers, BootCache } from './engine.js';
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
- // ── Config ──
42
- export { loadConfig, resolveConfig, injectRoot } from './config.js';
43
- export type { ResolvedConfig } from './config.js';
61
+ // ── Extension Runner ──
62
+ export { ExtensionRunner, EventBus } from './extension-runner.js';
44
63
 
45
- // ── Format ──
46
- export { formatContext } from './format.js';
47
- export type { FormatOptions } from './format.js';
64
+ // ── Extension API ──
65
+ export type { ExtensionAPI, ExtensionContextV6 } from './extension-api.js';
48
66
 
49
- // ── Logger ──
50
- export type { LogLevel, LogEntry, Logger } from './logger.js';
51
- export { NoopLogger, JsonFileLogger, StderrLogger } from './logger.js';
67
+ // ── Extension Loader ──
68
+ export { discoverExtensions, createV6CollectorAPI } from './extension-loader.js';
52
69
 
53
- // ── Loader ──
54
- export { registerProvider, clearProviders, createProviders } from './loader.js';
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
- // ── registerDefaults ──
63
- export { registerDefaults } from './loader.js';
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
+ }