@sidurijs/core 1.0.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/action-executor.d.ts +12 -0
- package/dist/action-executor.js +54 -0
- package/dist/action-policy.d.ts +79 -0
- package/dist/action-policy.js +439 -0
- package/dist/action-policy.test.d.ts +1 -0
- package/dist/action-policy.test.js +363 -0
- package/dist/action.d.ts +72 -0
- package/dist/action.js +2 -0
- package/dist/adversarial.test.d.ts +1 -0
- package/dist/adversarial.test.js +491 -0
- package/dist/architecture-boundary.test.d.ts +1 -0
- package/dist/architecture-boundary.test.js +108 -0
- package/dist/capability.d.ts +78 -0
- package/dist/capability.js +168 -0
- package/dist/capability.test.d.ts +1 -0
- package/dist/capability.test.js +311 -0
- package/dist/chat-contract.d.ts +94 -0
- package/dist/chat-contract.js +104 -0
- package/dist/cognition-planner.d.ts +15 -0
- package/dist/cognition-planner.js +23 -0
- package/dist/container.d.ts +74 -0
- package/dist/container.js +81 -0
- package/dist/context-retriever.d.ts +33 -0
- package/dist/context-retriever.js +260 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +85 -0
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +76 -0
- package/dist/conversational-teach.test.d.ts +1 -0
- package/dist/conversational-teach.test.js +941 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +17 -0
- package/dist/dispatcher.d.ts +14 -0
- package/dist/dispatcher.js +40 -0
- package/dist/dispatcher.test.d.ts +1 -0
- package/dist/dispatcher.test.js +58 -0
- package/dist/ear-types.d.ts +33 -0
- package/dist/ear-types.js +2 -0
- package/dist/evidence.d.ts +76 -0
- package/dist/evidence.js +27 -0
- package/dist/evidence.test.d.ts +1 -0
- package/dist/evidence.test.js +75 -0
- package/dist/experience-emitter.d.ts +21 -0
- package/dist/experience-emitter.js +47 -0
- package/dist/experience.d.ts +55 -0
- package/dist/experience.js +74 -0
- package/dist/experience.test.d.ts +1 -0
- package/dist/experience.test.js +55 -0
- package/dist/gating.d.ts +45 -0
- package/dist/gating.js +185 -0
- package/dist/gating.test.d.ts +1 -0
- package/dist/gating.test.js +186 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +56 -0
- package/dist/input-normalizer.d.ts +14 -0
- package/dist/input-normalizer.js +76 -0
- package/dist/input-normalizer.test.d.ts +1 -0
- package/dist/input-normalizer.test.js +48 -0
- package/dist/intent-classifier.d.ts +37 -0
- package/dist/intent-classifier.js +150 -0
- package/dist/intent-classifier.test.d.ts +1 -0
- package/dist/intent-classifier.test.js +118 -0
- package/dist/memory-settler.d.ts +49 -0
- package/dist/memory-settler.js +161 -0
- package/dist/mouth-types.d.ts +90 -0
- package/dist/mouth-types.js +2 -0
- package/dist/perception-cycle.test.d.ts +1 -0
- package/dist/perception-cycle.test.js +301 -0
- package/dist/perception-pipeline.d.ts +77 -0
- package/dist/perception-pipeline.js +304 -0
- package/dist/perception-pipeline.test.d.ts +1 -0
- package/dist/perception-pipeline.test.js +65 -0
- package/dist/prompt-compiler.d.ts +26 -0
- package/dist/prompt-compiler.js +81 -0
- package/dist/prompt-compiler.test.d.ts +1 -0
- package/dist/prompt-compiler.test.js +90 -0
- package/dist/proposals.d.ts +33 -0
- package/dist/proposals.js +2 -0
- package/dist/response-envelope.d.ts +30 -0
- package/dist/response-envelope.js +90 -0
- package/dist/runtime-facades.test.d.ts +1 -0
- package/dist/runtime-facades.test.js +95 -0
- package/dist/runtime.d.ts +116 -0
- package/dist/runtime.js +778 -0
- package/dist/schema-validator.d.ts +9 -0
- package/dist/schema-validator.js +107 -0
- package/dist/schema-validator.test.d.ts +1 -0
- package/dist/schema-validator.test.js +124 -0
- package/dist/session-history.d.ts +20 -0
- package/dist/session-history.js +55 -0
- package/dist/session-history.test.d.ts +1 -0
- package/dist/session-history.test.js +38 -0
- package/dist/siduri-db.d.ts +235 -0
- package/dist/siduri-db.js +1405 -0
- package/dist/siduri-db.test.d.ts +1 -0
- package/dist/siduri-db.test.js +1021 -0
- package/dist/sqlite-action-store.d.ts +21 -0
- package/dist/sqlite-action-store.js +281 -0
- package/dist/sqlite-action-store.test.d.ts +1 -0
- package/dist/sqlite-action-store.test.js +261 -0
- package/dist/teaching.d.ts +14 -0
- package/dist/teaching.js +14 -0
- package/package.json +43 -0
|
@@ -0,0 +1,941 @@
|
|
|
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
|
+
getApprovedClaims: async (companionId, limit) => db.getApprovedClaims(companionId || 'default', limit || 50),
|
|
92
|
+
getClaims: async (limit) => db.getAllClaims(undefined, limit || 500),
|
|
93
|
+
getPendingClaims: async (limit) => db.getAllClaims(undefined, limit || 500).filter((c) => c.status === 'pending'),
|
|
94
|
+
approveClaim: async (id) => db.approveClaim(id),
|
|
95
|
+
rejectClaim: async (id) => db.rejectClaim(id),
|
|
96
|
+
getDirectives: async (companionId) => db.getActiveDirectives(companionId || 'default'),
|
|
97
|
+
proposeDirective: async (dir) => {
|
|
98
|
+
const id = `dir-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
99
|
+
const directive = {
|
|
100
|
+
...dir,
|
|
101
|
+
id,
|
|
102
|
+
status: 'pending',
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
db.commitDirective(directive);
|
|
106
|
+
return directive;
|
|
107
|
+
},
|
|
108
|
+
approveDirective: async (id, companionId) => db.approveDirective(id, companionId),
|
|
109
|
+
rejectDirective: async (id, companionId) => db.rejectDirective(id, companionId),
|
|
110
|
+
revokeDirective: async (id, companionId) => db.revokeDirective(id, companionId),
|
|
111
|
+
disableDirective: async (id, companionId) => db.disableDirective(id, companionId),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Mock behavior compiler that compiles active_self tags from context
|
|
116
|
+
*/
|
|
117
|
+
function createBehaviorCompiler() {
|
|
118
|
+
return {
|
|
119
|
+
compile: async (ctx) => {
|
|
120
|
+
const parts = ['<active_self>'];
|
|
121
|
+
if (ctx.identity) {
|
|
122
|
+
parts.push(`Identity:\n- Name: ${ctx.identity.name}`);
|
|
123
|
+
if (ctx.identity.archetype || ctx.identity.role) {
|
|
124
|
+
parts.push(`- Role: ${ctx.identity.role || ctx.identity.archetype}`);
|
|
125
|
+
}
|
|
126
|
+
if (ctx.identity.origin) {
|
|
127
|
+
parts.push(`- Origin/Created By: ${ctx.identity.origin}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (ctx.relationship) {
|
|
131
|
+
const nameStr = ctx.relationship.name ? ` [Name: ${ctx.relationship.name}]` : '';
|
|
132
|
+
parts.push(`Relationship Stance:\n- Stance toward ${ctx.relationship.entityId}${nameStr}: ${ctx.relationship.stance} (Role: ${ctx.relationship.role})`);
|
|
133
|
+
}
|
|
134
|
+
if (ctx.directives && ctx.directives.length > 0) {
|
|
135
|
+
parts.push(`Behavioral Directives:\n${ctx.directives.map((d) => `- ${d.directive}`).join('\n')}`);
|
|
136
|
+
}
|
|
137
|
+
parts.push('</active_self>');
|
|
138
|
+
return parts.join('\n\n');
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function createRequestContext(companionId, mode = 'teach', actorId = 'kur-zagin') {
|
|
143
|
+
return {
|
|
144
|
+
companionId,
|
|
145
|
+
mode,
|
|
146
|
+
actor: {
|
|
147
|
+
actorId,
|
|
148
|
+
sessionId: 'sess-teach-1',
|
|
149
|
+
authorizationRole: 'OWNER',
|
|
150
|
+
authenticated: true,
|
|
151
|
+
},
|
|
152
|
+
conversation: {
|
|
153
|
+
channel: 'direct',
|
|
154
|
+
correlationId: `corr-${Date.now()}`,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function createCognitiveMockBrain(companionId) {
|
|
159
|
+
return {
|
|
160
|
+
generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
|
|
161
|
+
const lastMsg = brainCtx.recentMessages?.[brainCtx.recentMessages.length - 1]?.content || '';
|
|
162
|
+
const memoryProposals = [];
|
|
163
|
+
const behaviorProposals = [];
|
|
164
|
+
if (/AI researcher at VXNUS Studio/i.test(lastMsg)) {
|
|
165
|
+
memoryProposals.push({
|
|
166
|
+
subject: `companion:${companionId}`,
|
|
167
|
+
predicate: 'role',
|
|
168
|
+
value: 'AI researcher at VXNUS Studio',
|
|
169
|
+
});
|
|
170
|
+
behaviorProposals.push({
|
|
171
|
+
directive: 'Acknowledge role as AI researcher at VXNUS Studio',
|
|
172
|
+
category: 'relational',
|
|
173
|
+
subject: `companion:${companionId}`,
|
|
174
|
+
predicate: 'role',
|
|
175
|
+
value: 'AI researcher at VXNUS Studio',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else if (/Lead Architect at VXNUS Studio/i.test(lastMsg)) {
|
|
179
|
+
memoryProposals.push({
|
|
180
|
+
subject: `companion:${companionId}`,
|
|
181
|
+
predicate: 'role',
|
|
182
|
+
value: 'Lead Architect at VXNUS Studio',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (/Research Specialist/i.test(lastMsg)) {
|
|
186
|
+
memoryProposals.push({
|
|
187
|
+
subject: `companion:${companionId}`,
|
|
188
|
+
predicate: 'role',
|
|
189
|
+
value: 'Research Specialist',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (/Security Officer/i.test(lastMsg)) {
|
|
193
|
+
memoryProposals.push({
|
|
194
|
+
subject: `companion:${companionId}`,
|
|
195
|
+
predicate: 'role',
|
|
196
|
+
value: 'Security Officer',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else if (/VXNUS Studio Staff/i.test(lastMsg)) {
|
|
200
|
+
memoryProposals.push({
|
|
201
|
+
subject: `companion:${companionId}`,
|
|
202
|
+
predicate: 'role',
|
|
203
|
+
value: 'VXNUS Studio Staff',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (/\bcreator\b/i.test(lastMsg)) {
|
|
207
|
+
memoryProposals.push({
|
|
208
|
+
subject: 'actor:kur-zagin',
|
|
209
|
+
predicate: 'stated_relationship',
|
|
210
|
+
value: 'creator',
|
|
211
|
+
claimType: 'relationship',
|
|
212
|
+
});
|
|
213
|
+
behaviorProposals.push({
|
|
214
|
+
directive: 'Recognize actor:kur-zagin stated relationship as creator',
|
|
215
|
+
category: 'relational',
|
|
216
|
+
subject: 'actor:kur-zagin',
|
|
217
|
+
predicate: 'stated_relationship',
|
|
218
|
+
value: 'creator',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (/Kur Zagin/i.test(lastMsg)) {
|
|
222
|
+
memoryProposals.push({
|
|
223
|
+
subject: 'actor:kur-zagin',
|
|
224
|
+
predicate: 'name',
|
|
225
|
+
value: 'Kur Zagin',
|
|
226
|
+
claimType: 'preference',
|
|
227
|
+
});
|
|
228
|
+
behaviorProposals.push({
|
|
229
|
+
directive: 'Address actor:kur-zagin as Kur Zagin',
|
|
230
|
+
category: 'relational',
|
|
231
|
+
subject: 'actor:kur-zagin',
|
|
232
|
+
predicate: 'name',
|
|
233
|
+
value: 'Kur Zagin',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (/be concise when answering technical questions/i.test(lastMsg)) {
|
|
237
|
+
behaviorProposals.push({
|
|
238
|
+
directive: 'Be concise when answering technical questions',
|
|
239
|
+
category: 'behavioral',
|
|
240
|
+
priority: 60,
|
|
241
|
+
});
|
|
242
|
+
memoryProposals.push({
|
|
243
|
+
subject: 'actor:kur-zagin',
|
|
244
|
+
predicate: 'rule',
|
|
245
|
+
value: 'Be concise when answering technical questions',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
speech: 'Understood, I have acknowledged your input.',
|
|
250
|
+
language: 'en',
|
|
251
|
+
memoryProposals: memoryProposals.length > 0 ? memoryProposals : undefined,
|
|
252
|
+
behaviorProposals: behaviorProposals.length > 0 ? behaviorProposals : undefined,
|
|
253
|
+
_receivedSystemPrompt: brainCtx.systemPrompt,
|
|
254
|
+
};
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// =========================================================================
|
|
259
|
+
// Test A: Conversational role teaching in Teach mode
|
|
260
|
+
// =========================================================================
|
|
261
|
+
it('Test A: teaches companion role in Teach mode, approves proposal, and reflects role in Active Self', async () => {
|
|
262
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
263
|
+
const companionId = 'comp-test-a';
|
|
264
|
+
const self = createSelfRepository(db);
|
|
265
|
+
const memory = createMemoryOrgan(db);
|
|
266
|
+
const behavior = createBehaviorCompiler();
|
|
267
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
268
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
269
|
+
brain: mockBrain,
|
|
270
|
+
memory,
|
|
271
|
+
self,
|
|
272
|
+
behavior,
|
|
273
|
+
});
|
|
274
|
+
await runtime.initialize();
|
|
275
|
+
// 1. Send conversational role teaching message
|
|
276
|
+
const perceptionResult = await runtime.processPerception({
|
|
277
|
+
source: 'text_chat',
|
|
278
|
+
text: 'Siduri, you are an AI researcher at VXNUS Studio.',
|
|
279
|
+
context: createRequestContext(companionId, 'teach'),
|
|
280
|
+
});
|
|
281
|
+
expect(perceptionResult.status).toBe('APPROVED');
|
|
282
|
+
// 2. Verify proposal was generated
|
|
283
|
+
const proposals = perceptionResult.metadata?.proposals || [];
|
|
284
|
+
expect(proposals.length).toBeGreaterThanOrEqual(1);
|
|
285
|
+
const roleProposal = proposals.find((p) => p.predicate === 'role' || p.predicate === 'archetype');
|
|
286
|
+
expect(roleProposal).toBeDefined();
|
|
287
|
+
expect(roleProposal.value).toBe('AI researcher at VXNUS Studio');
|
|
288
|
+
expect(roleProposal.status).toBe('pending');
|
|
289
|
+
// 3. Truth Gate check: Before approval, SelfRepository must NOT contain the new role
|
|
290
|
+
const identityBefore = await self.getIdentity(companionId);
|
|
291
|
+
expect(identityBefore?.role).toBeUndefined();
|
|
292
|
+
expect(identityBefore?.archetype).toBeUndefined();
|
|
293
|
+
// 4. Operator / Owner approves the proposal
|
|
294
|
+
const approveResult = await runtime.approveProposal(roleProposal.id, { companionId });
|
|
295
|
+
expect(approveResult.success).toBe(true);
|
|
296
|
+
// 5. Verify SelfRepository now has the learned role
|
|
297
|
+
const identityAfter = await self.getIdentity(companionId);
|
|
298
|
+
expect(identityAfter).toBeDefined();
|
|
299
|
+
expect(identityAfter?.role).toBe('AI researcher at VXNUS Studio');
|
|
300
|
+
expect(identityAfter?.archetype).toBe('AI researcher at VXNUS Studio');
|
|
301
|
+
// Also verify a relational directive acknowledging the role was added
|
|
302
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
303
|
+
const roleDirective = directives.find((d) => d.directive.includes('AI researcher at VXNUS Studio'));
|
|
304
|
+
expect(roleDirective).toBeDefined();
|
|
305
|
+
// 6. Next conversational turn: verify prompt compiler injects the updated Active Self
|
|
306
|
+
await runtime.processPerception({
|
|
307
|
+
source: 'text_chat',
|
|
308
|
+
text: 'What is your primary mission?',
|
|
309
|
+
context: createRequestContext(companionId, 'hybrid'),
|
|
310
|
+
});
|
|
311
|
+
expect(mockBrain.generatePlan).toHaveBeenCalled();
|
|
312
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
|
|
313
|
+
expect(lastCallCtx.systemPrompt).toContain('<active_self>');
|
|
314
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: AI researcher at VXNUS Studio');
|
|
315
|
+
db.close();
|
|
316
|
+
});
|
|
317
|
+
// =========================================================================
|
|
318
|
+
// Test B: Creator relationship teaching
|
|
319
|
+
// =========================================================================
|
|
320
|
+
it('Test B: teaches creator relationship, approves proposal, and updates relationship stance', async () => {
|
|
321
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
322
|
+
const companionId = 'comp-test-b';
|
|
323
|
+
const self = createSelfRepository(db);
|
|
324
|
+
const memory = createMemoryOrgan(db);
|
|
325
|
+
const behavior = createBehaviorCompiler();
|
|
326
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
327
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
328
|
+
brain: mockBrain,
|
|
329
|
+
memory,
|
|
330
|
+
self,
|
|
331
|
+
behavior,
|
|
332
|
+
});
|
|
333
|
+
await runtime.initialize();
|
|
334
|
+
// 1. Send creator teaching message
|
|
335
|
+
const perceptionResult = await runtime.processPerception({
|
|
336
|
+
source: 'text_chat',
|
|
337
|
+
text: 'I am your creator, Kur Zagin.',
|
|
338
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
339
|
+
});
|
|
340
|
+
const proposals = perceptionResult.metadata?.proposals || [];
|
|
341
|
+
const relProposal = proposals.find((p) => p.predicate === 'stated_relationship' ||
|
|
342
|
+
p.predicate === 'relationship' ||
|
|
343
|
+
p.claimType === 'relationship');
|
|
344
|
+
expect(relProposal).toBeDefined();
|
|
345
|
+
expect(relProposal.value).toBe('creator');
|
|
346
|
+
// 2. Truth Gate: Before approval, relationship does not have creator stance
|
|
347
|
+
const relBefore = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
348
|
+
expect(relBefore?.role).toBeUndefined();
|
|
349
|
+
// 3. Approve the relationship proposal
|
|
350
|
+
await runtime.approveProposal(relProposal.id, { companionId });
|
|
351
|
+
// 4. Verify SelfRepository now has creator relationship with loyal stance
|
|
352
|
+
const relAfter = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
353
|
+
expect(relAfter).toBeDefined();
|
|
354
|
+
expect(relAfter?.role).toBe('creator');
|
|
355
|
+
expect(relAfter?.stance).toBe('familiar_loyal');
|
|
356
|
+
expect(relAfter?.trustScore).toBe(1.0);
|
|
357
|
+
// 5. Subsequent conversation retrieves relationship in prompt
|
|
358
|
+
await runtime.processPerception({
|
|
359
|
+
source: 'text_chat',
|
|
360
|
+
text: 'Status update please.',
|
|
361
|
+
context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
|
|
362
|
+
});
|
|
363
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
|
|
364
|
+
expect(lastCallCtx.systemPrompt).toContain('Stance toward actor:kur-zagin: familiar_loyal (Role: creator)');
|
|
365
|
+
db.close();
|
|
366
|
+
});
|
|
367
|
+
// =========================================================================
|
|
368
|
+
// Test C: Behavioral rule teaching
|
|
369
|
+
// =========================================================================
|
|
370
|
+
it('Test C: teaches behavioral rule, generates proposal, approves, and activates directive in Self', async () => {
|
|
371
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
372
|
+
const companionId = 'comp-test-c';
|
|
373
|
+
const self = createSelfRepository(db);
|
|
374
|
+
const memory = createMemoryOrgan(db);
|
|
375
|
+
const behavior = createBehaviorCompiler();
|
|
376
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
377
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
378
|
+
brain: mockBrain,
|
|
379
|
+
memory,
|
|
380
|
+
self,
|
|
381
|
+
behavior,
|
|
382
|
+
});
|
|
383
|
+
await runtime.initialize();
|
|
384
|
+
// 1. Send behavioral rule teaching message
|
|
385
|
+
const perceptionResult = await runtime.processPerception({
|
|
386
|
+
source: 'text_chat',
|
|
387
|
+
text: 'Remember this rule: be concise when answering technical questions.',
|
|
388
|
+
context: createRequestContext(companionId, 'teach'),
|
|
389
|
+
});
|
|
390
|
+
// Check behavioral proposals or memory proposals
|
|
391
|
+
const behavioralReceipts = perceptionResult.metadata?.behavioral_proposals || [];
|
|
392
|
+
const memoryProposals = perceptionResult.metadata?.proposals || [];
|
|
393
|
+
const hasDirectiveProposal = behavioralReceipts.length > 0 ||
|
|
394
|
+
memoryProposals.some((p) => p.predicate === 'rule' || p.predicate === 'behavioral_rule');
|
|
395
|
+
expect(hasDirectiveProposal).toBe(true);
|
|
396
|
+
const directiveId = behavioralReceipts[0]?.directive_id;
|
|
397
|
+
const proposalId = memoryProposals[0]?.id;
|
|
398
|
+
// 2. Active directives before approval must not include the new rule
|
|
399
|
+
const directivesBefore = await self.getActiveDirectives(companionId);
|
|
400
|
+
expect(directivesBefore.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(false);
|
|
401
|
+
// 3. Approve directive via runtime
|
|
402
|
+
if (directiveId) {
|
|
403
|
+
await runtime.approveDirective(directiveId, { companionId });
|
|
404
|
+
}
|
|
405
|
+
else if (proposalId) {
|
|
406
|
+
await runtime.approveProposal(proposalId, { companionId });
|
|
407
|
+
}
|
|
408
|
+
// 4. Verify directive is now ACTIVE in SelfRepository
|
|
409
|
+
const directivesAfter = await self.getActiveDirectives(companionId);
|
|
410
|
+
expect(directivesAfter.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(true);
|
|
411
|
+
db.close();
|
|
412
|
+
});
|
|
413
|
+
// =========================================================================
|
|
414
|
+
// Test D: Casual banter in Casual mode (Zero Memory Drift)
|
|
415
|
+
// =========================================================================
|
|
416
|
+
it('Test D: casual banter in casual mode causes Zero Memory Drift and does NOT mutate Self', async () => {
|
|
417
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
418
|
+
const companionId = 'comp-test-d';
|
|
419
|
+
const self = createSelfRepository(db);
|
|
420
|
+
const memory = createMemoryOrgan(db);
|
|
421
|
+
const mockBrain = {
|
|
422
|
+
generatePlan: jest.fn().mockResolvedValue({
|
|
423
|
+
speech: 'Haha, thank you! I try my best.',
|
|
424
|
+
language: 'en',
|
|
425
|
+
}),
|
|
426
|
+
};
|
|
427
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
428
|
+
brain: mockBrain,
|
|
429
|
+
memory,
|
|
430
|
+
self,
|
|
431
|
+
});
|
|
432
|
+
await runtime.initialize();
|
|
433
|
+
// "You are probably the funniest AI I've ever talked to." in casual mode
|
|
434
|
+
const perceptionResult = await runtime.processPerception({
|
|
435
|
+
source: 'text_chat',
|
|
436
|
+
text: "You are probably the funniest AI I've ever talked to.",
|
|
437
|
+
context: createRequestContext(companionId, 'casual'),
|
|
438
|
+
});
|
|
439
|
+
expect(perceptionResult.status).toBe('APPROVED');
|
|
440
|
+
// Zero proposals allowed in casual mode
|
|
441
|
+
expect(perceptionResult.metadata?.proposals).toEqual([]);
|
|
442
|
+
expect(perceptionResult.metadata?.memory_proposals).toEqual([]);
|
|
443
|
+
// Self must remain unchanged
|
|
444
|
+
const identity = await self.getIdentity(companionId);
|
|
445
|
+
expect(identity?.role).toBeUndefined();
|
|
446
|
+
expect(identity?.archetype).toBeUndefined();
|
|
447
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
448
|
+
expect(directives).toHaveLength(0);
|
|
449
|
+
db.close();
|
|
450
|
+
});
|
|
451
|
+
// =========================================================================
|
|
452
|
+
// Test E: Approval gating: rejection leaves Self unchanged
|
|
453
|
+
// =========================================================================
|
|
454
|
+
it('Test E: rejecting a proposal prevents mutation of Self', async () => {
|
|
455
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
456
|
+
const companionId = 'comp-test-e';
|
|
457
|
+
const self = createSelfRepository(db);
|
|
458
|
+
const memory = createMemoryOrgan(db);
|
|
459
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
460
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
461
|
+
brain: mockBrain,
|
|
462
|
+
memory,
|
|
463
|
+
self,
|
|
464
|
+
});
|
|
465
|
+
await runtime.initialize();
|
|
466
|
+
// 1. Propose a role change
|
|
467
|
+
const perceptionResult = await runtime.processPerception({
|
|
468
|
+
source: 'text_chat',
|
|
469
|
+
text: 'Siduri, your role is Security Officer.',
|
|
470
|
+
context: createRequestContext(companionId, 'teach'),
|
|
471
|
+
});
|
|
472
|
+
const proposal = perceptionResult.metadata?.proposals?.[0];
|
|
473
|
+
expect(proposal).toBeDefined();
|
|
474
|
+
// 2. Reject the proposal
|
|
475
|
+
await runtime.rejectProposal(proposal.id, { companionId });
|
|
476
|
+
// 3. Verify Self is NOT mutated
|
|
477
|
+
const identity = await self.getIdentity(companionId);
|
|
478
|
+
expect(identity?.role).toBeUndefined();
|
|
479
|
+
expect(identity?.archetype).toBeUndefined();
|
|
480
|
+
db.close();
|
|
481
|
+
});
|
|
482
|
+
// =========================================================================
|
|
483
|
+
// Test F: Cross-restart durability (survives runtime restart)
|
|
484
|
+
// =========================================================================
|
|
485
|
+
it('Test F: persists learned state across runtime destruction and recreation', async () => {
|
|
486
|
+
const companionId = 'comp-test-f';
|
|
487
|
+
// --- Phase 1: Runtime 1 teaches and approves ---
|
|
488
|
+
{
|
|
489
|
+
const db1 = new index_1.SiduriDatabase({ dbPath });
|
|
490
|
+
const self1 = createSelfRepository(db1);
|
|
491
|
+
const memory1 = createMemoryOrgan(db1);
|
|
492
|
+
const mockBrain1 = createCognitiveMockBrain(companionId);
|
|
493
|
+
const runtime1 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
494
|
+
brain: mockBrain1,
|
|
495
|
+
memory: memory1,
|
|
496
|
+
self: self1,
|
|
497
|
+
});
|
|
498
|
+
await runtime1.initialize();
|
|
499
|
+
const res = await runtime1.processPerception({
|
|
500
|
+
source: 'text_chat',
|
|
501
|
+
text: 'Siduri, you are the Lead Architect at VXNUS Studio.',
|
|
502
|
+
context: createRequestContext(companionId, 'teach'),
|
|
503
|
+
});
|
|
504
|
+
const roleProposal = res.metadata?.proposals?.find((p) => p.predicate === 'role');
|
|
505
|
+
expect(roleProposal).toBeDefined();
|
|
506
|
+
await runtime1.approveProposal(roleProposal.id, { companionId });
|
|
507
|
+
// Verify in runtime 1
|
|
508
|
+
const id1 = await self1.getIdentity(companionId);
|
|
509
|
+
expect(id1?.role).toBe('Lead Architect at VXNUS Studio');
|
|
510
|
+
// Close and destroy runtime 1 completely
|
|
511
|
+
db1.close();
|
|
512
|
+
}
|
|
513
|
+
// --- Phase 2: Runtime 2 initialized afresh pointing to same SQLite database ---
|
|
514
|
+
{
|
|
515
|
+
const db2 = new index_1.SiduriDatabase({ dbPath });
|
|
516
|
+
const self2 = createSelfRepository(db2);
|
|
517
|
+
const memory2 = createMemoryOrgan(db2);
|
|
518
|
+
const behavior2 = createBehaviorCompiler();
|
|
519
|
+
const mockBrain2 = createCognitiveMockBrain(companionId);
|
|
520
|
+
const runtime2 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
521
|
+
brain: mockBrain2,
|
|
522
|
+
memory: memory2,
|
|
523
|
+
self: self2,
|
|
524
|
+
behavior: behavior2,
|
|
525
|
+
});
|
|
526
|
+
await runtime2.initialize();
|
|
527
|
+
// Verify that runtime 2 retrieves the learned role from SQLite!
|
|
528
|
+
const id2 = await self2.getIdentity(companionId);
|
|
529
|
+
expect(id2).toBeDefined();
|
|
530
|
+
expect(id2?.role).toBe('Lead Architect at VXNUS Studio');
|
|
531
|
+
expect(id2?.archetype).toBe('Lead Architect at VXNUS Studio');
|
|
532
|
+
// Verify that conversation in runtime 2 retrieves the learned role in prompt compilation
|
|
533
|
+
await runtime2.processPerception({
|
|
534
|
+
source: 'text_chat',
|
|
535
|
+
text: 'What do you do?',
|
|
536
|
+
context: createRequestContext(companionId, 'hybrid'),
|
|
537
|
+
});
|
|
538
|
+
const lastCallCtx = mockBrain2.generatePlan.mock.calls[0][0];
|
|
539
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: Lead Architect at VXNUS Studio');
|
|
540
|
+
db2.close();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
// =========================================================================
|
|
544
|
+
// Test G: Idempotency (duplicate approval does not corrupt database)
|
|
545
|
+
// =========================================================================
|
|
546
|
+
it('Test G: approving the same proposal twice is idempotent and does not create duplicate entries', async () => {
|
|
547
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
548
|
+
const companionId = 'comp-test-g';
|
|
549
|
+
const self = createSelfRepository(db);
|
|
550
|
+
const memory = createMemoryOrgan(db);
|
|
551
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
552
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
553
|
+
brain: mockBrain,
|
|
554
|
+
memory,
|
|
555
|
+
self,
|
|
556
|
+
});
|
|
557
|
+
await runtime.initialize();
|
|
558
|
+
const res = await runtime.processPerception({
|
|
559
|
+
source: 'text_chat',
|
|
560
|
+
text: 'Siduri, you are a Research Specialist.',
|
|
561
|
+
context: createRequestContext(companionId, 'teach'),
|
|
562
|
+
});
|
|
563
|
+
const proposal = res.metadata?.proposals?.[0];
|
|
564
|
+
expect(proposal).toBeDefined();
|
|
565
|
+
// Approve once
|
|
566
|
+
await runtime.approveProposal(proposal.id, { companionId });
|
|
567
|
+
const idFirst = await self.getIdentity(companionId);
|
|
568
|
+
expect(idFirst?.role).toBe('Research Specialist');
|
|
569
|
+
// Approve a second time (should be completely idempotent and not error)
|
|
570
|
+
await runtime.approveProposal(proposal.id, { companionId });
|
|
571
|
+
const idSecond = await self.getIdentity(companionId);
|
|
572
|
+
expect(idSecond?.role).toBe('Research Specialist');
|
|
573
|
+
// Check directives: should only have one directive for acknowledging the role
|
|
574
|
+
const directives = await self.getActiveDirectives(companionId);
|
|
575
|
+
const roleDirectives = directives.filter((d) => d.directive.includes('Research Specialist'));
|
|
576
|
+
expect(roleDirectives).toHaveLength(1);
|
|
577
|
+
db.close();
|
|
578
|
+
});
|
|
579
|
+
// =========================================================================
|
|
580
|
+
// Test H: Natural conversational teaching (past-tense role, user name, creator)
|
|
581
|
+
// =========================================================================
|
|
582
|
+
it('Test H: teaches companion past role ("she was VXNUS Studio Staff"), creator, and name, then recognizes user on subsequent turn', async () => {
|
|
583
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
584
|
+
const companionId = 'comp-test-h';
|
|
585
|
+
const self = createSelfRepository(db);
|
|
586
|
+
const memory = createMemoryOrgan(db);
|
|
587
|
+
const behavior = createBehaviorCompiler();
|
|
588
|
+
const mockBrain = createCognitiveMockBrain(companionId);
|
|
589
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
590
|
+
brain: mockBrain,
|
|
591
|
+
memory,
|
|
592
|
+
self,
|
|
593
|
+
behavior,
|
|
594
|
+
});
|
|
595
|
+
await runtime.initialize();
|
|
596
|
+
// 1. "she was VXNUS Studio Staff" in Teach mode
|
|
597
|
+
const resRole = await runtime.processPerception({
|
|
598
|
+
source: 'text_chat',
|
|
599
|
+
text: 'She was VXNUS Studio Staff.',
|
|
600
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
601
|
+
});
|
|
602
|
+
expect(resRole.status).toBe('APPROVED');
|
|
603
|
+
const roleProp = resRole.metadata?.proposals?.find((p) => p.predicate === 'role');
|
|
604
|
+
expect(roleProp).toBeDefined();
|
|
605
|
+
expect(roleProp.value).toBe('VXNUS Studio Staff');
|
|
606
|
+
await runtime.approveProposal(roleProp.id, { companionId });
|
|
607
|
+
// 2. "my name is Kur Zagin" in Teach mode
|
|
608
|
+
const resName = await runtime.processPerception({
|
|
609
|
+
source: 'text_chat',
|
|
610
|
+
text: 'My name is Kur Zagin.',
|
|
611
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
612
|
+
});
|
|
613
|
+
expect(resName.status).toBe('APPROVED');
|
|
614
|
+
const nameProp = resName.metadata?.proposals?.find((p) => p.predicate === 'name');
|
|
615
|
+
expect(nameProp).toBeDefined();
|
|
616
|
+
expect(nameProp.value).toBe('Kur Zagin');
|
|
617
|
+
await runtime.approveProposal(nameProp.id, { companionId });
|
|
618
|
+
// 3. "I am your creator" in Teach mode
|
|
619
|
+
const resRel = await runtime.processPerception({
|
|
620
|
+
source: 'text_chat',
|
|
621
|
+
text: 'I am your creator.',
|
|
622
|
+
context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
|
|
623
|
+
});
|
|
624
|
+
expect(resRel.status).toBe('APPROVED');
|
|
625
|
+
const relProp = resRel.metadata?.proposals?.find((p) => p.predicate === 'stated_relationship');
|
|
626
|
+
expect(relProp).toBeDefined();
|
|
627
|
+
await runtime.approveProposal(relProp.id, { companionId });
|
|
628
|
+
// 4. Verify Self relationship has both creator stance AND user name preserved
|
|
629
|
+
const rel = await self.getRelationship?.(companionId, 'actor:kur-zagin');
|
|
630
|
+
expect(rel).toBeDefined();
|
|
631
|
+
expect(rel?.role).toBe('creator');
|
|
632
|
+
expect(rel?.stance).toBe('familiar_loyal');
|
|
633
|
+
expect(rel?.name).toBe('Kur Zagin');
|
|
634
|
+
// 5. Subsequent conversation turn: "do you know me?"
|
|
635
|
+
await runtime.processPerception({
|
|
636
|
+
source: 'text_chat',
|
|
637
|
+
text: 'do you know me?',
|
|
638
|
+
context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
|
|
639
|
+
});
|
|
640
|
+
const lastCallCtx = mockBrain.generatePlan.mock.calls[mockBrain.generatePlan.mock.calls.length - 1][0];
|
|
641
|
+
// Active Self contains both the creator stance and the user's name
|
|
642
|
+
expect(lastCallCtx.systemPrompt).toContain('Role: VXNUS Studio Staff');
|
|
643
|
+
expect(lastCallCtx.systemPrompt).toContain('familiar_loyal');
|
|
644
|
+
expect(lastCallCtx.systemPrompt).toContain('Kur Zagin');
|
|
645
|
+
db.close();
|
|
646
|
+
});
|
|
647
|
+
// =========================================================================
|
|
648
|
+
// Test I: Benchmark replication (name, creator, new chat turn-1 recognition)
|
|
649
|
+
// =========================================================================
|
|
650
|
+
it('Test I: reproduces benchmark scenario: teaches name, teaches creator relationship, and verifies new session recognizes creator in origin and relationship', async () => {
|
|
651
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
652
|
+
const companionId = 'comp-test-i';
|
|
653
|
+
const self = createSelfRepository(db);
|
|
654
|
+
const memory = createMemoryOrgan(db);
|
|
655
|
+
const behavior = createBehaviorCompiler();
|
|
656
|
+
const mockBrain = {
|
|
657
|
+
generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
|
|
658
|
+
const lastMsg = brainCtx.recentMessages?.[brainCtx.recentMessages.length - 1]?.content || '';
|
|
659
|
+
const memoryProposals = [];
|
|
660
|
+
const behaviorProposals = [];
|
|
661
|
+
if (/your name is Siduri/i.test(lastMsg)) {
|
|
662
|
+
memoryProposals.push({
|
|
663
|
+
subject: 'companion:self',
|
|
664
|
+
predicate: 'name',
|
|
665
|
+
value: 'Siduri',
|
|
666
|
+
});
|
|
667
|
+
behaviorProposals.push({
|
|
668
|
+
directive: 'Address self as Siduri',
|
|
669
|
+
category: 'relational',
|
|
670
|
+
subject: 'companion:self',
|
|
671
|
+
predicate: 'name',
|
|
672
|
+
value: 'Siduri',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (/i am Kur Zagin, your creator/i.test(lastMsg)) {
|
|
676
|
+
memoryProposals.push({
|
|
677
|
+
subject: 'actor:kur_zagin',
|
|
678
|
+
predicate: 'name',
|
|
679
|
+
value: 'Kur Zagin',
|
|
680
|
+
});
|
|
681
|
+
memoryProposals.push({
|
|
682
|
+
subject: 'actor:kur_zagin',
|
|
683
|
+
predicate: 'stated_relationship',
|
|
684
|
+
value: 'creator of companion Siduri',
|
|
685
|
+
});
|
|
686
|
+
behaviorProposals.push({
|
|
687
|
+
directive: 'Acknowledge actor:kur_zagin as creator of companion Siduri',
|
|
688
|
+
category: 'relational',
|
|
689
|
+
subject: 'actor:kur_zagin',
|
|
690
|
+
predicate: 'stated_relationship',
|
|
691
|
+
value: 'creator of companion Siduri',
|
|
692
|
+
});
|
|
693
|
+
behaviorProposals.push({
|
|
694
|
+
directive: 'Address actor:kur_zagin as Kur Zagin',
|
|
695
|
+
category: 'behavioral',
|
|
696
|
+
subject: 'actor:kur_zagin',
|
|
697
|
+
predicate: 'name',
|
|
698
|
+
value: 'Kur Zagin',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
speech: 'I understand and acknowledge.',
|
|
703
|
+
language: 'en',
|
|
704
|
+
memoryProposals: memoryProposals.length > 0 ? memoryProposals : undefined,
|
|
705
|
+
behaviorProposals: behaviorProposals.length > 0 ? behaviorProposals : undefined,
|
|
706
|
+
_receivedSystemPrompt: brainCtx.systemPrompt,
|
|
707
|
+
};
|
|
708
|
+
}),
|
|
709
|
+
};
|
|
710
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
711
|
+
brain: mockBrain,
|
|
712
|
+
memory,
|
|
713
|
+
self,
|
|
714
|
+
behavior,
|
|
715
|
+
});
|
|
716
|
+
await runtime.initialize();
|
|
717
|
+
// Session 1 - Turn 1: "your name is Siduri"
|
|
718
|
+
const res1 = await runtime.processPerception({
|
|
719
|
+
source: 'text_chat',
|
|
720
|
+
text: 'your name is Siduri',
|
|
721
|
+
context: createRequestContext(companionId, 'teach', 'owner-user'),
|
|
722
|
+
});
|
|
723
|
+
expect(res1.status).toBe('APPROVED');
|
|
724
|
+
const nameProp = res1.metadata?.proposals?.find((p) => p.predicate === 'name');
|
|
725
|
+
expect(nameProp).toBeDefined();
|
|
726
|
+
await runtime.approveProposal(nameProp.id, { companionId });
|
|
727
|
+
// Verify companion name in self_identity
|
|
728
|
+
const idStep1 = await self.getIdentity(companionId);
|
|
729
|
+
expect(idStep1?.name).toBe('Siduri');
|
|
730
|
+
// Session 1 - Turn 2: "i am Kur Zagin, your creator"
|
|
731
|
+
const res2 = await runtime.processPerception({
|
|
732
|
+
source: 'text_chat',
|
|
733
|
+
text: 'i am Kur Zagin, your creator',
|
|
734
|
+
context: createRequestContext(companionId, 'teach', 'owner-user'),
|
|
735
|
+
});
|
|
736
|
+
expect(res2.status).toBe('APPROVED');
|
|
737
|
+
// Approve both proposals (name and creator of companion Siduri)
|
|
738
|
+
const proposals2 = res2.metadata?.proposals || [];
|
|
739
|
+
const creatorProp = proposals2.find((p) => p.predicate === 'stated_relationship');
|
|
740
|
+
const userProp = proposals2.find((p) => p.predicate === 'name');
|
|
741
|
+
expect(creatorProp).toBeDefined();
|
|
742
|
+
expect(userProp).toBeDefined();
|
|
743
|
+
await runtime.approveProposal(creatorProp.id, { companionId });
|
|
744
|
+
await runtime.approveProposal(userProp.id, { companionId });
|
|
745
|
+
// Also approve the behavioral directives
|
|
746
|
+
const dirProps = res2.metadata?.behavioral_proposals || [];
|
|
747
|
+
for (const d of dirProps) {
|
|
748
|
+
if (d.directive_id) {
|
|
749
|
+
await runtime.approveDirective(d.directive_id, { companionId });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Verify self_identity.origin has been dual-promoted!
|
|
753
|
+
const idStep2 = await self.getIdentity(companionId);
|
|
754
|
+
expect(idStep2?.name).toBe('Siduri');
|
|
755
|
+
expect(idStep2?.origin).toBe('Kur Zagin');
|
|
756
|
+
// Verify relationship has creator role, loyal stance, and user name
|
|
757
|
+
const relStep2 = await self.getRelationship(companionId, 'actor:kur_zagin');
|
|
758
|
+
expect(relStep2).toBeDefined();
|
|
759
|
+
expect(relStep2?.role).toBe('creator');
|
|
760
|
+
expect(relStep2?.stance).toBe('familiar_loyal');
|
|
761
|
+
expect(relStep2?.trustScore).toBe(1.0);
|
|
762
|
+
expect(relStep2?.name).toBe('Kur Zagin');
|
|
763
|
+
// Verify single-owner fallback: an incoming request with 'owner-user' gets the primary relationship!
|
|
764
|
+
const relOwnerUser = await self.getRelationship(companionId, 'owner-user');
|
|
765
|
+
expect(relOwnerUser).toBeDefined();
|
|
766
|
+
expect(relOwnerUser?.name).toBe('Kur Zagin');
|
|
767
|
+
expect(relOwnerUser?.role).toBe('creator');
|
|
768
|
+
expect(relOwnerUser?.stance).toBe('familiar_loyal');
|
|
769
|
+
// =======================================================================
|
|
770
|
+
// Session 2 ("New Chat"): user opens a fresh session as 'owner-user'
|
|
771
|
+
// =======================================================================
|
|
772
|
+
// Turn 1: "hey, who are you?"
|
|
773
|
+
await runtime.processPerception({
|
|
774
|
+
source: 'text_chat',
|
|
775
|
+
text: 'hey, who are you?',
|
|
776
|
+
context: createRequestContext(companionId, 'hybrid', 'owner-user'),
|
|
777
|
+
});
|
|
778
|
+
const calls = mockBrain.generatePlan.mock.calls;
|
|
779
|
+
const newChatTurn1Ctx = calls[calls.length - 1][0];
|
|
780
|
+
// Identity nucleus has origin
|
|
781
|
+
expect(newChatTurn1Ctx.systemPrompt).toContain('Name: Siduri');
|
|
782
|
+
expect(newChatTurn1Ctx.systemPrompt).toContain('Origin/Created By: Kur Zagin');
|
|
783
|
+
// Relationship stance recognizes Kur Zagin as creator
|
|
784
|
+
expect(newChatTurn1Ctx.systemPrompt).toContain('Kur Zagin');
|
|
785
|
+
expect(newChatTurn1Ctx.systemPrompt).toContain('familiar_loyal');
|
|
786
|
+
// Turn 2: "who is your creator?"
|
|
787
|
+
await runtime.processPerception({
|
|
788
|
+
source: 'text_chat',
|
|
789
|
+
text: 'who is your creator?',
|
|
790
|
+
context: createRequestContext(companionId, 'hybrid', 'owner-user'),
|
|
791
|
+
});
|
|
792
|
+
const newChatTurn2Ctx = calls[calls.length - 1][0];
|
|
793
|
+
expect(newChatTurn2Ctx.systemPrompt).toContain('Origin/Created By: Kur Zagin');
|
|
794
|
+
// Turn 3: From an anonymous guest session:
|
|
795
|
+
// Should NOT get the personal relationship stance, but Identity origin remains!
|
|
796
|
+
await runtime.processPerception({
|
|
797
|
+
source: 'text_chat',
|
|
798
|
+
text: 'who is your creator?',
|
|
799
|
+
context: createRequestContext(companionId, 'hybrid', 'anonymous-session'),
|
|
800
|
+
});
|
|
801
|
+
const guestTurnCtx = calls[calls.length - 1][0];
|
|
802
|
+
expect(guestTurnCtx.systemPrompt).toContain('Origin/Created By: Kur Zagin');
|
|
803
|
+
expect(guestTurnCtx.systemPrompt).not.toContain('Stance toward anonymous-session [Name: Kur Zagin]');
|
|
804
|
+
// Turn 4: "who am i?" from owner session:
|
|
805
|
+
// Approved memory claims are retrieved in MEMORY: context even if FTS search yielded 0 keyword matches
|
|
806
|
+
await runtime.processPerception({
|
|
807
|
+
source: 'text_chat',
|
|
808
|
+
text: 'who am i?',
|
|
809
|
+
context: createRequestContext(companionId, 'hybrid', 'owner-user'),
|
|
810
|
+
});
|
|
811
|
+
const whoAmICtx = calls[calls.length - 1][0];
|
|
812
|
+
expect(whoAmICtx.contextPrompt).toContain('MEMORY:');
|
|
813
|
+
expect(whoAmICtx.contextPrompt).toContain('Kur Zagin');
|
|
814
|
+
db.close();
|
|
815
|
+
});
|
|
816
|
+
it('promotes approved Life / Knowledge proposals to sovereign Life DB via Truth Gate', async () => {
|
|
817
|
+
const companionId = 'comp-truth-knowledge';
|
|
818
|
+
const db = new index_1.SiduriDatabase({ dbPath });
|
|
819
|
+
const self = createSelfRepository(db);
|
|
820
|
+
const memory = createMemoryOrgan(db);
|
|
821
|
+
const mockKnowledge = {
|
|
822
|
+
entities: {
|
|
823
|
+
saveEntity: jest.fn(async (entity) => db.upsertEntity(entity)),
|
|
824
|
+
},
|
|
825
|
+
events: {
|
|
826
|
+
addEvent: jest.fn(async (event) => db.addEvent(event)),
|
|
827
|
+
},
|
|
828
|
+
tasks: {
|
|
829
|
+
saveTask: jest.fn(async (task) => db.upsertTask(task)),
|
|
830
|
+
},
|
|
831
|
+
schedule: {
|
|
832
|
+
saveItem: jest.fn(async (sch) => db.upsertScheduleItem(sch)),
|
|
833
|
+
},
|
|
834
|
+
preferences: {
|
|
835
|
+
setPreference: jest.fn(async (pref) => db.upsertPreference(pref)),
|
|
836
|
+
},
|
|
837
|
+
queryContext: jest.fn(async () => []),
|
|
838
|
+
};
|
|
839
|
+
const runtime = new index_1.SiduriRuntime(companionId, {
|
|
840
|
+
name: 'Siduri',
|
|
841
|
+
initialMode: 'teach',
|
|
842
|
+
}, {
|
|
843
|
+
self,
|
|
844
|
+
memory,
|
|
845
|
+
knowledge: mockKnowledge,
|
|
846
|
+
});
|
|
847
|
+
// 1. Propose knowledge claims in memory store (quarantined as PENDING)
|
|
848
|
+
const entityClaim = await memory.proposeClaim({
|
|
849
|
+
companionId,
|
|
850
|
+
subject: 'entity:Workstation Rig',
|
|
851
|
+
predicate: 'inventory',
|
|
852
|
+
value: 'Threadripper 64-core',
|
|
853
|
+
claimType: 'life_entity',
|
|
854
|
+
evidence: { domain: 'hardware', properties: { cores: 64, ram: '256GB' } },
|
|
855
|
+
});
|
|
856
|
+
const eventClaim = await memory.proposeClaim({
|
|
857
|
+
companionId,
|
|
858
|
+
subject: 'finance:expense',
|
|
859
|
+
predicate: 'expense',
|
|
860
|
+
value: '45.00',
|
|
861
|
+
claimType: 'life_event',
|
|
862
|
+
evidence: { metricValue: -45.0, stream: 'finance', category: 'dining' },
|
|
863
|
+
});
|
|
864
|
+
const taskClaim = await memory.proposeClaim({
|
|
865
|
+
companionId,
|
|
866
|
+
subject: 'task:Review PR',
|
|
867
|
+
predicate: 'todo',
|
|
868
|
+
value: 'Review PR #42 for Life DB',
|
|
869
|
+
claimType: 'life_task',
|
|
870
|
+
evidence: { status: 'in_progress' },
|
|
871
|
+
});
|
|
872
|
+
// Before approval, Life DB tables in sqlite are empty
|
|
873
|
+
expect(db.getEntities(companionId)).toHaveLength(0);
|
|
874
|
+
expect(db.getEvents(companionId)).toHaveLength(0);
|
|
875
|
+
expect(db.getTasks(companionId)).toHaveLength(0);
|
|
876
|
+
// 2. Truth Gate Approval
|
|
877
|
+
const resEntity = await runtime.approveProposal(entityClaim.id, { companionId });
|
|
878
|
+
expect(resEntity.success).toBe(true);
|
|
879
|
+
expect(resEntity.target).toBe('knowledge');
|
|
880
|
+
const resEvent = await runtime.approveProposal(eventClaim.id, { companionId });
|
|
881
|
+
expect(resEvent.success).toBe(true);
|
|
882
|
+
expect(resEvent.target).toBe('knowledge');
|
|
883
|
+
const resTask = await runtime.approveProposal(taskClaim.id, { companionId });
|
|
884
|
+
expect(resTask.success).toBe(true);
|
|
885
|
+
expect(resTask.target).toBe('knowledge');
|
|
886
|
+
// 3. Verify persistent commit to sovereign Life DB in SQLite
|
|
887
|
+
const entities = db.getEntities(companionId);
|
|
888
|
+
expect(entities).toHaveLength(1);
|
|
889
|
+
expect(entities[0].name).toBe('Workstation Rig');
|
|
890
|
+
expect(entities[0].domain).toBe('hardware');
|
|
891
|
+
const events = db.getEvents(companionId);
|
|
892
|
+
expect(events).toHaveLength(1);
|
|
893
|
+
expect(events[0].stream).toBe('finance');
|
|
894
|
+
expect(events[0].metricValue).toBe(-45.0);
|
|
895
|
+
const tasks = db.getTasks(companionId);
|
|
896
|
+
expect(tasks).toHaveLength(1);
|
|
897
|
+
expect(tasks[0].title).toBe('Review PR #42 for Life DB');
|
|
898
|
+
expect(tasks[0].status).toBe('in_progress');
|
|
899
|
+
db.close();
|
|
900
|
+
});
|
|
901
|
+
it('updates companion identity name dynamically when companion naming claim or directive is approved', async () => {
|
|
902
|
+
const companionId = 'dynamic-companion';
|
|
903
|
+
const db = new index_1.SiduriDatabase({ dbPath: ':memory:' });
|
|
904
|
+
const memory = createMemoryOrgan(db);
|
|
905
|
+
const self = createSelfRepository(db);
|
|
906
|
+
const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
|
|
907
|
+
memory,
|
|
908
|
+
self,
|
|
909
|
+
});
|
|
910
|
+
await runtime.initialize();
|
|
911
|
+
// Verify initial default identity
|
|
912
|
+
const initialIdentity = await self.getIdentity(companionId);
|
|
913
|
+
expect(initialIdentity?.name || 'Siduri').toBe('Siduri');
|
|
914
|
+
// 1. Propose and approve claim with subject "assistant" and predicate "name"
|
|
915
|
+
const nameClaim = await memory.proposeClaim({
|
|
916
|
+
companionId,
|
|
917
|
+
subject: 'assistant',
|
|
918
|
+
predicate: 'name',
|
|
919
|
+
value: 'Athena',
|
|
920
|
+
claimType: 'semantic',
|
|
921
|
+
});
|
|
922
|
+
await runtime.approveProposal(nameClaim.id, { companionId });
|
|
923
|
+
const identityAfterClaim = await self.getIdentity(companionId);
|
|
924
|
+
expect(identityAfterClaim?.name).toBe('Athena');
|
|
925
|
+
// 2. Propose and approve behavioral directive "Address companion as Athena Prime"
|
|
926
|
+
const dir = {
|
|
927
|
+
id: 'dir-name-test-1',
|
|
928
|
+
companionId,
|
|
929
|
+
directive: 'Address companion as Athena Prime',
|
|
930
|
+
priority: 80,
|
|
931
|
+
status: 'pending',
|
|
932
|
+
category: 'relational',
|
|
933
|
+
createdAt: new Date().toISOString(),
|
|
934
|
+
};
|
|
935
|
+
await self.commitDirectives(companionId, [dir]);
|
|
936
|
+
await runtime.approveDirective(dir.id, { companionId });
|
|
937
|
+
const identityAfterDirective = await self.getIdentity(companionId);
|
|
938
|
+
expect(identityAfterDirective?.name).toBe('Athena Prime');
|
|
939
|
+
db.close();
|
|
940
|
+
});
|
|
941
|
+
});
|