@cleocode/core 2026.3.57 → 2026.3.59
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/agents/agent-registry.d.ts +206 -0
- package/dist/agents/agent-registry.d.ts.map +1 -0
- package/dist/agents/agent-schema.d.ts.map +1 -1
- package/dist/agents/execution-learning.d.ts +223 -0
- package/dist/agents/execution-learning.d.ts.map +1 -0
- package/dist/agents/health-monitor.d.ts +161 -0
- package/dist/agents/health-monitor.d.ts.map +1 -0
- package/dist/agents/index.d.ts +4 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/retry.d.ts +57 -4
- package/dist/agents/retry.d.ts.map +1 -1
- package/dist/backfill/index.d.ts +83 -0
- package/dist/backfill/index.d.ts.map +1 -0
- package/dist/bootstrap.d.ts +1 -1
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6985 -5068
- package/dist/index.js.map +4 -4
- package/dist/intelligence/adaptive-validation.d.ts +151 -0
- package/dist/intelligence/adaptive-validation.d.ts.map +1 -0
- package/dist/intelligence/impact.d.ts +34 -1
- package/dist/intelligence/impact.d.ts.map +1 -1
- package/dist/intelligence/index.d.ts +7 -2
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/types.d.ts +60 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/internal.d.ts +8 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/retry.d.ts +128 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/nexus/sharing/index.d.ts +48 -2
- package/dist/nexus/sharing/index.d.ts.map +1 -1
- package/dist/sessions/session-enforcement.d.ts.map +1 -1
- package/dist/stats/index.d.ts +1 -0
- package/dist/stats/index.d.ts.map +1 -1
- package/dist/stats/workflow-telemetry.d.ts +89 -0
- package/dist/stats/workflow-telemetry.d.ts.map +1 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/converters.d.ts.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +93 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -0
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/migration-sqlite.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/task-store.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +18 -3
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +32 -0
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/tasks/add.d.ts +10 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/enforcement.d.ts +22 -0
- package/dist/tasks/enforcement.d.ts.map +1 -0
- package/dist/tasks/epic-enforcement.d.ts +199 -0
- package/dist/tasks/epic-enforcement.d.ts.map +1 -0
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/pipeline-stage.d.ts +181 -0
- package/dist/tasks/pipeline-stage.d.ts.map +1 -0
- package/dist/tasks/update.d.ts +2 -0
- package/dist/tasks/update.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/migration.sql +12 -0
- package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/snapshot.json +1232 -0
- package/migrations/drizzle-tasks/20260321000000_t033-connection-health/migration.sql +518 -0
- package/migrations/drizzle-tasks/20260321000000_t033-connection-health/snapshot.json +4312 -0
- package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/migration.sql +82 -0
- package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/snapshot.json +9 -0
- package/package.json +5 -5
- package/schemas/config.schema.json +37 -1547
- package/src/__tests__/sharing.test.ts +24 -0
- package/src/agents/__tests__/agent-registry.test.ts +351 -0
- package/src/agents/__tests__/execution-learning.test.ts +684 -0
- package/src/agents/__tests__/health-monitor.test.ts +332 -0
- package/src/agents/__tests__/registry.test.ts +30 -2
- package/src/agents/agent-registry.ts +394 -0
- package/src/agents/agent-schema.ts +5 -0
- package/src/agents/execution-learning.ts +675 -0
- package/src/agents/health-monitor.ts +279 -0
- package/src/agents/index.ts +37 -1
- package/src/agents/retry.ts +57 -4
- package/src/backfill/index.ts +309 -0
- package/src/bootstrap.ts +1 -1
- package/src/config.ts +126 -0
- package/src/index.ts +8 -1
- package/src/intelligence/__tests__/adaptive-validation.test.ts +694 -0
- package/src/intelligence/__tests__/impact.test.ts +165 -1
- package/src/intelligence/adaptive-validation.ts +764 -0
- package/src/intelligence/impact.ts +203 -0
- package/src/intelligence/index.ts +19 -0
- package/src/intelligence/types.ts +76 -0
- package/src/internal.ts +39 -0
- package/src/lib/__tests__/retry.test.ts +321 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/retry.ts +224 -0
- package/src/lifecycle/__tests__/chain-store.test.ts +7 -0
- package/src/lifecycle/__tests__/tessera-engine.test.ts +52 -0
- package/src/nexus/sharing/index.ts +142 -2
- package/src/sessions/__tests__/session-edge-cases.test.ts +24 -1
- package/src/sessions/session-enforcement.ts +13 -2
- package/src/stats/index.ts +7 -0
- package/src/stats/workflow-telemetry.ts +502 -0
- package/src/store/__tests__/migration-safety.test.ts +3 -0
- package/src/store/__tests__/session-store.test.ts +132 -1
- package/src/store/__tests__/task-store.test.ts +22 -1
- package/src/store/__tests__/test-db-helper.ts +29 -2
- package/src/store/brain-schema.ts +4 -1
- package/src/store/converters.ts +2 -0
- package/src/store/cross-db-cleanup.ts +192 -0
- package/src/store/db-helpers.ts +2 -0
- package/src/store/migration-sqlite.ts +6 -0
- package/src/store/sqlite-data-accessor.ts +20 -28
- package/src/store/sqlite.ts +14 -2
- package/src/store/task-store.ts +6 -0
- package/src/store/tasks-schema.ts +59 -20
- package/src/tasks/__tests__/add.test.ts +16 -0
- package/src/tasks/__tests__/complete-unblocks.test.ts +10 -1
- package/src/tasks/__tests__/complete.test.ts +11 -2
- package/src/tasks/__tests__/epic-enforcement.test.ts +909 -0
- package/src/tasks/__tests__/minimal-test.test.ts +28 -0
- package/src/tasks/__tests__/pipeline-stage.test.ts +403 -0
- package/src/tasks/__tests__/update.test.ts +40 -6
- package/src/tasks/add.ts +128 -2
- package/src/tasks/complete.ts +29 -17
- package/src/tasks/enforcement.ts +127 -0
- package/src/tasks/epic-enforcement.ts +364 -0
- package/src/tasks/index.ts +1 -0
- package/src/tasks/pipeline-stage.ts +293 -0
- package/src/tasks/update.ts +62 -0
- package/templates/config.template.json +34 -111
- package/templates/global-config.template.json +24 -40
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Agent Execution Learning module.
|
|
3
|
+
*
|
|
4
|
+
* Covers: execution event recording, performance history queries,
|
|
5
|
+
* failure pattern accumulation, healing strategy storage, and the
|
|
6
|
+
* compound processAgentLifecycleEvent function.
|
|
7
|
+
*
|
|
8
|
+
* Uses real SQLite (via brain.db) in a temp directory per test.
|
|
9
|
+
*
|
|
10
|
+
* @module agents/__tests__/execution-learning.test
|
|
11
|
+
* @task T034
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
|
+
|
|
19
|
+
import { getBrainAccessor } from '../../store/brain-accessor.js';
|
|
20
|
+
import { resetBrainDbState } from '../../store/brain-sqlite.js';
|
|
21
|
+
import {
|
|
22
|
+
_getAgentPerformanceHistoryWithAccessor,
|
|
23
|
+
_getSelfHealingSuggestionsWithAccessor,
|
|
24
|
+
_recordAgentExecutionWithAccessor,
|
|
25
|
+
_recordFailurePatternWithAccessor,
|
|
26
|
+
_storeHealingStrategyWithAccessor,
|
|
27
|
+
type AgentExecutionEvent,
|
|
28
|
+
getAgentPerformanceHistory,
|
|
29
|
+
getSelfHealingSuggestions,
|
|
30
|
+
processAgentLifecycleEvent,
|
|
31
|
+
recordAgentExecution,
|
|
32
|
+
recordFailurePattern,
|
|
33
|
+
storeHealingStrategy,
|
|
34
|
+
} from '../execution-learning.js';
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Test setup
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
describe('Agent Execution Learning', () => {
|
|
41
|
+
let tempDir: string;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-exec-learn-test-'));
|
|
45
|
+
await mkdir(join(tempDir, '.cleo'), { recursive: true });
|
|
46
|
+
await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
resetBrainDbState();
|
|
51
|
+
try {
|
|
52
|
+
const { closeAllDatabases } = await import('../../store/sqlite.js');
|
|
53
|
+
await closeAllDatabases();
|
|
54
|
+
} catch {
|
|
55
|
+
/* module may not be loaded */
|
|
56
|
+
}
|
|
57
|
+
await Promise.race([
|
|
58
|
+
rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
|
|
59
|
+
new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ==========================================================================
|
|
64
|
+
// recordAgentExecution
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
|
|
67
|
+
describe('recordAgentExecution', () => {
|
|
68
|
+
it('stores a successful execution as a tactical brain decision', async () => {
|
|
69
|
+
const event: AgentExecutionEvent = {
|
|
70
|
+
agentId: 'agt_20260321_abc123',
|
|
71
|
+
agentType: 'executor',
|
|
72
|
+
taskId: 'T001',
|
|
73
|
+
taskType: 'task',
|
|
74
|
+
outcome: 'success',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const row = await recordAgentExecution(event, tempDir);
|
|
78
|
+
|
|
79
|
+
expect(row).not.toBeNull();
|
|
80
|
+
expect(row!.id).toMatch(/^AGT-/);
|
|
81
|
+
expect(row!.type).toBe('tactical');
|
|
82
|
+
expect(row!.outcome).toBe('success');
|
|
83
|
+
expect(row!.confidence).toBe('high');
|
|
84
|
+
expect(row!.contextTaskId).toBe('T001');
|
|
85
|
+
expect(row!.decision).toContain('executor');
|
|
86
|
+
expect(row!.decision).toContain('successfully completed');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('stores a failure execution with low confidence and failure outcome', async () => {
|
|
90
|
+
const event: AgentExecutionEvent = {
|
|
91
|
+
agentId: 'agt_20260321_abc123',
|
|
92
|
+
agentType: 'researcher',
|
|
93
|
+
taskId: 'T002',
|
|
94
|
+
taskType: 'epic',
|
|
95
|
+
outcome: 'failure',
|
|
96
|
+
errorMessage: 'ECONNREFUSED',
|
|
97
|
+
errorType: 'retriable',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const row = await recordAgentExecution(event, tempDir);
|
|
101
|
+
|
|
102
|
+
expect(row).not.toBeNull();
|
|
103
|
+
expect(row!.outcome).toBe('failure');
|
|
104
|
+
expect(row!.confidence).toBe('low');
|
|
105
|
+
expect(row!.decision).toContain('failed');
|
|
106
|
+
expect(row!.decision).toContain('researcher');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('stores a partial execution with medium confidence and mixed outcome', async () => {
|
|
110
|
+
const event: AgentExecutionEvent = {
|
|
111
|
+
agentId: 'agt_20260321_abc123',
|
|
112
|
+
agentType: 'validator',
|
|
113
|
+
taskId: 'T003',
|
|
114
|
+
taskType: 'subtask',
|
|
115
|
+
outcome: 'partial',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const row = await recordAgentExecution(event, tempDir);
|
|
119
|
+
|
|
120
|
+
expect(row).not.toBeNull();
|
|
121
|
+
expect(row!.outcome).toBe('mixed');
|
|
122
|
+
expect(row!.confidence).toBe('medium');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('embeds structured metadata in alternativesJson', async () => {
|
|
126
|
+
const event: AgentExecutionEvent = {
|
|
127
|
+
agentId: 'agt_20260321_abc123',
|
|
128
|
+
agentType: 'orchestrator',
|
|
129
|
+
taskId: 'T010',
|
|
130
|
+
taskType: 'task',
|
|
131
|
+
taskLabels: ['schema', 'core'],
|
|
132
|
+
outcome: 'success',
|
|
133
|
+
sessionId: 'ses_test_abc',
|
|
134
|
+
durationMs: 4200,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const row = await recordAgentExecution(event, tempDir);
|
|
138
|
+
expect(row).not.toBeNull();
|
|
139
|
+
|
|
140
|
+
const meta = JSON.parse(row!.alternativesJson ?? '{}') as Record<string, unknown>;
|
|
141
|
+
expect(meta.agentType).toBe('orchestrator');
|
|
142
|
+
expect(meta.taskType).toBe('task');
|
|
143
|
+
expect(meta.taskLabels).toEqual(['schema', 'core']);
|
|
144
|
+
expect(meta.sessionId).toBe('ses_test_abc');
|
|
145
|
+
expect(meta.durationMs).toBe(4200);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns null gracefully when brain.db is unavailable (bad path)', async () => {
|
|
149
|
+
// Use a path that will fail — brain.db accessor will throw
|
|
150
|
+
const event: AgentExecutionEvent = {
|
|
151
|
+
agentId: 'agt_test',
|
|
152
|
+
agentType: 'executor',
|
|
153
|
+
taskId: 'T001',
|
|
154
|
+
taskType: 'task',
|
|
155
|
+
outcome: 'success',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Pass a non-existent cwd that will cause sqlite open to fail
|
|
159
|
+
const result = await recordAgentExecution(event, '/nonexistent/path/abc');
|
|
160
|
+
// Should return null without throwing
|
|
161
|
+
expect(result === null || result !== undefined).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
// getAgentPerformanceHistory
|
|
167
|
+
// ==========================================================================
|
|
168
|
+
|
|
169
|
+
describe('getAgentPerformanceHistory', () => {
|
|
170
|
+
it('returns empty array when no execution events exist', async () => {
|
|
171
|
+
const summaries = await getAgentPerformanceHistory({}, tempDir);
|
|
172
|
+
expect(summaries).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('aggregates successes and failures by (agentType, taskType)', async () => {
|
|
176
|
+
const brain = await getBrainAccessor(tempDir);
|
|
177
|
+
|
|
178
|
+
const events: AgentExecutionEvent[] = [
|
|
179
|
+
{
|
|
180
|
+
agentId: 'agt_1',
|
|
181
|
+
agentType: 'executor',
|
|
182
|
+
taskId: 'T001',
|
|
183
|
+
taskType: 'task',
|
|
184
|
+
outcome: 'success',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
agentId: 'agt_1',
|
|
188
|
+
agentType: 'executor',
|
|
189
|
+
taskId: 'T002',
|
|
190
|
+
taskType: 'task',
|
|
191
|
+
outcome: 'success',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
agentId: 'agt_1',
|
|
195
|
+
agentType: 'executor',
|
|
196
|
+
taskId: 'T003',
|
|
197
|
+
taskType: 'task',
|
|
198
|
+
outcome: 'failure',
|
|
199
|
+
errorType: 'retriable',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
agentId: 'agt_2',
|
|
203
|
+
agentType: 'researcher',
|
|
204
|
+
taskId: 'T004',
|
|
205
|
+
taskType: 'epic',
|
|
206
|
+
outcome: 'success',
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
for (const e of events) {
|
|
211
|
+
await _recordAgentExecutionWithAccessor(e, brain);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const summaries = await _getAgentPerformanceHistoryWithAccessor({}, brain);
|
|
215
|
+
|
|
216
|
+
expect(summaries.length).toBe(2); // executor/task + researcher/epic
|
|
217
|
+
|
|
218
|
+
const executorSummary = summaries.find(
|
|
219
|
+
(s) => s.agentType === 'executor' && s.taskType === 'task',
|
|
220
|
+
);
|
|
221
|
+
expect(executorSummary).toBeDefined();
|
|
222
|
+
expect(executorSummary!.totalAttempts).toBe(3);
|
|
223
|
+
expect(executorSummary!.successCount).toBe(2);
|
|
224
|
+
expect(executorSummary!.failureCount).toBe(1);
|
|
225
|
+
expect(executorSummary!.successRate).toBeCloseTo(0.667, 2);
|
|
226
|
+
|
|
227
|
+
const researcherSummary = summaries.find(
|
|
228
|
+
(s) => s.agentType === 'researcher' && s.taskType === 'epic',
|
|
229
|
+
);
|
|
230
|
+
expect(researcherSummary).toBeDefined();
|
|
231
|
+
expect(researcherSummary!.totalAttempts).toBe(1);
|
|
232
|
+
expect(researcherSummary!.successRate).toBe(1.0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('filters by agentType when specified', async () => {
|
|
236
|
+
const brain = await getBrainAccessor(tempDir);
|
|
237
|
+
|
|
238
|
+
await _recordAgentExecutionWithAccessor(
|
|
239
|
+
{
|
|
240
|
+
agentId: 'a1',
|
|
241
|
+
agentType: 'executor',
|
|
242
|
+
taskId: 'T1',
|
|
243
|
+
taskType: 'task',
|
|
244
|
+
outcome: 'success',
|
|
245
|
+
},
|
|
246
|
+
brain,
|
|
247
|
+
);
|
|
248
|
+
await _recordAgentExecutionWithAccessor(
|
|
249
|
+
{
|
|
250
|
+
agentId: 'a2',
|
|
251
|
+
agentType: 'researcher',
|
|
252
|
+
taskId: 'T2',
|
|
253
|
+
taskType: 'epic',
|
|
254
|
+
outcome: 'success',
|
|
255
|
+
},
|
|
256
|
+
brain,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const summaries = await _getAgentPerformanceHistoryWithAccessor(
|
|
260
|
+
{ agentType: 'executor' },
|
|
261
|
+
brain,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(summaries.length).toBe(1);
|
|
265
|
+
expect(summaries[0]!.agentType).toBe('executor');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('filters by taskType when specified', async () => {
|
|
269
|
+
const brain = await getBrainAccessor(tempDir);
|
|
270
|
+
|
|
271
|
+
await _recordAgentExecutionWithAccessor(
|
|
272
|
+
{
|
|
273
|
+
agentId: 'a1',
|
|
274
|
+
agentType: 'executor',
|
|
275
|
+
taskId: 'T1',
|
|
276
|
+
taskType: 'task',
|
|
277
|
+
outcome: 'success',
|
|
278
|
+
},
|
|
279
|
+
brain,
|
|
280
|
+
);
|
|
281
|
+
await _recordAgentExecutionWithAccessor(
|
|
282
|
+
{
|
|
283
|
+
agentId: 'a2',
|
|
284
|
+
agentType: 'executor',
|
|
285
|
+
taskId: 'T2',
|
|
286
|
+
taskType: 'epic',
|
|
287
|
+
outcome: 'failure',
|
|
288
|
+
errorType: 'permanent',
|
|
289
|
+
},
|
|
290
|
+
brain,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const summaries = await _getAgentPerformanceHistoryWithAccessor({ taskType: 'epic' }, brain);
|
|
294
|
+
|
|
295
|
+
expect(summaries.length).toBe(1);
|
|
296
|
+
expect(summaries[0]!.taskType).toBe('epic');
|
|
297
|
+
expect(summaries[0]!.failureCount).toBe(1);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('tracks lastOutcome and lastSeenAt', async () => {
|
|
301
|
+
const brain = await getBrainAccessor(tempDir);
|
|
302
|
+
|
|
303
|
+
await _recordAgentExecutionWithAccessor(
|
|
304
|
+
{
|
|
305
|
+
agentId: 'a1',
|
|
306
|
+
agentType: 'executor',
|
|
307
|
+
taskId: 'T1',
|
|
308
|
+
taskType: 'task',
|
|
309
|
+
outcome: 'success',
|
|
310
|
+
},
|
|
311
|
+
brain,
|
|
312
|
+
);
|
|
313
|
+
await _recordAgentExecutionWithAccessor(
|
|
314
|
+
{
|
|
315
|
+
agentId: 'a1',
|
|
316
|
+
agentType: 'executor',
|
|
317
|
+
taskId: 'T2',
|
|
318
|
+
taskType: 'task',
|
|
319
|
+
outcome: 'failure',
|
|
320
|
+
errorType: 'retriable',
|
|
321
|
+
},
|
|
322
|
+
brain,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const summaries = await _getAgentPerformanceHistoryWithAccessor({}, brain);
|
|
326
|
+
const s = summaries.find((x) => x.agentType === 'executor');
|
|
327
|
+
|
|
328
|
+
expect(s).toBeDefined();
|
|
329
|
+
expect(s!.lastOutcome).toBe('failure'); // last recorded
|
|
330
|
+
expect(s!.lastSeenAt).not.toBeNull();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ==========================================================================
|
|
335
|
+
// recordFailurePattern
|
|
336
|
+
// ==========================================================================
|
|
337
|
+
|
|
338
|
+
describe('recordFailurePattern', () => {
|
|
339
|
+
it('returns null for non-failure events', async () => {
|
|
340
|
+
const event: AgentExecutionEvent = {
|
|
341
|
+
agentId: 'agt_1',
|
|
342
|
+
agentType: 'executor',
|
|
343
|
+
taskId: 'T001',
|
|
344
|
+
taskType: 'task',
|
|
345
|
+
outcome: 'success',
|
|
346
|
+
};
|
|
347
|
+
const result = await recordFailurePattern(event, tempDir);
|
|
348
|
+
expect(result).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('creates a new failure pattern on first occurrence', async () => {
|
|
352
|
+
const event: AgentExecutionEvent = {
|
|
353
|
+
agentId: 'agt_1',
|
|
354
|
+
agentType: 'executor',
|
|
355
|
+
taskId: 'T001',
|
|
356
|
+
taskType: 'task',
|
|
357
|
+
outcome: 'failure',
|
|
358
|
+
errorType: 'permanent',
|
|
359
|
+
errorMessage: 'Permission denied',
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const pattern = await recordFailurePattern(event, tempDir);
|
|
363
|
+
|
|
364
|
+
expect(pattern).not.toBeNull();
|
|
365
|
+
expect(pattern!.id).toMatch(/^P-agt-/);
|
|
366
|
+
expect(pattern!.type).toBe('failure');
|
|
367
|
+
expect(pattern!.frequency).toBe(1);
|
|
368
|
+
expect(pattern!.pattern).toContain('executor');
|
|
369
|
+
expect(pattern!.pattern).toContain('task');
|
|
370
|
+
expect(pattern!.pattern).toContain('permanent');
|
|
371
|
+
expect(pattern!.mitigation).toContain('Reassign task');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('increments frequency on repeated failures with same pattern', async () => {
|
|
375
|
+
const brain = await getBrainAccessor(tempDir);
|
|
376
|
+
|
|
377
|
+
const event: AgentExecutionEvent = {
|
|
378
|
+
agentId: 'agt_1',
|
|
379
|
+
agentType: 'researcher',
|
|
380
|
+
taskId: 'T001',
|
|
381
|
+
taskType: 'epic',
|
|
382
|
+
outcome: 'failure',
|
|
383
|
+
errorType: 'retriable',
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const first = await _recordFailurePatternWithAccessor(event, brain);
|
|
387
|
+
expect(first!.frequency).toBe(1);
|
|
388
|
+
|
|
389
|
+
// Second occurrence — same agentType, taskType, errorType
|
|
390
|
+
const second = await _recordFailurePatternWithAccessor({ ...event, taskId: 'T002' }, brain);
|
|
391
|
+
expect(second!.frequency).toBe(2);
|
|
392
|
+
|
|
393
|
+
// Third occurrence
|
|
394
|
+
const third = await _recordFailurePatternWithAccessor({ ...event, taskId: 'T003' }, brain);
|
|
395
|
+
expect(third!.frequency).toBe(3);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('builds retry suggestion for retriable errors', async () => {
|
|
399
|
+
const brain = await getBrainAccessor(tempDir);
|
|
400
|
+
|
|
401
|
+
const pattern = await _recordFailurePatternWithAccessor(
|
|
402
|
+
{
|
|
403
|
+
agentId: 'agt_1',
|
|
404
|
+
agentType: 'executor',
|
|
405
|
+
taskId: 'T001',
|
|
406
|
+
taskType: 'task',
|
|
407
|
+
outcome: 'failure',
|
|
408
|
+
errorType: 'retriable',
|
|
409
|
+
},
|
|
410
|
+
brain,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(pattern!.mitigation).toContain('Retry with exponential backoff');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('builds reassign suggestion for permanent errors', async () => {
|
|
417
|
+
const brain = await getBrainAccessor(tempDir);
|
|
418
|
+
|
|
419
|
+
const pattern = await _recordFailurePatternWithAccessor(
|
|
420
|
+
{
|
|
421
|
+
agentId: 'agt_1',
|
|
422
|
+
agentType: 'executor',
|
|
423
|
+
taskId: 'T001',
|
|
424
|
+
taskType: 'task',
|
|
425
|
+
outcome: 'failure',
|
|
426
|
+
errorType: 'permanent',
|
|
427
|
+
},
|
|
428
|
+
brain,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(pattern!.mitigation).toContain('Reassign task to a different agent type');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ==========================================================================
|
|
436
|
+
// storeHealingStrategy
|
|
437
|
+
// ==========================================================================
|
|
438
|
+
|
|
439
|
+
describe('storeHealingStrategy', () => {
|
|
440
|
+
it('returns null for non-failure events', async () => {
|
|
441
|
+
const event: AgentExecutionEvent = {
|
|
442
|
+
agentId: 'agt_1',
|
|
443
|
+
agentType: 'executor',
|
|
444
|
+
taskId: 'T001',
|
|
445
|
+
taskType: 'task',
|
|
446
|
+
outcome: 'success',
|
|
447
|
+
};
|
|
448
|
+
const result = await storeHealingStrategy(event, 'some strategy', tempDir);
|
|
449
|
+
expect(result).toBeNull();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('stores a change observation with healing narrative', async () => {
|
|
453
|
+
const brain = await getBrainAccessor(tempDir);
|
|
454
|
+
|
|
455
|
+
const event: AgentExecutionEvent = {
|
|
456
|
+
agentId: 'agt_1',
|
|
457
|
+
agentType: 'executor',
|
|
458
|
+
taskId: 'T001',
|
|
459
|
+
taskType: 'task',
|
|
460
|
+
outcome: 'failure',
|
|
461
|
+
errorType: 'permanent',
|
|
462
|
+
sessionId: 'ses_test_abc',
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const obs = await _storeHealingStrategyWithAccessor(
|
|
466
|
+
event,
|
|
467
|
+
'Switch to validator agent',
|
|
468
|
+
brain,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(obs).not.toBeNull();
|
|
472
|
+
expect(obs!.id).toMatch(/^O-agt-/);
|
|
473
|
+
expect(obs!.type).toBe('change');
|
|
474
|
+
expect(obs!.sourceType).toBe('agent');
|
|
475
|
+
expect(obs!.narrative).toBe('Switch to validator agent');
|
|
476
|
+
expect(obs!.title).toContain('executor');
|
|
477
|
+
expect(obs!.title).toContain('task');
|
|
478
|
+
expect(obs!.sourceSessionId).toBe('ses_test_abc');
|
|
479
|
+
|
|
480
|
+
const facts = JSON.parse(obs!.factsJson ?? '[]') as string[];
|
|
481
|
+
expect(facts.some((f) => f.includes('executor'))).toBe(true);
|
|
482
|
+
expect(facts.some((f) => f.includes('Switch to validator agent'))).toBe(true);
|
|
483
|
+
|
|
484
|
+
const concepts = JSON.parse(obs!.conceptsJson ?? '[]') as string[];
|
|
485
|
+
expect(concepts).toContain('self-healing');
|
|
486
|
+
expect(concepts).toContain('executor');
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// ==========================================================================
|
|
491
|
+
// getSelfHealingSuggestions
|
|
492
|
+
// ==========================================================================
|
|
493
|
+
|
|
494
|
+
describe('getSelfHealingSuggestions', () => {
|
|
495
|
+
it('returns empty array when no failure patterns exist', async () => {
|
|
496
|
+
const suggestions = await getSelfHealingSuggestions('executor', 'task', tempDir);
|
|
497
|
+
expect(suggestions).toEqual([]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('returns suggestions matching agentType and taskType', async () => {
|
|
501
|
+
const brain = await getBrainAccessor(tempDir);
|
|
502
|
+
|
|
503
|
+
// Record three failures to build up a pattern
|
|
504
|
+
const event: AgentExecutionEvent = {
|
|
505
|
+
agentId: 'agt_1',
|
|
506
|
+
agentType: 'researcher',
|
|
507
|
+
taskId: 'T001',
|
|
508
|
+
taskType: 'epic',
|
|
509
|
+
outcome: 'failure',
|
|
510
|
+
errorType: 'retriable',
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
for (let i = 0; i < 3; i++) {
|
|
514
|
+
await _recordFailurePatternWithAccessor({ ...event, taskId: `T00${i}` }, brain);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const suggestions = await _getSelfHealingSuggestionsWithAccessor('researcher', 'epic', brain);
|
|
518
|
+
|
|
519
|
+
expect(suggestions.length).toBeGreaterThanOrEqual(1);
|
|
520
|
+
expect(suggestions[0]!.frequency).toBe(3);
|
|
521
|
+
expect(suggestions[0]!.failurePattern).toContain('researcher');
|
|
522
|
+
expect(suggestions[0]!.failurePattern).toContain('epic');
|
|
523
|
+
expect(suggestions[0]!.suggestion).toBeTruthy();
|
|
524
|
+
expect(suggestions[0]!.confidence).toBeGreaterThan(0);
|
|
525
|
+
expect(suggestions[0]!.confidence).toBeLessThanOrEqual(0.9);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('does not return suggestions for a different agent type', async () => {
|
|
529
|
+
const brain = await getBrainAccessor(tempDir);
|
|
530
|
+
|
|
531
|
+
await _recordFailurePatternWithAccessor(
|
|
532
|
+
{
|
|
533
|
+
agentId: 'agt_1',
|
|
534
|
+
agentType: 'executor',
|
|
535
|
+
taskId: 'T001',
|
|
536
|
+
taskType: 'task',
|
|
537
|
+
outcome: 'failure',
|
|
538
|
+
errorType: 'permanent',
|
|
539
|
+
},
|
|
540
|
+
brain,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Query for a different agent type
|
|
544
|
+
const suggestions = await _getSelfHealingSuggestionsWithAccessor('researcher', 'task', brain);
|
|
545
|
+
expect(suggestions).toEqual([]);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('orders suggestions by frequency descending', async () => {
|
|
549
|
+
const brain = await getBrainAccessor(tempDir);
|
|
550
|
+
|
|
551
|
+
// Build retriable pattern with high frequency
|
|
552
|
+
for (let i = 0; i < 5; i++) {
|
|
553
|
+
await _recordFailurePatternWithAccessor(
|
|
554
|
+
{
|
|
555
|
+
agentId: 'agt_1',
|
|
556
|
+
agentType: 'validator',
|
|
557
|
+
taskId: `T${i}`,
|
|
558
|
+
taskType: 'task',
|
|
559
|
+
outcome: 'failure',
|
|
560
|
+
errorType: 'retriable',
|
|
561
|
+
},
|
|
562
|
+
brain,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Build permanent pattern with lower frequency
|
|
567
|
+
await _recordFailurePatternWithAccessor(
|
|
568
|
+
{
|
|
569
|
+
agentId: 'agt_1',
|
|
570
|
+
agentType: 'validator',
|
|
571
|
+
taskId: 'T99',
|
|
572
|
+
taskType: 'task',
|
|
573
|
+
outcome: 'failure',
|
|
574
|
+
errorType: 'permanent',
|
|
575
|
+
},
|
|
576
|
+
brain,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const suggestions = await _getSelfHealingSuggestionsWithAccessor('validator', 'task', brain);
|
|
580
|
+
|
|
581
|
+
expect(suggestions.length).toBe(2);
|
|
582
|
+
// Higher frequency should come first
|
|
583
|
+
expect(suggestions[0]!.frequency).toBeGreaterThan(suggestions[1]!.frequency);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ==========================================================================
|
|
588
|
+
// processAgentLifecycleEvent
|
|
589
|
+
// ==========================================================================
|
|
590
|
+
|
|
591
|
+
describe('processAgentLifecycleEvent', () => {
|
|
592
|
+
it('records decision and returns decisionId for success events', async () => {
|
|
593
|
+
const event: AgentExecutionEvent = {
|
|
594
|
+
agentId: 'agt_1',
|
|
595
|
+
agentType: 'executor',
|
|
596
|
+
taskId: 'T001',
|
|
597
|
+
taskType: 'task',
|
|
598
|
+
outcome: 'success',
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const result = await processAgentLifecycleEvent(event, tempDir);
|
|
602
|
+
|
|
603
|
+
expect(result.decisionId).toMatch(/^AGT-/);
|
|
604
|
+
expect(result.patternId).toBeNull(); // no failure pattern for success
|
|
605
|
+
expect(result.observationId).toBeNull();
|
|
606
|
+
expect(result.healingSuggestions).toEqual([]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('records decision + pattern for failure events', async () => {
|
|
610
|
+
const event: AgentExecutionEvent = {
|
|
611
|
+
agentId: 'agt_1',
|
|
612
|
+
agentType: 'researcher',
|
|
613
|
+
taskId: 'T002',
|
|
614
|
+
taskType: 'epic',
|
|
615
|
+
outcome: 'failure',
|
|
616
|
+
errorType: 'permanent',
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const result = await processAgentLifecycleEvent(event, tempDir);
|
|
620
|
+
|
|
621
|
+
expect(result.decisionId).toMatch(/^AGT-/);
|
|
622
|
+
expect(result.patternId).toMatch(/^P-agt-/);
|
|
623
|
+
expect(result.observationId).toBeNull(); // frequency = 1, threshold not met
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('records healing observation once pattern frequency reaches 3', async () => {
|
|
627
|
+
const event: AgentExecutionEvent = {
|
|
628
|
+
agentId: 'agt_1',
|
|
629
|
+
agentType: 'executor',
|
|
630
|
+
taskId: 'T001',
|
|
631
|
+
taskType: 'subtask',
|
|
632
|
+
outcome: 'failure',
|
|
633
|
+
errorType: 'retriable',
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// First two calls — observation not stored yet
|
|
637
|
+
await processAgentLifecycleEvent({ ...event, taskId: 'T001' }, tempDir);
|
|
638
|
+
const second = await processAgentLifecycleEvent({ ...event, taskId: 'T002' }, tempDir);
|
|
639
|
+
expect(second.observationId).toBeNull();
|
|
640
|
+
|
|
641
|
+
// Third call — threshold reached
|
|
642
|
+
const third = await processAgentLifecycleEvent({ ...event, taskId: 'T003' }, tempDir);
|
|
643
|
+
expect(third.observationId).toMatch(/^O-agt-/);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('returns healing suggestions for failure events when patterns exist', async () => {
|
|
647
|
+
const event: AgentExecutionEvent = {
|
|
648
|
+
agentId: 'agt_1',
|
|
649
|
+
agentType: 'orchestrator',
|
|
650
|
+
taskId: 'T001',
|
|
651
|
+
taskType: 'epic',
|
|
652
|
+
outcome: 'failure',
|
|
653
|
+
errorType: 'unknown',
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Build up frequency
|
|
657
|
+
await processAgentLifecycleEvent({ ...event, taskId: 'T001' }, tempDir);
|
|
658
|
+
await processAgentLifecycleEvent({ ...event, taskId: 'T002' }, tempDir);
|
|
659
|
+
const result = await processAgentLifecycleEvent({ ...event, taskId: 'T003' }, tempDir);
|
|
660
|
+
|
|
661
|
+
expect(result.healingSuggestions.length).toBeGreaterThanOrEqual(1);
|
|
662
|
+
expect(result.healingSuggestions[0]!.frequency).toBeGreaterThanOrEqual(3);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('is entirely best-effort — does not throw when brain.db unavailable', async () => {
|
|
666
|
+
// bad cwd -> brain.db open will fail silently
|
|
667
|
+
const event: AgentExecutionEvent = {
|
|
668
|
+
agentId: 'agt_1',
|
|
669
|
+
agentType: 'executor',
|
|
670
|
+
taskId: 'T001',
|
|
671
|
+
taskType: 'task',
|
|
672
|
+
outcome: 'failure',
|
|
673
|
+
errorType: 'retriable',
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
await expect(processAgentLifecycleEvent(event, '/nonexistent/xyz')).resolves.toEqual({
|
|
677
|
+
decisionId: null,
|
|
678
|
+
patternId: null,
|
|
679
|
+
observationId: null,
|
|
680
|
+
healingSuggestions: [],
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
});
|