@siduri-x/core 2.0.4 → 2.0.6
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/context-retriever.d.ts +3 -0
- package/dist/context-retriever.js +23 -2
- package/dist/conversational-teach.test.d.ts +1 -0
- package/dist/conversational-teach.test.js +643 -0
- package/dist/index.d.ts +7 -1
- package/dist/intent-classifier.js +2 -2
- package/dist/memory-settler.d.ts +22 -1
- package/dist/memory-settler.js +69 -8
- package/dist/perception-cycle.test.js +14 -0
- package/dist/perception-pipeline.js +13 -2
- package/dist/prompt-compiler.d.ts +3 -0
- package/dist/prompt-compiler.js +5 -2
- package/dist/response-envelope.d.ts +1 -0
- package/dist/response-envelope.js +2 -1
- package/dist/runtime.d.ts +46 -0
- package/dist/runtime.js +237 -1
- package/dist/siduri-db.d.ts +7 -0
- package/dist/siduri-db.js +172 -11
- package/dist/siduri-db.test.js +2 -1
- package/dist/teaching.d.ts +6 -7
- package/dist/teaching.js +7 -125
- package/package.json +1 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const index_1 = require("./index");
|
|
40
|
+
describe('Conversational Teach Mode End-to-End Lifecycle', () => {
|
|
41
|
+
let tmpDir;
|
|
42
|
+
let dbPath;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'siduri-teach-test-'));
|
|
45
|
+
dbPath = path.join(tmpDir, 'siduri.db');
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
try {
|
|
49
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// ignore cleanup errors
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Helper to build a SelfRepository wrapping a SiduriDatabase
|
|
57
|
+
*/
|
|
58
|
+
function createSelfRepository(db) {
|
|
59
|
+
return {
|
|
60
|
+
getIdentity: async (companionId) => db.getIdentity(companionId),
|
|
61
|
+
setIdentity: async (identity) => db.setIdentity(identity),
|
|
62
|
+
getPersonality: async (companionId) => db.getPersonality(companionId) || {
|
|
63
|
+
warmth: 0.5,
|
|
64
|
+
formality: 0.5,
|
|
65
|
+
sarcasm: 0.5,
|
|
66
|
+
verbosity: 0.5,
|
|
67
|
+
curiosity: 0.5,
|
|
68
|
+
},
|
|
69
|
+
setPersonality: async (companionId, traits) => db.setPersonality(companionId, traits),
|
|
70
|
+
getActiveDirectives: async (companionId) => db.getActiveDirectives(companionId),
|
|
71
|
+
commitDirectives: async (companionId, directives) => {
|
|
72
|
+
for (const d of directives) {
|
|
73
|
+
db.commitDirective(d);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
getRelationship: async (companionId, entityId) => db.getRelationship(companionId, entityId) || null,
|
|
77
|
+
updateRelationship: async (companionId, rel) => db.upsertRelationship({ ...rel, companionId }),
|
|
78
|
+
approveDirective: async (id, companionId) => db.approveDirective(id, companionId),
|
|
79
|
+
rejectDirective: async (id, companionId) => db.rejectDirective(id, companionId),
|
|
80
|
+
revokeDirective: async (id, companionId) => db.revokeDirective(id, companionId),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Helper to build a MemoryOrgan wrapping a SiduriDatabase
|
|
85
|
+
*/
|
|
86
|
+
function createMemoryOrgan(db) {
|
|
87
|
+
return {
|
|
88
|
+
initialize: async () => { },
|
|
89
|
+
proposeClaim: async (claim) => db.proposeClaim(claim),
|
|
90
|
+
searchClaims: async () => [],
|
|
91
|
+
getClaims: async (limit) => db.getAllClaims(undefined, limit || 500),
|
|
92
|
+
getPendingClaims: async (limit) => db.getAllClaims(undefined, limit || 500).filter((c) => c.status === 'pending'),
|
|
93
|
+
approveClaim: async (id) => db.approveClaim(id),
|
|
94
|
+
rejectClaim: async (id) => db.rejectClaim(id),
|
|
95
|
+
getDirectives: async (companionId) => db.getActiveDirectives(companionId || 'default'),
|
|
96
|
+
proposeDirective: async (dir) => {
|
|
97
|
+
const id = `dir-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
98
|
+
const directive = {
|
|
99
|
+
...dir,
|
|
100
|
+
id,
|
|
101
|
+
status: 'pending',
|
|
102
|
+
createdAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
db.commitDirective(directive);
|
|
105
|
+
return directive;
|
|
106
|
+
},
|
|
107
|
+
approveDirective: async (id, companionId) => db.approveDirective(id, companionId),
|
|
108
|
+
rejectDirective: async (id, companionId) => db.rejectDirective(id, companionId),
|
|
109
|
+
revokeDirective: async (id, companionId) => db.revokeDirective(id, companionId),
|
|
110
|
+
disableDirective: async (id, companionId) => db.disableDirective(id, companionId),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Mock behavior compiler that compiles active_self tags from context
|
|
115
|
+
*/
|
|
116
|
+
function createBehaviorCompiler() {
|
|
117
|
+
return {
|
|
118
|
+
compile: async (ctx) => {
|
|
119
|
+
const parts = ['<active_self>'];
|
|
120
|
+
if (ctx.identity) {
|
|
121
|
+
parts.push(`Identity:\n- Name: ${ctx.identity.name}`);
|
|
122
|
+
if (ctx.identity.archetype || ctx.identity.role) {
|
|
123
|
+
parts.push(`- Role: ${ctx.identity.role || ctx.identity.archetype}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (ctx.relationship) {
|
|
127
|
+
const nameStr = ctx.relationship.name ? ` [Name: ${ctx.relationship.name}]` : '';
|
|
128
|
+
parts.push(`Relationship Stance:\n- Stance toward ${ctx.relationship.entityId}${nameStr}: ${ctx.relationship.stance} (Role: ${ctx.relationship.role})`);
|
|
129
|
+
}
|
|
130
|
+
if (ctx.directives && ctx.directives.length > 0) {
|
|
131
|
+
parts.push(`Behavioral Directives:\n${ctx.directives.map((d) => `- ${d.directive}`).join('\n')}`);
|
|
132
|
+
}
|
|
133
|
+
parts.push('</active_self>');
|
|
134
|
+
return parts.join('\n\n');
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function createRequestContext(companionId, mode = 'teach', actorId = 'kur-zagin') {
|
|
139
|
+
return {
|
|
140
|
+
companionId,
|
|
141
|
+
mode,
|
|
142
|
+
actor: {
|
|
143
|
+
actorId,
|
|
144
|
+
sessionId: 'sess-teach-1',
|
|
145
|
+
authorizationRole: 'OWNER',
|
|
146
|
+
authenticated: true,
|
|
147
|
+
},
|
|
148
|
+
conversation: {
|
|
149
|
+
channel: 'direct',
|
|
150
|
+
correlationId: `corr-${Date.now()}`,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function createCognitiveMockBrain(companionId) {
|
|
155
|
+
return {
|
|
156
|
+
generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
|
|
157
|
+
const lastMsg = brainCtx.recentMessages?.[brainCtx.recentMessages.length - 1]?.content || '';
|
|
158
|
+
const memoryProposals = [];
|
|
159
|
+
const behaviorProposals = [];
|
|
160
|
+
if (/AI researcher at VXNUS Studio/i.test(lastMsg)) {
|
|
161
|
+
memoryProposals.push({
|
|
162
|
+
subject: `companion:${companionId}`,
|
|
163
|
+
predicate: 'role',
|
|
164
|
+
value: 'AI researcher at VXNUS Studio',
|
|
165
|
+
});
|
|
166
|
+
behaviorProposals.push({
|
|
167
|
+
directive: 'Acknowledge role as AI researcher at VXNUS Studio',
|
|
168
|
+
category: 'relational',
|
|
169
|
+
subject: `companion:${companionId}`,
|
|
170
|
+
predicate: 'role',
|
|
171
|
+
value: 'AI researcher at VXNUS Studio',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (/Lead Architect at VXNUS Studio/i.test(lastMsg)) {
|
|
175
|
+
memoryProposals.push({
|
|
176
|
+
subject: `companion:${companionId}`,
|
|
177
|
+
predicate: 'role',
|
|
178
|
+
value: 'Lead Architect at VXNUS Studio',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else if (/Research Specialist/i.test(lastMsg)) {
|
|
182
|
+
memoryProposals.push({
|
|
183
|
+
subject: `companion:${companionId}`,
|
|
184
|
+
predicate: 'role',
|
|
185
|
+
value: 'Research Specialist',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else if (/Security Officer/i.test(lastMsg)) {
|
|
189
|
+
memoryProposals.push({
|
|
190
|
+
subject: `companion:${companionId}`,
|
|
191
|
+
predicate: 'role',
|
|
192
|
+
value: 'Security Officer',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (/VXNUS Studio Staff/i.test(lastMsg)) {
|
|
196
|
+
memoryProposals.push({
|
|
197
|
+
subject: `companion:${companionId}`,
|
|
198
|
+
predicate: 'role',
|
|
199
|
+
value: 'VXNUS Studio Staff',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (/\bcreator\b/i.test(lastMsg)) {
|
|
203
|
+
memoryProposals.push({
|
|
204
|
+
subject: 'actor:kur-zagin',
|
|
205
|
+
predicate: 'stated_relationship',
|
|
206
|
+
value: 'creator',
|
|
207
|
+
claimType: 'relationship',
|
|
208
|
+
});
|
|
209
|
+
behaviorProposals.push({
|
|
210
|
+
directive: 'Recognize actor:kur-zagin stated relationship as creator',
|
|
211
|
+
category: 'relational',
|
|
212
|
+
subject: 'actor:kur-zagin',
|
|
213
|
+
predicate: 'stated_relationship',
|
|
214
|
+
value: 'creator',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (/Kur Zagin/i.test(lastMsg)) {
|
|
218
|
+
memoryProposals.push({
|
|
219
|
+
subject: 'actor:kur-zagin',
|
|
220
|
+
predicate: 'name',
|
|
221
|
+
value: 'Kur Zagin',
|
|
222
|
+
claimType: 'preference',
|
|
223
|
+
});
|
|
224
|
+
behaviorProposals.push({
|
|
225
|
+
directive: 'Address actor:kur-zagin as Kur Zagin',
|
|
226
|
+
category: 'relational',
|
|
227
|
+
subject: 'actor:kur-zagin',
|
|
228
|
+
predicate: 'name',
|
|
229
|
+
value: 'Kur Zagin',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (/be concise when answering technical questions/i.test(lastMsg)) {
|
|
233
|
+
behaviorProposals.push({
|
|
234
|
+
directive: 'Be concise when answering technical questions',
|
|
235
|
+
category: 'behavioral',
|
|
236
|
+
priority: 60,
|
|
237
|
+
});
|
|
238
|
+
memoryProposals.push({
|
|
239
|
+
subject: 'actor:kur-zagin',
|
|
240
|
+
predicate: 'rule',
|
|
241
|
+
value: 'Be concise when answering technical questions',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
speech: 'Understood, I have acknowledged your input.',
|
|
246
|
+
language: 'en',
|
|
247
|
+
memoryProposals: memoryProposals.length > 0 ? memoryProposals : undefined,
|
|
248
|
+
behaviorProposals: behaviorProposals.length > 0 ? behaviorProposals : undefined,
|
|
249
|
+
_receivedSystemPrompt: brainCtx.systemPrompt,
|
|
250
|
+
};
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// =========================================================================
|
|
255
|
+
// Test A: Conversational role teaching in Teach mode
|
|
256
|
+
// =========================================================================
|
|
257
|
+
it('Test A: teaches companion role in Teach mode, approves proposal, and reflects role in Active Self', async () => {
|
|
258
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
259
|
+
const companionId = 'comp-test-a';
|
|
260
|
+
const self = createSelfRepository(db);
|
|
261
|
+
const memory = createMemoryOrgan(db);
|
|
262
|
+
const behavior = createBehaviorCompiler();
|
|
263
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
264
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
265
|
+
brain: mockBrain,
|
|
266
|
+
memory,
|
|
267
|
+
self,
|
|
268
|
+
behavior,
|
|
269
|
+
});
|
|
270
|
+
await runtime.initialize();
|
|
271
|
+
// 1. Send conversational role teaching message
|
|
272
|
+
const perceptionResult = await runtime.processPerception({
|
|
273
|
+
source: 'text_chat',
|
|
274
|
+
text: 'Siduri, you are an AI researcher at VXNUS Studio.',
|
|
275
|
+
context: createRequestContext(companionId, 'teach'),
|
|
276
|
+
});
|
|
277
|
+
expect(perceptionResult.status).toBe('APPROVED');
|
|
278
|
+
// 2. Verify proposal was generated
|
|
279
|
+
const proposals = perceptionResult.metadata?.proposals || [];
|
|
280
|
+
expect(proposals.length).toBeGreaterThanOrEqual(1);
|
|
281
|
+
const roleProposal = proposals.find((p) => p.predicate === 'role' || p.predicate === 'archetype');
|
|
282
|
+
expect(roleProposal).toBeDefined();
|
|
283
|
+
expect(roleProposal.value).toBe('AI researcher at VXNUS Studio');
|
|
284
|
+
expect(roleProposal.status).toBe('pending');
|
|
285
|
+
// 3. Truth Gate check: Before approval, SelfRepository must NOT contain the new role
|
|
286
|
+
const identityBefore = await self.getIdentity(companionId);
|
|
287
|
+
expect(identityBefore?.role).toBeUndefined();
|
|
288
|
+
expect(identityBefore?.archetype).toBeUndefined();
|
|
289
|
+
// 4. Operator / Owner approves the proposal
|
|
290
|
+
const approveResult = await runtime.approveProposal(roleProposal.id, { companionId });
|
|
291
|
+
expect(approveResult.success).toBe(true);
|
|
292
|
+
// 5. Verify SelfRepository now has the learned role
|
|
293
|
+
const identityAfter = await self.getIdentity(companionId);
|
|
294
|
+
expect(identityAfter).toBeDefined();
|
|
295
|
+
expect(identityAfter?.role).toBe('AI researcher at VXNUS Studio');
|
|
296
|
+
expect(identityAfter?.archetype).toBe('AI researcher at VXNUS Studio');
|
|
297
|
+
// Also verify a relational directive acknowledging the role was added
|
|
298
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
299
|
+
const roleDirective = directives.find((d) => d.directive.includes('AI researcher at VXNUS Studio'));
|
|
300
|
+
expect(roleDirective).toBeDefined();
|
|
301
|
+
// 6. Next conversational turn: verify prompt compiler injects the updated Active Self
|
|
302
|
+
await runtime.processPerception({
|
|
303
|
+
source: 'text_chat',
|
|
304
|
+
text: 'What is your primary mission?',
|
|
305
|
+
context: createRequestContext(companionId, 'hybrid'),
|
|
306
|
+
});
|
|
307
|
+
expect(mockBrain.generatePlan).toHaveBeenCalled();
|
|
308
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
|
|
309
|
+
expect(lastCallCtx.systemPrompt).toContain('<active_self>');
|
|
310
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: AI researcher at VXNUS Studio');
|
|
311
|
+
db.close();
|
|
312
|
+
});
|
|
313
|
+
// =========================================================================
|
|
314
|
+
// Test B: Creator relationship teaching
|
|
315
|
+
// =========================================================================
|
|
316
|
+
it('Test B: teaches creator relationship, approves proposal, and updates relationship stance', async () => {
|
|
317
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
318
|
+
const companionId = 'comp-test-b';
|
|
319
|
+
const self = createSelfRepository(db);
|
|
320
|
+
const memory = createMemoryOrgan(db);
|
|
321
|
+
const behavior = createBehaviorCompiler();
|
|
322
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
323
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
324
|
+
brain: mockBrain,
|
|
325
|
+
memory,
|
|
326
|
+
self,
|
|
327
|
+
behavior,
|
|
328
|
+
});
|
|
329
|
+
await runtime.initialize();
|
|
330
|
+
// 1. Send creator teaching message
|
|
331
|
+
const perceptionResult = await runtime.processPerception({
|
|
332
|
+
source: 'text_chat',
|
|
333
|
+
text: 'I am your creator, Kur Zagin.',
|
|
334
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
335
|
+
});
|
|
336
|
+
const proposals = perceptionResult.metadata?.proposals || [];
|
|
337
|
+
const relProposal = proposals.find((p) => p.predicate === 'stated_relationship' ||
|
|
338
|
+
p.predicate === 'relationship' ||
|
|
339
|
+
p.claimType === 'relationship');
|
|
340
|
+
expect(relProposal).toBeDefined();
|
|
341
|
+
expect(relProposal.value).toBe('creator');
|
|
342
|
+
// 2. Truth Gate: Before approval, relationship does not have creator stance
|
|
343
|
+
const relBefore = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
344
|
+
expect(relBefore?.role).toBeUndefined();
|
|
345
|
+
// 3. Approve the relationship proposal
|
|
346
|
+
await runtime.approveProposal(relProposal.id, { companionId });
|
|
347
|
+
// 4. Verify SelfRepository now has creator relationship with loyal stance
|
|
348
|
+
const relAfter = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
349
|
+
expect(relAfter).toBeDefined();
|
|
350
|
+
expect(relAfter?.role).toBe('creator');
|
|
351
|
+
expect(relAfter?.stance).toBe('familiar_loyal');
|
|
352
|
+
expect(relAfter?.trustScore).toBe(1.0);
|
|
353
|
+
// 5. Subsequent conversation retrieves relationship in prompt
|
|
354
|
+
await runtime.processPerception({
|
|
355
|
+
source: 'text_chat',
|
|
356
|
+
text: 'Status update please.',
|
|
357
|
+
context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
|
|
358
|
+
});
|
|
359
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
|
|
360
|
+
expect(lastCallCtx.systemPrompt).toContain('Stance toward actor:kur-zagin: familiar_loyal (Role: creator)');
|
|
361
|
+
db.close();
|
|
362
|
+
});
|
|
363
|
+
// =========================================================================
|
|
364
|
+
// Test C: Behavioral rule teaching
|
|
365
|
+
// =========================================================================
|
|
366
|
+
it('Test C: teaches behavioral rule, generates proposal, approves, and activates directive in Self', async () => {
|
|
367
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
368
|
+
const companionId = 'comp-test-c';
|
|
369
|
+
const self = createSelfRepository(db);
|
|
370
|
+
const memory = createMemoryOrgan(db);
|
|
371
|
+
const behavior = createBehaviorCompiler();
|
|
372
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
373
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
374
|
+
brain: mockBrain,
|
|
375
|
+
memory,
|
|
376
|
+
self,
|
|
377
|
+
behavior,
|
|
378
|
+
});
|
|
379
|
+
await runtime.initialize();
|
|
380
|
+
// 1. Send behavioral rule teaching message
|
|
381
|
+
const perceptionResult = await runtime.processPerception({
|
|
382
|
+
source: 'text_chat',
|
|
383
|
+
text: 'Remember this rule: be concise when answering technical questions.',
|
|
384
|
+
context: createRequestContext(companionId, 'teach'),
|
|
385
|
+
});
|
|
386
|
+
// Check behavioral proposals or memory proposals
|
|
387
|
+
const behavioralReceipts = perceptionResult.metadata?.behavioral_proposals || [];
|
|
388
|
+
const memoryProposals = perceptionResult.metadata?.proposals || [];
|
|
389
|
+
const hasDirectiveProposal = behavioralReceipts.length > 0 ||
|
|
390
|
+
memoryProposals.some((p) => p.predicate === 'rule' || p.predicate === 'behavioral_rule');
|
|
391
|
+
expect(hasDirectiveProposal).toBe(true);
|
|
392
|
+
const directiveId = behavioralReceipts[0]?.directive_id;
|
|
393
|
+
const proposalId = memoryProposals[0]?.id;
|
|
394
|
+
// 2. Active directives before approval must not include the new rule
|
|
395
|
+
const directivesBefore = await self.getActiveDirectives(companionId);
|
|
396
|
+
expect(directivesBefore.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(false);
|
|
397
|
+
// 3. Approve directive via runtime
|
|
398
|
+
if (directiveId) {
|
|
399
|
+
await runtime.approveDirective(directiveId, { companionId });
|
|
400
|
+
}
|
|
401
|
+
else if (proposalId) {
|
|
402
|
+
await runtime.approveProposal(proposalId, { companionId });
|
|
403
|
+
}
|
|
404
|
+
// 4. Verify directive is now ACTIVE in SelfRepository
|
|
405
|
+
const directivesAfter = await self.getActiveDirectives(companionId);
|
|
406
|
+
expect(directivesAfter.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(true);
|
|
407
|
+
db.close();
|
|
408
|
+
});
|
|
409
|
+
// =========================================================================
|
|
410
|
+
// Test D: Casual banter in Casual mode (Zero Memory Drift)
|
|
411
|
+
// =========================================================================
|
|
412
|
+
it('Test D: casual banter in casual mode causes Zero Memory Drift and does NOT mutate Self', async () => {
|
|
413
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
414
|
+
const companionId = 'comp-test-d';
|
|
415
|
+
const self = createSelfRepository(db);
|
|
416
|
+
const memory = createMemoryOrgan(db);
|
|
417
|
+
const mockBrain = {
|
|
418
|
+
generatePlan: jest.fn().mockResolvedValue({
|
|
419
|
+
speech: 'Haha, thank you! I try my best.',
|
|
420
|
+
language: 'en',
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
424
|
+
brain: mockBrain,
|
|
425
|
+
memory,
|
|
426
|
+
self,
|
|
427
|
+
});
|
|
428
|
+
await runtime.initialize();
|
|
429
|
+
// "You are probably the funniest AI I've ever talked to." in casual mode
|
|
430
|
+
const perceptionResult = await runtime.processPerception({
|
|
431
|
+
source: 'text_chat',
|
|
432
|
+
text: "You are probably the funniest AI I've ever talked to.",
|
|
433
|
+
context: createRequestContext(companionId, 'casual'),
|
|
434
|
+
});
|
|
435
|
+
expect(perceptionResult.status).toBe('APPROVED');
|
|
436
|
+
// Zero proposals allowed in casual mode
|
|
437
|
+
expect(perceptionResult.metadata?.proposals).toEqual([]);
|
|
438
|
+
expect(perceptionResult.metadata?.memory_proposals).toEqual([]);
|
|
439
|
+
// Self must remain unchanged
|
|
440
|
+
const identity = await self.getIdentity(companionId);
|
|
441
|
+
expect(identity?.role).toBeUndefined();
|
|
442
|
+
expect(identity?.archetype).toBeUndefined();
|
|
443
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
444
|
+
expect(directives).toHaveLength(0);
|
|
445
|
+
db.close();
|
|
446
|
+
});
|
|
447
|
+
// =========================================================================
|
|
448
|
+
// Test E: Approval gating: rejection leaves Self unchanged
|
|
449
|
+
// =========================================================================
|
|
450
|
+
it('Test E: rejecting a proposal prevents mutation of Self', async () => {
|
|
451
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
452
|
+
const companionId = 'comp-test-e';
|
|
453
|
+
const self = createSelfRepository(db);
|
|
454
|
+
const memory = createMemoryOrgan(db);
|
|
455
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
456
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
457
|
+
brain: mockBrain,
|
|
458
|
+
memory,
|
|
459
|
+
self,
|
|
460
|
+
});
|
|
461
|
+
await runtime.initialize();
|
|
462
|
+
// 1. Propose a role change
|
|
463
|
+
const perceptionResult = await runtime.processPerception({
|
|
464
|
+
source: 'text_chat',
|
|
465
|
+
text: 'Siduri, your role is Security Officer.',
|
|
466
|
+
context: createRequestContext(companionId, 'teach'),
|
|
467
|
+
});
|
|
468
|
+
const proposal = perceptionResult.metadata?.proposals?.[0];
|
|
469
|
+
expect(proposal).toBeDefined();
|
|
470
|
+
// 2. Reject the proposal
|
|
471
|
+
await runtime.rejectProposal(proposal.id, { companionId });
|
|
472
|
+
// 3. Verify Self is NOT mutated
|
|
473
|
+
const identity = await self.getIdentity(companionId);
|
|
474
|
+
expect(identity?.role).toBeUndefined();
|
|
475
|
+
expect(identity?.archetype).toBeUndefined();
|
|
476
|
+
db.close();
|
|
477
|
+
});
|
|
478
|
+
// =========================================================================
|
|
479
|
+
// Test F: Cross-restart durability (survives runtime restart)
|
|
480
|
+
// =========================================================================
|
|
481
|
+
it('Test F: persists learned state across runtime destruction and recreation', async () => {
|
|
482
|
+
const companionId = 'comp-test-f';
|
|
483
|
+
// --- Phase 1: Runtime 1 teaches and approves ---
|
|
484
|
+
{
|
|
485
|
+
const db1 = new index_1.SiduriDatabase({ dbPath });
|
|
486
|
+
const self1 = createSelfRepository(db1);
|
|
487
|
+
const memory1 = createMemoryOrgan(db1);
|
|
488
|
+
const mockBrain1 = createCognitiveMockBrain(companionId);
|
|
489
|
+
const runtime1 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
490
|
+
brain: mockBrain1,
|
|
491
|
+
memory: memory1,
|
|
492
|
+
self: self1,
|
|
493
|
+
});
|
|
494
|
+
await runtime1.initialize();
|
|
495
|
+
const res = await runtime1.processPerception({
|
|
496
|
+
source: 'text_chat',
|
|
497
|
+
text: 'Siduri, you are the Lead Architect at VXNUS Studio.',
|
|
498
|
+
context: createRequestContext(companionId, 'teach'),
|
|
499
|
+
});
|
|
500
|
+
const roleProposal = res.metadata?.proposals?.find((p) => p.predicate === 'role');
|
|
501
|
+
expect(roleProposal).toBeDefined();
|
|
502
|
+
await runtime1.approveProposal(roleProposal.id, { companionId });
|
|
503
|
+
// Verify in runtime 1
|
|
504
|
+
const id1 = await self1.getIdentity(companionId);
|
|
505
|
+
expect(id1?.role).toBe('Lead Architect at VXNUS Studio');
|
|
506
|
+
// Close and destroy runtime 1 completely
|
|
507
|
+
db1.close();
|
|
508
|
+
}
|
|
509
|
+
// --- Phase 2: Runtime 2 initialized afresh pointing to same SQLite database ---
|
|
510
|
+
{
|
|
511
|
+
const db2 = new index_1.SiduriDatabase({ dbPath });
|
|
512
|
+
const self2 = createSelfRepository(db2);
|
|
513
|
+
const memory2 = createMemoryOrgan(db2);
|
|
514
|
+
const behavior2 = createBehaviorCompiler();
|
|
515
|
+
const mockBrain2 = createCognitiveMockBrain(companionId);
|
|
516
|
+
const runtime2 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
517
|
+
brain: mockBrain2,
|
|
518
|
+
memory: memory2,
|
|
519
|
+
self: self2,
|
|
520
|
+
behavior: behavior2,
|
|
521
|
+
});
|
|
522
|
+
await runtime2.initialize();
|
|
523
|
+
// Verify that runtime 2 retrieves the learned role from SQLite!
|
|
524
|
+
const id2 = await self2.getIdentity(companionId);
|
|
525
|
+
expect(id2).toBeDefined();
|
|
526
|
+
expect(id2?.role).toBe('Lead Architect at VXNUS Studio');
|
|
527
|
+
expect(id2?.archetype).toBe('Lead Architect at VXNUS Studio');
|
|
528
|
+
// Verify that conversation in runtime 2 retrieves the learned role in prompt compilation
|
|
529
|
+
await runtime2.processPerception({
|
|
530
|
+
source: 'text_chat',
|
|
531
|
+
text: 'What do you do?',
|
|
532
|
+
context: createRequestContext(companionId, 'hybrid'),
|
|
533
|
+
});
|
|
534
|
+
const lastCallCtx = mockBrain2.generatePlan.mock.calls[0][0];
|
|
535
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: Lead Architect at VXNUS Studio');
|
|
536
|
+
db2.close();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// =========================================================================
|
|
540
|
+
// Test G: Idempotency (duplicate approval does not corrupt database)
|
|
541
|
+
// =========================================================================
|
|
542
|
+
it('Test G: approving the same proposal twice is idempotent and does not create duplicate entries', async () => {
|
|
543
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
544
|
+
const companionId = 'comp-test-g';
|
|
545
|
+
const self = createSelfRepository(db);
|
|
546
|
+
const memory = createMemoryOrgan(db);
|
|
547
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
548
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
549
|
+
brain: mockBrain,
|
|
550
|
+
memory,
|
|
551
|
+
self,
|
|
552
|
+
});
|
|
553
|
+
await runtime.initialize();
|
|
554
|
+
const res = await runtime.processPerception({
|
|
555
|
+
source: 'text_chat',
|
|
556
|
+
text: 'Siduri, you are a Research Specialist.',
|
|
557
|
+
context: createRequestContext(companionId, 'teach'),
|
|
558
|
+
});
|
|
559
|
+
const proposal = res.metadata?.proposals?.[0];
|
|
560
|
+
expect(proposal).toBeDefined();
|
|
561
|
+
// Approve once
|
|
562
|
+
await runtime.approveProposal(proposal.id, { companionId });
|
|
563
|
+
const idFirst = await self.getIdentity(companionId);
|
|
564
|
+
expect(idFirst?.role).toBe('Research Specialist');
|
|
565
|
+
// Approve a second time (should be completely idempotent and not error)
|
|
566
|
+
await runtime.approveProposal(proposal.id, { companionId });
|
|
567
|
+
const idSecond = await self.getIdentity(companionId);
|
|
568
|
+
expect(idSecond?.role).toBe('Research Specialist');
|
|
569
|
+
// Check directives: should only have one directive for acknowledging the role
|
|
570
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
571
|
+
const roleDirectives = directives.filter((d) => d.directive.includes('Research Specialist'));
|
|
572
|
+
expect(roleDirectives).toHaveLength(1);
|
|
573
|
+
db.close();
|
|
574
|
+
});
|
|
575
|
+
// =========================================================================
|
|
576
|
+
// Test H: Natural conversational teaching (past-tense role, user name, creator)
|
|
577
|
+
// =========================================================================
|
|
578
|
+
it('Test H: teaches companion past role ("she was VXNUS Studio Staff"), creator, and name, then recognizes user on subsequent turn', async () => {
|
|
579
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
580
|
+
const companionId = 'comp-test-h';
|
|
581
|
+
const self = createSelfRepository(db);
|
|
582
|
+
const memory = createMemoryOrgan(db);
|
|
583
|
+
const behavior = createBehaviorCompiler();
|
|
584
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
585
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
586
|
+
brain: mockBrain,
|
|
587
|
+
memory,
|
|
588
|
+
self,
|
|
589
|
+
behavior,
|
|
590
|
+
});
|
|
591
|
+
await runtime.initialize();
|
|
592
|
+
// 1. "she was VXNUS Studio Staff" in Teach mode
|
|
593
|
+
const resRole = await runtime.processPerception({
|
|
594
|
+
source: 'text_chat',
|
|
595
|
+
text: 'She was VXNUS Studio Staff.',
|
|
596
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
597
|
+
});
|
|
598
|
+
expect(resRole.status).toBe('APPROVED');
|
|
599
|
+
const roleProp = resRole.metadata?.proposals?.find((p) => p.predicate === 'role');
|
|
600
|
+
expect(roleProp).toBeDefined();
|
|
601
|
+
expect(roleProp.value).toBe('VXNUS Studio Staff');
|
|
602
|
+
await runtime.approveProposal(roleProp.id, { companionId });
|
|
603
|
+
// 2. "my name is Kur Zagin" in Teach mode
|
|
604
|
+
const resName = await runtime.processPerception({
|
|
605
|
+
source: 'text_chat',
|
|
606
|
+
text: 'My name is Kur Zagin.',
|
|
607
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
608
|
+
});
|
|
609
|
+
expect(resName.status).toBe('APPROVED');
|
|
610
|
+
const nameProp = resName.metadata?.proposals?.find((p) => p.predicate === 'name');
|
|
611
|
+
expect(nameProp).toBeDefined();
|
|
612
|
+
expect(nameProp.value).toBe('Kur Zagin');
|
|
613
|
+
await runtime.approveProposal(nameProp.id, { companionId });
|
|
614
|
+
// 3. "I am your creator" in Teach mode
|
|
615
|
+
const resRel = await runtime.processPerception({
|
|
616
|
+
source: 'text_chat',
|
|
617
|
+
text: 'I am your creator.',
|
|
618
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
619
|
+
});
|
|
620
|
+
expect(resRel.status).toBe('APPROVED');
|
|
621
|
+
const relProp = resRel.metadata?.proposals?.find((p) => p.predicate === 'stated_relationship');
|
|
622
|
+
expect(relProp).toBeDefined();
|
|
623
|
+
await runtime.approveProposal(relProp.id, { companionId });
|
|
624
|
+
// 4. Verify Self relationship has both creator stance AND user name preserved
|
|
625
|
+
const rel = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
626
|
+
expect(rel).toBeDefined();
|
|
627
|
+
expect(rel?.role).toBe('creator');
|
|
628
|
+
expect(rel?.stance).toBe('familiar_loyal');
|
|
629
|
+
expect(rel?.name).toBe('Kur Zagin');
|
|
630
|
+
// 5. Subsequent conversation turn: "do you know me?"
|
|
631
|
+
await runtime.processPerception({
|
|
632
|
+
source: 'text_chat',
|
|
633
|
+
text: 'do you know me?',
|
|
634
|
+
context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
|
|
635
|
+
});
|
|
636
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[mockBrain.generatePlan.mock.calls.length - 1][0];
|
|
637
|
+
// Active Self contains both the creator stance and the user's name
|
|
638
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: VXNUS Studio Staff');
|
|
639
|
+
expect(lastCallCtx.systemPrompt).toContain('familiar_loyal');
|
|
640
|
+
expect(lastCallCtx.systemPrompt).toContain('Kur Zagin');
|
|
641
|
+
db.close();
|
|
642
|
+
});
|
|
643
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -261,7 +261,9 @@ import type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship,
|
|
|
261
261
|
export type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, SelfDialogueExample, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent, };
|
|
262
262
|
export interface SelfRepository {
|
|
263
263
|
getIdentity(companionId: string): Promise<SelfIdentity | undefined>;
|
|
264
|
+
setIdentity(identity: SelfIdentity): Promise<void>;
|
|
264
265
|
getPersonality?(companionId: string): Promise<PersonalityTraits>;
|
|
266
|
+
setPersonality?(companionId: string, traits: PersonalityTraits): Promise<void>;
|
|
265
267
|
getActiveDirectives(companionId: string): Promise<SelfDirective[]>;
|
|
266
268
|
getRelationship(companionId: string, entityId: string): Promise<SelfRelationship | null>;
|
|
267
269
|
getRelationships?(companionId: string): Promise<SelfRelationship[]>;
|
|
@@ -269,7 +271,11 @@ export interface SelfRepository {
|
|
|
269
271
|
setExemplars?(companionId: string, exemplars: SelfDialogueExample[]): Promise<void>;
|
|
270
272
|
commitDirectives(companionId: string, directives: SelfDirective[]): Promise<void>;
|
|
271
273
|
updateRelationship(companionId: string, rel: SelfRelationship): Promise<void>;
|
|
272
|
-
disableDirective?(id: string): Promise<void>;
|
|
274
|
+
disableDirective?(id: string, companionId?: string): Promise<void>;
|
|
275
|
+
approveDirective?(id: string, companionId?: string): Promise<void>;
|
|
276
|
+
rejectDirective?(id: string, companionId?: string): Promise<void>;
|
|
277
|
+
revokeDirective?(id: string, companionId?: string): Promise<void>;
|
|
278
|
+
expireDirective?(id: string, companionId?: string): Promise<void>;
|
|
273
279
|
getActiveSelf?(companionId: string): Promise<{
|
|
274
280
|
identity?: SelfIdentity;
|
|
275
281
|
personality?: PersonalityTraits;
|