@soleri/core 2.8.0 → 2.9.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/extensions/index.d.ts +3 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/middleware.d.ts +13 -0
- package/dist/extensions/middleware.d.ts.map +1 -0
- package/dist/extensions/middleware.js +47 -0
- package/dist/extensions/middleware.js.map +1 -0
- package/dist/extensions/types.d.ts +64 -0
- package/dist/extensions/types.d.ts.map +1 -0
- package/dist/extensions/types.js +2 -0
- package/dist/extensions/types.js.map +1 -0
- package/dist/index.d.ts +8 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -16
- package/dist/index.js.map +1 -1
- package/dist/runtime/core-ops.d.ts +1 -1
- package/dist/runtime/core-ops.js +1 -1
- package/dist/runtime/facades/admin-facade.d.ts +8 -0
- package/dist/runtime/facades/admin-facade.d.ts.map +1 -0
- package/dist/runtime/facades/admin-facade.js +90 -0
- package/dist/runtime/facades/admin-facade.js.map +1 -0
- package/dist/runtime/facades/brain-facade.d.ts +8 -0
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -0
- package/dist/runtime/facades/brain-facade.js +294 -0
- package/dist/runtime/facades/brain-facade.js.map +1 -0
- package/dist/runtime/facades/cognee-facade.d.ts +8 -0
- package/dist/runtime/facades/cognee-facade.d.ts.map +1 -0
- package/dist/runtime/facades/cognee-facade.js +154 -0
- package/dist/runtime/facades/cognee-facade.js.map +1 -0
- package/dist/runtime/facades/control-facade.d.ts +8 -0
- package/dist/runtime/facades/control-facade.d.ts.map +1 -0
- package/dist/runtime/facades/control-facade.js +244 -0
- package/dist/runtime/facades/control-facade.js.map +1 -0
- package/dist/runtime/facades/curator-facade.d.ts +8 -0
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -0
- package/dist/runtime/facades/curator-facade.js +117 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts +10 -0
- package/dist/runtime/facades/index.d.ts.map +1 -0
- package/dist/runtime/facades/index.js +71 -0
- package/dist/runtime/facades/index.js.map +1 -0
- package/dist/runtime/facades/loop-facade.d.ts +8 -0
- package/dist/runtime/facades/loop-facade.d.ts.map +1 -0
- package/dist/runtime/facades/loop-facade.js +9 -0
- package/dist/runtime/facades/loop-facade.js.map +1 -0
- package/dist/runtime/facades/memory-facade.d.ts +8 -0
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -0
- package/dist/runtime/facades/memory-facade.js +108 -0
- package/dist/runtime/facades/memory-facade.js.map +1 -0
- package/dist/runtime/facades/orchestrate-facade.d.ts +8 -0
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -0
- package/dist/runtime/facades/orchestrate-facade.js +58 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -0
- package/dist/runtime/facades/plan-facade.d.ts +8 -0
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -0
- package/dist/runtime/facades/plan-facade.js +110 -0
- package/dist/runtime/facades/plan-facade.js.map +1 -0
- package/dist/runtime/facades/vault-facade.d.ts +8 -0
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -0
- package/dist/runtime/facades/vault-facade.js +194 -0
- package/dist/runtime/facades/vault-facade.js.map +1 -0
- package/dist/runtime/vault-extra-ops.d.ts +2 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +37 -2
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- package/dist/streams/index.d.ts +4 -0
- package/dist/streams/index.d.ts.map +1 -0
- package/dist/streams/index.js +3 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/normalize.d.ts +14 -0
- package/dist/streams/normalize.d.ts.map +1 -0
- package/dist/streams/normalize.js +43 -0
- package/dist/streams/normalize.js.map +1 -0
- package/dist/streams/replayable-stream.d.ts +19 -0
- package/dist/streams/replayable-stream.d.ts.map +1 -0
- package/dist/streams/replayable-stream.js +90 -0
- package/dist/streams/replayable-stream.js.map +1 -0
- package/dist/vault/content-hash.d.ts +16 -0
- package/dist/vault/content-hash.d.ts.map +1 -0
- package/dist/vault/content-hash.js +21 -0
- package/dist/vault/content-hash.js.map +1 -0
- package/dist/vault/vault.d.ts +9 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +49 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/content-hash.test.ts +60 -0
- package/src/__tests__/core-ops.test.ts +10 -7
- package/src/__tests__/extensions.test.ts +233 -0
- package/src/__tests__/grading-ops.test.ts +2 -2
- package/src/__tests__/memory-cross-project-ops.test.ts +2 -2
- package/src/__tests__/normalize.test.ts +75 -0
- package/src/__tests__/playbook.test.ts +4 -4
- package/src/__tests__/replayable-stream.test.ts +66 -0
- package/src/__tests__/vault-extra-ops.test.ts +1 -1
- package/src/__tests__/vault.test.ts +72 -0
- package/src/extensions/index.ts +2 -0
- package/src/extensions/middleware.ts +53 -0
- package/src/extensions/types.ts +64 -0
- package/src/index.ts +14 -17
- package/src/runtime/facades/admin-facade.ts +101 -0
- package/src/runtime/facades/brain-facade.ts +331 -0
- package/src/runtime/facades/cognee-facade.ts +162 -0
- package/src/runtime/facades/control-facade.ts +279 -0
- package/src/runtime/facades/curator-facade.ts +132 -0
- package/src/runtime/facades/index.ts +74 -0
- package/src/runtime/facades/loop-facade.ts +12 -0
- package/src/runtime/facades/memory-facade.ts +114 -0
- package/src/runtime/facades/orchestrate-facade.ts +68 -0
- package/src/runtime/facades/plan-facade.ts +119 -0
- package/src/runtime/facades/vault-facade.ts +223 -0
- package/src/runtime/vault-extra-ops.ts +38 -2
- package/src/streams/index.ts +3 -0
- package/src/streams/normalize.ts +56 -0
- package/src/streams/replayable-stream.ts +92 -0
- package/src/vault/content-hash.ts +31 -0
- package/src/vault/vault.ts +73 -3
- package/src/runtime/core-ops.ts +0 -1443
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { wrapWithMiddleware } from '../extensions/middleware.js';
|
|
3
|
+
import type { FacadeConfig } from '../facades/types.js';
|
|
4
|
+
import type { OpMiddleware, AgentExtensions } from '../extensions/types.js';
|
|
5
|
+
|
|
6
|
+
describe('extensions', () => {
|
|
7
|
+
describe('AgentExtensions type', () => {
|
|
8
|
+
it('should accept empty extensions', () => {
|
|
9
|
+
const ext: AgentExtensions = {};
|
|
10
|
+
expect(ext.ops).toBeUndefined();
|
|
11
|
+
expect(ext.facades).toBeUndefined();
|
|
12
|
+
expect(ext.middleware).toBeUndefined();
|
|
13
|
+
expect(ext.hooks).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should accept extensions with ops', () => {
|
|
17
|
+
const ext: AgentExtensions = {
|
|
18
|
+
ops: [
|
|
19
|
+
{
|
|
20
|
+
name: 'custom_op',
|
|
21
|
+
description: 'A custom op',
|
|
22
|
+
auth: 'read',
|
|
23
|
+
handler: async () => ({ ok: true }),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
expect(ext.ops).toHaveLength(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should accept extensions with facades', () => {
|
|
31
|
+
const ext: AgentExtensions = {
|
|
32
|
+
facades: [
|
|
33
|
+
{
|
|
34
|
+
name: 'my_facade',
|
|
35
|
+
description: 'Custom facade',
|
|
36
|
+
ops: [
|
|
37
|
+
{
|
|
38
|
+
name: 'do_thing',
|
|
39
|
+
description: 'Does a thing',
|
|
40
|
+
auth: 'write',
|
|
41
|
+
handler: async () => ({ done: true }),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
expect(ext.facades).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('wrapWithMiddleware', () => {
|
|
52
|
+
it('should wrap facade ops with before middleware', async () => {
|
|
53
|
+
const calls: string[] = [];
|
|
54
|
+
const facade: FacadeConfig = {
|
|
55
|
+
name: 'test',
|
|
56
|
+
description: 'Test facade',
|
|
57
|
+
ops: [
|
|
58
|
+
{
|
|
59
|
+
name: 'greet',
|
|
60
|
+
description: 'Say hello',
|
|
61
|
+
auth: 'read',
|
|
62
|
+
handler: async (params) => {
|
|
63
|
+
calls.push('handler');
|
|
64
|
+
return { message: `Hello ${params.name}` };
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mw: OpMiddleware = {
|
|
71
|
+
name: 'logger',
|
|
72
|
+
before: async (ctx) => {
|
|
73
|
+
calls.push(`before:${ctx.op}`);
|
|
74
|
+
return ctx.params;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
wrapWithMiddleware([facade], [mw]);
|
|
79
|
+
const result = await facade.ops[0].handler({ name: 'World' });
|
|
80
|
+
|
|
81
|
+
expect(calls).toEqual(['before:greet', 'handler']);
|
|
82
|
+
expect(result).toEqual({ message: 'Hello World' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should wrap facade ops with after middleware', async () => {
|
|
86
|
+
const facade: FacadeConfig = {
|
|
87
|
+
name: 'test',
|
|
88
|
+
description: 'Test facade',
|
|
89
|
+
ops: [
|
|
90
|
+
{
|
|
91
|
+
name: 'greet',
|
|
92
|
+
description: 'Say hello',
|
|
93
|
+
auth: 'read',
|
|
94
|
+
handler: async () => ({ message: 'Hello' }),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mw: OpMiddleware = {
|
|
100
|
+
name: 'enricher',
|
|
101
|
+
after: async (ctx) => {
|
|
102
|
+
const data = ctx.result as Record<string, unknown>;
|
|
103
|
+
return { ...data, enriched: true };
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
wrapWithMiddleware([facade], [mw]);
|
|
108
|
+
const result = await facade.ops[0].handler({});
|
|
109
|
+
expect(result).toEqual({ message: 'Hello', enriched: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should chain multiple middleware in order', async () => {
|
|
113
|
+
const order: string[] = [];
|
|
114
|
+
const facade: FacadeConfig = {
|
|
115
|
+
name: 'test',
|
|
116
|
+
description: 'Test',
|
|
117
|
+
ops: [
|
|
118
|
+
{
|
|
119
|
+
name: 'op1',
|
|
120
|
+
description: 'Op',
|
|
121
|
+
auth: 'read',
|
|
122
|
+
handler: async () => {
|
|
123
|
+
order.push('handler');
|
|
124
|
+
return { v: 1 };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const mw1: OpMiddleware = {
|
|
131
|
+
name: 'first',
|
|
132
|
+
before: async (ctx) => {
|
|
133
|
+
order.push('first:before');
|
|
134
|
+
return ctx.params;
|
|
135
|
+
},
|
|
136
|
+
after: async (ctx) => {
|
|
137
|
+
order.push('first:after');
|
|
138
|
+
return ctx.result;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const mw2: OpMiddleware = {
|
|
142
|
+
name: 'second',
|
|
143
|
+
before: async (ctx) => {
|
|
144
|
+
order.push('second:before');
|
|
145
|
+
return ctx.params;
|
|
146
|
+
},
|
|
147
|
+
after: async (ctx) => {
|
|
148
|
+
order.push('second:after');
|
|
149
|
+
return ctx.result;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
wrapWithMiddleware([facade], [mw1, mw2]);
|
|
154
|
+
await facade.ops[0].handler({});
|
|
155
|
+
|
|
156
|
+
expect(order).toEqual([
|
|
157
|
+
'first:before',
|
|
158
|
+
'second:before',
|
|
159
|
+
'handler',
|
|
160
|
+
'second:after',
|
|
161
|
+
'first:after',
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should allow before middleware to modify params', async () => {
|
|
166
|
+
const facade: FacadeConfig = {
|
|
167
|
+
name: 'test',
|
|
168
|
+
description: 'Test',
|
|
169
|
+
ops: [
|
|
170
|
+
{
|
|
171
|
+
name: 'echo',
|
|
172
|
+
description: 'Echo',
|
|
173
|
+
auth: 'read',
|
|
174
|
+
handler: async (params) => params,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const mw: OpMiddleware = {
|
|
180
|
+
name: 'injector',
|
|
181
|
+
before: async (ctx) => ({ ...ctx.params, injected: true }),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
wrapWithMiddleware([facade], [mw]);
|
|
185
|
+
const result = await facade.ops[0].handler({ original: true });
|
|
186
|
+
expect(result).toEqual({ original: true, injected: true });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle empty middleware array (no-op)', async () => {
|
|
190
|
+
const facade: FacadeConfig = {
|
|
191
|
+
name: 'test',
|
|
192
|
+
description: 'Test',
|
|
193
|
+
ops: [
|
|
194
|
+
{
|
|
195
|
+
name: 'op',
|
|
196
|
+
description: 'Op',
|
|
197
|
+
auth: 'read',
|
|
198
|
+
handler: async () => ({ ok: true }),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
wrapWithMiddleware([facade], []);
|
|
204
|
+
const result = await facade.ops[0].handler({});
|
|
205
|
+
expect(result).toEqual({ ok: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should propagate middleware errors', async () => {
|
|
209
|
+
const facade: FacadeConfig = {
|
|
210
|
+
name: 'test',
|
|
211
|
+
description: 'Test',
|
|
212
|
+
ops: [
|
|
213
|
+
{
|
|
214
|
+
name: 'op',
|
|
215
|
+
description: 'Op',
|
|
216
|
+
auth: 'read',
|
|
217
|
+
handler: async () => ({ ok: true }),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const mw: OpMiddleware = {
|
|
223
|
+
name: 'blocker',
|
|
224
|
+
before: async () => {
|
|
225
|
+
throw new Error('Blocked by policy');
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
wrapWithMiddleware([facade], [mw]);
|
|
230
|
+
await expect(facade.ops[0].handler({})).rejects.toThrow('Blocked by policy');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
6
|
-
import {
|
|
6
|
+
import { createSemanticFacades } from '../runtime/facades/index.js';
|
|
7
7
|
import type { AgentRuntime } from '../runtime/types.js';
|
|
8
8
|
import type { OpDefinition } from '../facades/types.js';
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ describe('Grading Ops', () => {
|
|
|
20
20
|
vaultPath: ':memory:',
|
|
21
21
|
plansPath: join(plannerDir, 'plans.json'),
|
|
22
22
|
});
|
|
23
|
-
ops =
|
|
23
|
+
ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
afterEach(() => {
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
6
|
-
import {
|
|
6
|
+
import { createSemanticFacades } from '../runtime/facades/index.js';
|
|
7
7
|
import type { AgentRuntime } from '../runtime/types.js';
|
|
8
8
|
import type { OpDefinition } from '../facades/types.js';
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ describe('Memory Cross-Project Ops', () => {
|
|
|
20
20
|
vaultPath: ':memory:',
|
|
21
21
|
plansPath: join(tempDir, 'plans.json'),
|
|
22
22
|
});
|
|
23
|
-
ops =
|
|
23
|
+
ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
afterEach(() => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalize, collect } from '../streams/normalize.js';
|
|
3
|
+
|
|
4
|
+
describe('normalize', () => {
|
|
5
|
+
it('passes through a single value', async () => {
|
|
6
|
+
expect(await collect(normalize(42))).toEqual([42]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('flattens an array', async () => {
|
|
10
|
+
expect(await collect(normalize([1, 2, 3]))).toEqual([1, 2, 3]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('resolves a promise', async () => {
|
|
14
|
+
expect(await collect(normalize(Promise.resolve(99)))).toEqual([99]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('resolves a promise of array', async () => {
|
|
18
|
+
expect(await collect(normalize(Promise.resolve([4, 5])))).toEqual([4, 5]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('consumes an async iterable', async () => {
|
|
22
|
+
async function* gen() {
|
|
23
|
+
yield 'a';
|
|
24
|
+
yield 'b';
|
|
25
|
+
}
|
|
26
|
+
expect(await collect(normalize(gen()))).toEqual(['a', 'b']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('flattens nested arrays', async () => {
|
|
30
|
+
const input = [
|
|
31
|
+
[1, 2],
|
|
32
|
+
[3, [4, 5]],
|
|
33
|
+
];
|
|
34
|
+
expect(await collect(normalize(input))).toEqual([1, 2, 3, 4, 5]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('handles mixed nesting', async () => {
|
|
38
|
+
async function* gen() {
|
|
39
|
+
yield 30;
|
|
40
|
+
}
|
|
41
|
+
const input = [10, Promise.resolve(20), gen()];
|
|
42
|
+
expect(await collect(normalize(input))).toEqual([10, 20, 30]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('handles empty array', async () => {
|
|
46
|
+
expect(await collect(normalize([]))).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles string as leaf (not iterable)', async () => {
|
|
50
|
+
expect(await collect(normalize('hello'))).toEqual(['hello']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('handles deeply nested structure', async () => {
|
|
54
|
+
const input = [[[1]], [[2, [3]]]];
|
|
55
|
+
expect(await collect(normalize(input))).toEqual([1, 2, 3]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('collect', () => {
|
|
60
|
+
it('collects async iterable to array', async () => {
|
|
61
|
+
async function* gen() {
|
|
62
|
+
yield 1;
|
|
63
|
+
yield 2;
|
|
64
|
+
yield 3;
|
|
65
|
+
}
|
|
66
|
+
expect(await collect(gen())).toEqual([1, 2, 3]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns empty for empty iterable', async () => {
|
|
70
|
+
async function* gen() {
|
|
71
|
+
/* empty */
|
|
72
|
+
}
|
|
73
|
+
expect(await collect(gen())).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -7,7 +7,7 @@ import { validatePlaybook, parsePlaybookFromEntry } from '../vault/playbook.js';
|
|
|
7
7
|
import type { Playbook } from '../vault/playbook.js';
|
|
8
8
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
9
9
|
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createSemanticFacades } from '../runtime/facades/index.js';
|
|
11
11
|
import type { AgentRuntime, OpDefinition } from '../runtime/types.js';
|
|
12
12
|
|
|
13
13
|
function makePlaybook(overrides: Partial<Playbook> = {}): Playbook {
|
|
@@ -189,7 +189,7 @@ describe('playbook_create op', () => {
|
|
|
189
189
|
vaultPath: ':memory:',
|
|
190
190
|
plansPath: join(plannerDir, 'plans.json'),
|
|
191
191
|
});
|
|
192
|
-
ops =
|
|
192
|
+
ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
afterEach(() => {
|
|
@@ -300,7 +300,7 @@ describe('playbook_match op', () => {
|
|
|
300
300
|
vaultPath: ':memory:',
|
|
301
301
|
plansPath: join(plannerDir, 'plans.json'),
|
|
302
302
|
});
|
|
303
|
-
ops =
|
|
303
|
+
ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
306
|
afterEach(() => {
|
|
@@ -356,7 +356,7 @@ describe('playbook_seed op', () => {
|
|
|
356
356
|
vaultPath: ':memory:',
|
|
357
357
|
plansPath: join(plannerDir, 'plans.json'),
|
|
358
358
|
});
|
|
359
|
-
ops =
|
|
359
|
+
ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
|
|
360
360
|
});
|
|
361
361
|
|
|
362
362
|
afterEach(() => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ReplayableStream } from '../streams/replayable-stream.js';
|
|
3
|
+
|
|
4
|
+
async function* generate(items: number[]): AsyncIterable<number> {
|
|
5
|
+
for (const item of items) yield item;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let sourceCallCount = 0;
|
|
9
|
+
async function* trackedGenerate(items: number[]): AsyncIterable<number> {
|
|
10
|
+
sourceCallCount++;
|
|
11
|
+
for (const item of items) yield item;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('ReplayableStream', () => {
|
|
15
|
+
it('single consumer iterates all items', async () => {
|
|
16
|
+
const stream = new ReplayableStream(generate([1, 2, 3]));
|
|
17
|
+
const result = await stream.collect();
|
|
18
|
+
expect(result).toEqual([1, 2, 3]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('multiple consumers each see full stream', async () => {
|
|
22
|
+
const stream = new ReplayableStream(generate([10, 20, 30]));
|
|
23
|
+
const a = stream.collect();
|
|
24
|
+
const b = stream.collect();
|
|
25
|
+
expect(await a).toEqual([10, 20, 30]);
|
|
26
|
+
expect(await b).toEqual([10, 20, 30]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('source executes exactly once', async () => {
|
|
30
|
+
sourceCallCount = 0;
|
|
31
|
+
const stream = new ReplayableStream(trackedGenerate([1, 2]));
|
|
32
|
+
await stream.collect();
|
|
33
|
+
await stream.collect();
|
|
34
|
+
await stream.collect();
|
|
35
|
+
expect(sourceCallCount).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('bufferedCount tracks buffer size', async () => {
|
|
39
|
+
const stream = new ReplayableStream(generate([1, 2, 3]));
|
|
40
|
+
expect(stream.bufferedCount).toBe(0);
|
|
41
|
+
await stream.collect();
|
|
42
|
+
expect(stream.bufferedCount).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('empty stream returns empty array', async () => {
|
|
46
|
+
const stream = new ReplayableStream(generate([]));
|
|
47
|
+
expect(await stream.collect()).toEqual([]);
|
|
48
|
+
expect(stream.isDone).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('error propagates to all consumers', async () => {
|
|
52
|
+
async function* failing(): AsyncIterable<number> {
|
|
53
|
+
yield 1;
|
|
54
|
+
throw new Error('source failed');
|
|
55
|
+
}
|
|
56
|
+
const stream = new ReplayableStream(failing());
|
|
57
|
+
await expect(stream.collect()).rejects.toThrow('source failed');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('for-await-of works', async () => {
|
|
61
|
+
const stream = new ReplayableStream(generate([5, 6]));
|
|
62
|
+
const items: number[] = [];
|
|
63
|
+
for await (const item of stream) items.push(item);
|
|
64
|
+
expect(items).toEqual([5, 6]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { Vault } from '../vault/vault.js';
|
|
3
|
+
import { computeContentHash } from '../vault/content-hash.js';
|
|
3
4
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
4
5
|
|
|
5
6
|
function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
|
|
@@ -675,4 +676,75 @@ describe('Vault', () => {
|
|
|
675
676
|
v.close();
|
|
676
677
|
});
|
|
677
678
|
});
|
|
679
|
+
|
|
680
|
+
describe('Content-addressable hashing', () => {
|
|
681
|
+
it('seed populates content_hash', () => {
|
|
682
|
+
vault.seed([
|
|
683
|
+
{
|
|
684
|
+
id: 'ch-1',
|
|
685
|
+
type: 'pattern',
|
|
686
|
+
domain: 'test',
|
|
687
|
+
title: 'Hash test',
|
|
688
|
+
severity: 'warning',
|
|
689
|
+
description: 'Desc',
|
|
690
|
+
tags: ['a'],
|
|
691
|
+
},
|
|
692
|
+
]);
|
|
693
|
+
const hash = computeContentHash({
|
|
694
|
+
type: 'pattern',
|
|
695
|
+
domain: 'test',
|
|
696
|
+
title: 'Hash test',
|
|
697
|
+
description: 'Desc',
|
|
698
|
+
tags: ['a'],
|
|
699
|
+
});
|
|
700
|
+
expect(vault.findByContentHash(hash)).toBe('ch-1');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('findByContentHash returns null for unknown hash', () => {
|
|
704
|
+
expect(vault.findByContentHash('0000000000000000000000000000000000000000')).toBeNull();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('contentHashStats returns correct counts', () => {
|
|
708
|
+
vault.seed([
|
|
709
|
+
{
|
|
710
|
+
id: 'hs-1',
|
|
711
|
+
type: 'pattern',
|
|
712
|
+
domain: 'd',
|
|
713
|
+
title: 'T1',
|
|
714
|
+
severity: 'warning',
|
|
715
|
+
description: 'D1',
|
|
716
|
+
tags: ['a'],
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
id: 'hs-2',
|
|
720
|
+
type: 'rule',
|
|
721
|
+
domain: 'd',
|
|
722
|
+
title: 'T2',
|
|
723
|
+
severity: 'warning',
|
|
724
|
+
description: 'D2',
|
|
725
|
+
tags: ['b'],
|
|
726
|
+
},
|
|
727
|
+
]);
|
|
728
|
+
const stats = vault.contentHashStats();
|
|
729
|
+
expect(stats.total).toBeGreaterThanOrEqual(2);
|
|
730
|
+
expect(stats.hashed).toBe(stats.total);
|
|
731
|
+
expect(stats.uniqueHashes).toBe(stats.total);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('backfill hashes existing entries on re-initialize', () => {
|
|
735
|
+
vault.seed([
|
|
736
|
+
{
|
|
737
|
+
id: 'bf-1',
|
|
738
|
+
type: 'pattern',
|
|
739
|
+
domain: 'd',
|
|
740
|
+
title: 'Backfill',
|
|
741
|
+
severity: 'warning',
|
|
742
|
+
description: 'D',
|
|
743
|
+
tags: [],
|
|
744
|
+
},
|
|
745
|
+
]);
|
|
746
|
+
const stats = vault.contentHashStats();
|
|
747
|
+
expect(stats.hashed).toBe(stats.total);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
678
750
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FacadeConfig } from '../facades/types.js';
|
|
2
|
+
import type { OpMiddleware } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap all ops in the given facades with middleware.
|
|
6
|
+
*
|
|
7
|
+
* Middleware chain follows the onion model:
|
|
8
|
+
* - before hooks: first middleware → last middleware → handler
|
|
9
|
+
* - after hooks: last middleware → first middleware (reverse)
|
|
10
|
+
*
|
|
11
|
+
* This mutates the facade ops in-place (replaces handlers).
|
|
12
|
+
*/
|
|
13
|
+
export function wrapWithMiddleware(facades: FacadeConfig[], middleware: OpMiddleware[]): void {
|
|
14
|
+
if (middleware.length === 0) return;
|
|
15
|
+
|
|
16
|
+
for (const facade of facades) {
|
|
17
|
+
for (const op of facade.ops) {
|
|
18
|
+
const originalHandler = op.handler;
|
|
19
|
+
|
|
20
|
+
op.handler = async (params: Record<string, unknown>) => {
|
|
21
|
+
// Run before hooks (first → last)
|
|
22
|
+
let currentParams = params;
|
|
23
|
+
for (const mw of middleware) {
|
|
24
|
+
if (mw.before) {
|
|
25
|
+
currentParams = await mw.before({
|
|
26
|
+
facade: facade.name,
|
|
27
|
+
op: op.name,
|
|
28
|
+
params: currentParams,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Run original handler
|
|
34
|
+
let result = await originalHandler(currentParams);
|
|
35
|
+
|
|
36
|
+
// Run after hooks (last → first)
|
|
37
|
+
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
38
|
+
const mw = middleware[i];
|
|
39
|
+
if (mw.after) {
|
|
40
|
+
result = await mw.after({
|
|
41
|
+
facade: facade.name,
|
|
42
|
+
op: op.name,
|
|
43
|
+
params: currentParams,
|
|
44
|
+
result,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { OpDefinition, FacadeConfig } from '../facades/types.js';
|
|
2
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware that wraps op execution with before/after hooks.
|
|
6
|
+
*
|
|
7
|
+
* - `before` runs before the op handler. Return modified params or throw to reject.
|
|
8
|
+
* - `after` runs after the op handler. Return modified result or throw.
|
|
9
|
+
*
|
|
10
|
+
* Multiple middleware are chained: before hooks run first→last,
|
|
11
|
+
* after hooks run last→first (onion model).
|
|
12
|
+
*/
|
|
13
|
+
export interface OpMiddleware {
|
|
14
|
+
/** Middleware name (for logging/debugging) */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Runs before op handler. Return modified params or throw to reject. */
|
|
17
|
+
before?: (ctx: MiddlewareContext) => Promise<Record<string, unknown>>;
|
|
18
|
+
/** Runs after op handler. Return modified result or throw. */
|
|
19
|
+
after?: (ctx: MiddlewareContext & { result: unknown }) => Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MiddlewareContext {
|
|
23
|
+
facade: string;
|
|
24
|
+
op: string;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* User-defined extensions for a Soleri agent.
|
|
30
|
+
*
|
|
31
|
+
* Extensions live in `src/extensions/` and are auto-discovered by the entry
|
|
32
|
+
* point at startup. Core ops from `@soleri/core` are never modified — extensions
|
|
33
|
+
* are additive (new ops, new facades) or decorative (middleware).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // src/extensions/index.ts
|
|
38
|
+
* import type { AgentExtensions } from '@soleri/core';
|
|
39
|
+
* import type { AgentRuntime } from '@soleri/core';
|
|
40
|
+
*
|
|
41
|
+
* export default function loadExtensions(runtime: AgentRuntime): AgentExtensions {
|
|
42
|
+
* return {
|
|
43
|
+
* ops: [myCustomOp(runtime)],
|
|
44
|
+
* facades: [myCustomFacade(runtime)],
|
|
45
|
+
* middleware: [auditLogger],
|
|
46
|
+
* };
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface AgentExtensions {
|
|
51
|
+
/** Extra ops merged into the core facade */
|
|
52
|
+
ops?: OpDefinition[];
|
|
53
|
+
/** New facades registered as separate MCP tools */
|
|
54
|
+
facades?: FacadeConfig[];
|
|
55
|
+
/** Middleware applied to all ops across all facades */
|
|
56
|
+
middleware?: OpMiddleware[];
|
|
57
|
+
/** Lifecycle hooks */
|
|
58
|
+
hooks?: {
|
|
59
|
+
/** Called after runtime init, before MCP server starts */
|
|
60
|
+
onStartup?: (runtime: AgentRuntime) => Promise<void>;
|
|
61
|
+
/** Called on SIGTERM/SIGINT before process exits */
|
|
62
|
+
onShutdown?: (runtime: AgentRuntime) => Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
}
|