@siduri-x/core 2.0.5 → 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.
@@ -124,7 +124,8 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
124
124
  }
125
125
  }
126
126
  if (ctx.relationship) {
127
- parts.push(`Relationship Stance:\n- Stance toward ${ctx.relationship.entityId}: ${ctx.relationship.stance} (Role: ${ctx.relationship.role})`);
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})`);
128
129
  }
129
130
  if (ctx.directives && ctx.directives.length > 0) {
130
131
  parts.push(`Behavioral Directives:\n${ctx.directives.map((d) => `- ${d.directive}`).join('\n')}`);
@@ -150,6 +151,106 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
150
151
  },
151
152
  };
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
+ }
153
254
  // =========================================================================
154
255
  // Test A: Conversational role teaching in Teach mode
155
256
  // =========================================================================
@@ -159,16 +260,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
159
260
  const self = createSelfRepository(db);
160
261
  const memory = createMemoryOrgan(db);
161
262
  const behavior = createBehaviorCompiler();
162
- const mockBrain = {
163
- generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
164
- return {
165
- speech: 'Understood, I have acknowledged my role.',
166
- language: 'en',
167
- // Brain context should have received the system prompt
168
- _receivedSystemPrompt: brainCtx.systemPrompt,
169
- };
170
- }),
171
- };
263
+ const mockBrain = createCognitiveMockBrain(companionId);
172
264
  const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
173
265
  brain: mockBrain,
174
266
  memory,
@@ -227,12 +319,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
227
319
  const self = createSelfRepository(db);
228
320
  const memory = createMemoryOrgan(db);
229
321
  const behavior = createBehaviorCompiler();
230
- const mockBrain = {
231
- generatePlan: jest.fn().mockResolvedValue({
232
- speech: 'Greetings, Creator.',
233
- language: 'en',
234
- }),
235
- };
322
+ const mockBrain = createCognitiveMockBrain(companionId);
236
323
  const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
237
324
  brain: mockBrain,
238
325
  memory,
@@ -282,12 +369,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
282
369
  const self = createSelfRepository(db);
283
370
  const memory = createMemoryOrgan(db);
284
371
  const behavior = createBehaviorCompiler();
285
- const mockBrain = {
286
- generatePlan: jest.fn().mockResolvedValue({
287
- speech: 'Rule noted.',
288
- language: 'en',
289
- }),
290
- };
372
+ const mockBrain = createCognitiveMockBrain(companionId);
291
373
  const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
292
374
  brain: mockBrain,
293
375
  memory,
@@ -370,12 +452,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
370
452
  const companionId = 'comp-test-e';
371
453
  const self = createSelfRepository(db);
372
454
  const memory = createMemoryOrgan(db);
373
- const mockBrain = {
374
- generatePlan: jest.fn().mockResolvedValue({
375
- speech: 'Understood.',
376
- language: 'en',
377
- }),
378
- };
455
+ const mockBrain = createCognitiveMockBrain(companionId);
379
456
  const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
380
457
  brain: mockBrain,
381
458
  memory,
@@ -408,12 +485,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
408
485
  const db1 = new index_1.SiduriDatabase({ dbPath });
409
486
  const self1 = createSelfRepository(db1);
410
487
  const memory1 = createMemoryOrgan(db1);
411
- const mockBrain1 = {
412
- generatePlan: jest.fn().mockResolvedValue({
413
- speech: 'I have recorded my new role as Lead Architect.',
414
- language: 'en',
415
- }),
416
- };
488
+ const mockBrain1 = createCognitiveMockBrain(companionId);
417
489
  const runtime1 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
418
490
  brain: mockBrain1,
419
491
  memory: memory1,
@@ -440,12 +512,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
440
512
  const self2 = createSelfRepository(db2);
441
513
  const memory2 = createMemoryOrgan(db2);
442
514
  const behavior2 = createBehaviorCompiler();
443
- const mockBrain2 = {
444
- generatePlan: jest.fn().mockResolvedValue({
445
- speech: 'Ready to build architecture.',
446
- language: 'en',
447
- }),
448
- };
515
+ const mockBrain2 = createCognitiveMockBrain(companionId);
449
516
  const runtime2 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
450
517
  brain: mockBrain2,
451
518
  memory: memory2,
@@ -477,12 +544,7 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
477
544
  const companionId = 'comp-test-g';
478
545
  const self = createSelfRepository(db);
479
546
  const memory = createMemoryOrgan(db);
480
- const mockBrain = {
481
- generatePlan: jest.fn().mockResolvedValue({
482
- speech: 'Understood.',
483
- language: 'en',
484
- }),
485
- };
547
+ const mockBrain = createCognitiveMockBrain(companionId);
486
548
  const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
487
549
  brain: mockBrain,
488
550
  memory,
@@ -510,4 +572,72 @@ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
510
572
  expect(roleDirectives).toHaveLength(1);
511
573
  db.close();
512
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
+ });
513
643
  });
@@ -15,9 +15,9 @@ function classifyInputIntent(text, context, overrides) {
15
15
  const isTeachingLike = overrides?.isTeachingLike ??
16
16
  (explicitTeaching.claims.length > 0 ||
17
17
  explicitTeaching.behaviorProposals.length > 0 ||
18
- /\bremember that\b/.test(normalizedMessage));
18
+ /\b(?:remember that|my name is|call me)\b/i.test(normalizedMessage));
19
19
  const isSelfIdentityRequest = overrides?.isSelfIdentityRequest ??
20
- /\b(?:who|what) are you\b|\bwho is siduri\b|\b(?:your|my) name\b|\btell me about yourself\b|\bdescribe yourself\b|\bwhat is your origin\b|\bwho created you\b|\bwho made you\b|\bintroduce yourself\b/.test(normalizedMessage);
20
+ /\b(?:who|what) are you\b|\bwho is siduri\b|\b(?:your|my) name\b|\bdo you know me\b|\bwho am i\b|\btell me about yourself\b|\bdescribe yourself\b|\bwhat is your origin\b|\bwho created you\b|\bwho made you\b|\bintroduce yourself\b/.test(normalizedMessage);
21
21
  const isGreeting = overrides?.isGreeting ??
22
22
  /^(?:hello|hi|hey|greetings|good morning|good afternoon|good evening|howdy|yo)[.!]?$/.test(normalizedMessage);
23
23
  const shouldQueryKnowledge = overrides?.shouldQueryKnowledge ??
@@ -212,6 +212,13 @@ describe('SiduriRuntime Unified Perception Cycle & Session History', () => {
212
212
  generatePlan: jest.fn().mockResolvedValue({
213
213
  speech: 'I have recorded your preferred title as Chief Engineer.',
214
214
  language: 'en',
215
+ memoryProposals: [
216
+ {
217
+ subject: 'actor:alice',
218
+ predicate: 'preferred_address',
219
+ value: 'Chief Engineer',
220
+ },
221
+ ],
215
222
  }),
216
223
  };
217
224
  const runtime = new runtime_1.SiduriRuntime('comp-teach', { name: 'TeachBot' }, {
@@ -255,6 +262,13 @@ describe('SiduriRuntime Unified Perception Cycle & Session History', () => {
255
262
  generatePlan: jest.fn().mockResolvedValue({
256
263
  speech: 'Recorded the command.',
257
264
  language: 'en',
265
+ memoryProposals: [
266
+ {
267
+ subject: 'companion:comp-infer',
268
+ predicate: 'name',
269
+ value: 'Atlas',
270
+ },
271
+ ],
258
272
  }),
259
273
  };
260
274
  const runtime = new runtime_1.SiduriRuntime('comp-infer', { name: 'InferBot' }, {
@@ -49,7 +49,7 @@ async function compilePrompts(params) {
49
49
  const modeInstruction = effectiveMode === 'casual'
50
50
  ? 'Operating Mode: Casual (Zero memory drift - do not attempt to persist personal claims or directives).'
51
51
  : effectiveMode === 'teach'
52
- ? 'Operating Mode: Teach Mode (Active learning session - accurately capture user preferences and proposed boundaries for operator review).'
52
+ ? 'Operating Mode: Teach Mode (Active learning session - listen attentively to what the user shares about their identity, affiliations, relationship, or preferences, and companion identity/role. Accurately formulate candidate memoryProposals and behaviorProposals for review).'
53
53
  : undefined;
54
54
  const subtitleInstruction = subtitleLanguage && subtitleLanguage !== 'off'
55
55
  ? `Requested Subtitle Language: "${subtitleLanguage}". Along with your primary speech, provide a natural subtitle translation in "${subtitleLanguage}" in the subtitle field.`
package/dist/runtime.js CHANGED
@@ -264,25 +264,55 @@ async function promoteApprovedClaimToSelf(claim, self, companionId) {
264
264
  }
265
265
  return;
266
266
  }
267
- // 2. Relationship mutations: creator or user stated relationship
267
+ // 2. Relationship mutations: creator or user stated relationship, name, or affiliation
268
268
  if (claim.claimType === 'relationship' ||
269
269
  predicate === 'stated_relationship' ||
270
270
  predicate === 'relationship' ||
271
- predicate === 'relationship_to_siduri') {
271
+ predicate === 'relationship_to_siduri' ||
272
+ (predicate === 'name' && (subject.startsWith('actor:') || subject === 'user' || subject === 'primary_user')) ||
273
+ predicate === 'preferred_address' ||
274
+ predicate === 'affiliation') {
272
275
  const rawSubject = (claim.subject || 'actor:user').replace(/^actor:actor:/, 'actor:');
273
276
  const isCreator = value.toLowerCase() === 'creator';
277
+ const isName = predicate === 'name' || predicate === 'preferred_address';
278
+ const isAffil = predicate === 'affiliation';
279
+ const existingRel = typeof self.getRelationship === 'function'
280
+ ? await self.getRelationship(targetCompanionId, rawSubject)
281
+ : null;
282
+ const role = isCreator ? value : (existingRel?.role || (isName || isAffil ? existingRel?.role : value));
283
+ const name = isName ? value : existingRel?.name;
284
+ const affiliation = isAffil ? value : existingRel?.affiliation;
285
+ const stance = isCreator ? 'familiar_loyal' : (existingRel?.stance || 'neutral');
286
+ const trustScore = isCreator ? 1.0 : (existingRel?.trustScore ?? 0.8);
287
+ const familiarity = isCreator ? 0.9 : (existingRel?.familiarity ?? 0.5);
288
+ const interactionConventions = isCreator
289
+ ? Array.from(new Set([...(existingRel?.interactionConventions || []), 'Direct communication', 'Highest administrative trust']))
290
+ : (existingRel?.interactionConventions || []);
274
291
  await self.updateRelationship(targetCompanionId, {
275
292
  companionId: targetCompanionId,
276
293
  entityId: rawSubject,
277
294
  entityType: 'human',
278
- role: value,
279
- stance: isCreator ? 'familiar_loyal' : 'neutral',
280
- trustScore: isCreator ? 1.0 : 0.8,
281
- familiarity: isCreator ? 0.9 : 0.5,
282
- interactionConventions: isCreator
283
- ? ['Direct communication', 'Highest administrative trust']
284
- : [],
295
+ name,
296
+ affiliation,
297
+ role,
298
+ stance,
299
+ trustScore,
300
+ familiarity,
301
+ interactionConventions,
285
302
  });
303
+ if (isName) {
304
+ await self.commitDirectives(targetCompanionId, [
305
+ {
306
+ id: `dir-name-${claim.id || Date.now()}`,
307
+ companionId: targetCompanionId,
308
+ priority: 75,
309
+ directive: `Address ${rawSubject} as ${value}`,
310
+ status: 'active',
311
+ category: 'relational',
312
+ createdAt: new Date().toISOString(),
313
+ },
314
+ ]);
315
+ }
286
316
  return;
287
317
  }
288
318
  // 3. Behavioral rule claim
@@ -31,6 +31,8 @@ export interface SelfRelationship {
31
31
  companionId: string;
32
32
  entityId: string;
33
33
  entityType?: 'human' | 'companion' | 'system';
34
+ name?: string;
35
+ affiliation?: string;
34
36
  role?: string;
35
37
  stance?: string;
36
38
  trustScore?: number;
package/dist/siduri-db.js CHANGED
@@ -59,6 +59,8 @@ class SiduriDatabase {
59
59
  companion_id TEXT NOT NULL,
60
60
  entity_id TEXT NOT NULL,
61
61
  entity_type TEXT NOT NULL DEFAULT 'human',
62
+ name TEXT,
63
+ affiliation TEXT,
62
64
  role TEXT DEFAULT 'user',
63
65
  stance TEXT DEFAULT 'neutral',
64
66
  trust_score REAL DEFAULT 0.5,
@@ -222,6 +224,18 @@ class SiduriDatabase {
222
224
  catch {
223
225
  // Column already exists
224
226
  }
227
+ try {
228
+ this.db.exec("ALTER TABLE self_relationships ADD COLUMN name TEXT");
229
+ }
230
+ catch {
231
+ // Column already exists
232
+ }
233
+ try {
234
+ this.db.exec("ALTER TABLE self_relationships ADD COLUMN affiliation TEXT");
235
+ }
236
+ catch {
237
+ // Column already exists
238
+ }
225
239
  }
226
240
  close() {
227
241
  this.db.close();
@@ -439,6 +453,8 @@ class SiduriDatabase {
439
453
  companionId: row.companion_id,
440
454
  entityId: row.entity_id,
441
455
  entityType: row.entity_type || 'human',
456
+ name: row.name || undefined,
457
+ affiliation: row.affiliation || undefined,
442
458
  role: row.role || 'user',
443
459
  stance: row.stance || 'neutral',
444
460
  trustScore: row.trust_score !== undefined && row.trust_score !== null ? row.trust_score : 0.5,
@@ -453,6 +469,8 @@ class SiduriDatabase {
453
469
  companionId: row.companion_id,
454
470
  entityId: row.entity_id,
455
471
  entityType: row.entity_type || 'human',
472
+ name: row.name || undefined,
473
+ affiliation: row.affiliation || undefined,
456
474
  role: row.role || 'user',
457
475
  stance: row.stance || 'neutral',
458
476
  trustScore: row.trust_score !== undefined && row.trust_score !== null ? row.trust_score : 0.5,
@@ -463,18 +481,20 @@ class SiduriDatabase {
463
481
  }
464
482
  upsertRelationship(rel) {
465
483
  const stmt = this.db.prepare(`
466
- INSERT INTO self_relationships (companion_id, entity_id, entity_type, role, stance, trust_score, familiarity, interaction_conventions, updated_at)
467
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
484
+ INSERT INTO self_relationships (companion_id, entity_id, entity_type, name, affiliation, role, stance, trust_score, familiarity, interaction_conventions, updated_at)
485
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
468
486
  ON CONFLICT(companion_id, entity_id) DO UPDATE SET
469
487
  entity_type = excluded.entity_type,
470
- role = excluded.role,
471
- stance = excluded.stance,
488
+ name = COALESCE(excluded.name, self_relationships.name),
489
+ affiliation = COALESCE(excluded.affiliation, self_relationships.affiliation),
490
+ role = COALESCE(excluded.role, self_relationships.role),
491
+ stance = COALESCE(excluded.stance, self_relationships.stance),
472
492
  trust_score = excluded.trust_score,
473
493
  familiarity = excluded.familiarity,
474
494
  interaction_conventions = excluded.interaction_conventions,
475
495
  updated_at = datetime('now')
476
496
  `);
477
- stmt.run(rel.companionId, rel.entityId, rel.entityType || 'human', rel.role || 'user', rel.stance || 'neutral', rel.trustScore !== undefined && rel.trustScore !== null ? rel.trustScore : 0.5, rel.familiarity !== undefined && rel.familiarity !== null ? rel.familiarity : 0.5, JSON.stringify(rel.interactionConventions || []));
497
+ stmt.run(rel.companionId, rel.entityId, rel.entityType || 'human', rel.name || null, rel.affiliation || null, rel.role || 'user', rel.stance || 'neutral', rel.trustScore !== undefined && rel.trustScore !== null ? rel.trustScore : 0.5, rel.familiarity !== undefined && rel.familiarity !== null ? rel.familiarity : 0.5, JSON.stringify(rel.interactionConventions || []));
478
498
  }
479
499
  getExemplars(companionId) {
480
500
  const stmt = this.db.prepare('SELECT * FROM self_exemplars WHERE companion_id = ? ORDER BY created_at ASC');
@@ -760,25 +780,51 @@ class SiduriDatabase {
760
780
  }
761
781
  return;
762
782
  }
763
- // 2. Relationship mutations: creator or user stated relationship
783
+ // 2. Relationship mutations: creator or user stated relationship, name, or affiliation
764
784
  if (claim.claimType === 'relationship' ||
765
785
  predicate === 'stated_relationship' ||
766
786
  predicate === 'relationship' ||
767
- predicate === 'relationship_to_siduri') {
787
+ predicate === 'relationship_to_siduri' ||
788
+ (predicate === 'name' && (subject.startsWith('actor:') || subject === 'user' || subject === 'primary_user')) ||
789
+ predicate === 'preferred_address' ||
790
+ predicate === 'affiliation') {
768
791
  const rawSubject = (claim.subject || 'actor:user').replace(/^actor:actor:/, 'actor:');
769
792
  const isCreator = value.toLowerCase() === 'creator';
793
+ const isName = predicate === 'name' || predicate === 'preferred_address';
794
+ const isAffil = predicate === 'affiliation';
795
+ const existingRel = this.getRelationship(companionId, rawSubject);
796
+ const role = isCreator ? value : (existingRel?.role || (isName || isAffil ? existingRel?.role : value));
797
+ const name = isName ? value : existingRel?.name;
798
+ const affiliation = isAffil ? value : existingRel?.affiliation;
799
+ const stance = isCreator ? 'familiar_loyal' : (existingRel?.stance || 'neutral');
800
+ const trustScore = isCreator ? 1.0 : (existingRel?.trustScore ?? 0.8);
801
+ const familiarity = isCreator ? 0.9 : (existingRel?.familiarity ?? 0.5);
802
+ const interactionConventions = isCreator
803
+ ? Array.from(new Set([...(existingRel?.interactionConventions || []), 'Direct communication', 'Highest administrative trust']))
804
+ : (existingRel?.interactionConventions || []);
770
805
  this.upsertRelationship({
771
806
  companionId,
772
807
  entityId: rawSubject,
773
808
  entityType: 'human',
774
- role: value,
775
- stance: isCreator ? 'familiar_loyal' : 'neutral',
776
- trustScore: isCreator ? 1.0 : 0.8,
777
- familiarity: isCreator ? 0.9 : 0.5,
778
- interactionConventions: isCreator
779
- ? ['Direct communication', 'Highest administrative trust']
780
- : [],
809
+ name,
810
+ affiliation,
811
+ role,
812
+ stance,
813
+ trustScore,
814
+ familiarity,
815
+ interactionConventions,
781
816
  });
817
+ if (isName) {
818
+ this.commitDirective({
819
+ id: `dir-name-${claim.id || Date.now()}`,
820
+ companionId,
821
+ priority: 75,
822
+ directive: `Address ${rawSubject} as ${value}`,
823
+ status: 'active',
824
+ category: 'relational',
825
+ createdAt: new Date().toISOString(),
826
+ });
827
+ }
782
828
  return;
783
829
  }
784
830
  // 3. Behavioral rule claim
@@ -4,12 +4,11 @@ export interface ExtractedTeaching {
4
4
  behaviorProposals: BehaviorProposal[];
5
5
  }
6
6
  /**
7
- * Deterministically extracts teaching candidates from user messages according to single-owner model.
7
+ * Deterministic teaching extraction placeholder.
8
8
  *
9
- * Rules:
10
- * - Scoped to the requesting actor context (subject: `actor:${actorId}`), NEVER `primary_user`.
11
- * - Candidates are pending proposals only, never active/approved.
12
- * - In a single-owner companion, preferences apply across the companion instance without audience partitioning.
13
- * - Companion identity is isolated.
9
+ * In Siduri-X, conversational memory and behavioral proposals are generated
10
+ * directly by the Brain LLM via structured cognitive planning (e.g. `submitResponsePlan`).
11
+ * Deterministic regex matching has been removed in favor of pure LLM comprehension,
12
+ * as all candidates are quarantined in 'pending' status until explicitly approved by the user.
14
13
  */
15
- export declare function extractDeterministicTeaching(message: string, context?: RequestContext, sourceEventId?: string): ExtractedTeaching;
14
+ export declare function extractDeterministicTeaching(_message: string, _context?: RequestContext, _sourceEventId?: string): ExtractedTeaching;
package/dist/teaching.js CHANGED
@@ -1,321 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractDeterministicTeaching = extractDeterministicTeaching;
4
- function cleanValue(value, limit = 160) {
5
- return value.replace(/\s+/g, ' ').replace(/^[ .,!?:;"']+|[ .,!?:;"']+$/g, '').slice(0, limit);
6
- }
7
4
  /**
8
- * Deterministically extracts teaching candidates from user messages according to single-owner model.
5
+ * Deterministic teaching extraction placeholder.
9
6
  *
10
- * Rules:
11
- * - Scoped to the requesting actor context (subject: `actor:${actorId}`), NEVER `primary_user`.
12
- * - Candidates are pending proposals only, never active/approved.
13
- * - In a single-owner companion, preferences apply across the companion instance without audience partitioning.
14
- * - Companion identity is isolated.
7
+ * In Siduri-X, conversational memory and behavioral proposals are generated
8
+ * directly by the Brain LLM via structured cognitive planning (e.g. `submitResponsePlan`).
9
+ * Deterministic regex matching has been removed in favor of pure LLM comprehension,
10
+ * as all candidates are quarantined in 'pending' status until explicitly approved by the user.
15
11
  */
16
- function extractDeterministicTeaching(message, context, sourceEventId) {
17
- const text = cleanValue(message, 1000);
18
- const claims = [];
19
- const behaviorProposals = [];
20
- if (!text) {
21
- return { claims, behaviorProposals };
22
- }
23
- const rawActorId = context?.actor?.actorId;
24
- const cleanActorId = rawActorId ? rawActorId.replace(/^actor:/, '') : undefined;
25
- const actorSubject = cleanActorId ? `actor:${cleanActorId}` : 'actor:anonymous';
26
- const companionId = context?.companionId || 'default';
27
- const sensitivity = context?.conversation?.channel === 'public' ? 'public' : 'private';
28
- // Strip leading companion addressing: "Siduri, you are...", "Hey Siduri: ..."
29
- const cleanText = text.replace(/^(?:(?:hey|hi|hello)\s+)?(?:siduri|companion|assistant)\s*[,:]\s*/i, '');
30
- const isTeachMode = context?.mode === 'teach' || /^(?:!teach|\/teach|\bteach mode\b|\blearn this rule\b)/i.test(text);
31
- const hasExplicitTeachingCue = isTeachMode ||
32
- /\b(?:remember that|remember this rule|remember this|from now on|learn this|rule:)\b/i.test(text);
33
- // Casual banter / hedged opinion filter to avoid mutating Self from generic conversation
34
- const isCasualBanter = /\b(?:probably|maybe|such a|so |very |really |just |not |the funniest|the best|the worst|the coolest|great|awesome|funny|nice|kind|crazy|wrong|right|silly)\b/i;
35
- // 1. Companion's Name: "your name is X" / "you are called X"
36
- const companionNameMatch = cleanText.match(/\b(?:your name is|you are called)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
37
- if (companionNameMatch) {
38
- const name = cleanValue(companionNameMatch[1], 80);
39
- claims.push({
40
- subject: `companion:${companionId}`,
41
- predicate: 'name',
42
- value: name,
43
- content: `The companion's name is ${name}.`,
44
- claimType: 'semantic',
45
- provenance: 'deterministic_teaching',
46
- sensitivity: 'public',
47
- sourceEventId,
48
- });
49
- behaviorProposals.push({
50
- directive: `Acknowledge configured name as ${name}`,
51
- priority: 70,
52
- subject: `companion:${companionId}`,
53
- predicate: 'name',
54
- value: name,
55
- memoryClass: 'identity',
56
- category: 'relational',
57
- sourceEventId,
58
- });
59
- }
60
- // 2. Identity / Role Teaching:
61
- // "your role is X", "you work at X", "you were created by X", "you are from X",
62
- // or explicit "you are [an/a] X [at Y]" when in teach mode or with explicit teaching cues
63
- const roleMatch = cleanText.match(/\byour role is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
64
- const workMatch = cleanText.match(/\byou work (?:at|for)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
65
- const createdByMatch = cleanText.match(/\byou (?:were|are) created by\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
66
- const fromMatch = cleanText.match(/\byou are from\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
67
- // Match "you are [an/a/the] X [at Y]" only when in Teach mode or explicit teaching cues, and not casual banter
68
- const youAreMatch = (isTeachMode || hasExplicitTeachingCue)
69
- ? cleanText.match(/\b(?:remember that\s+)?you are\s+(?:(?:an?|the)\s+)?(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i)
70
- : null;
71
- const validYouAreRole = youAreMatch &&
72
- !companionNameMatch &&
73
- !fromMatch &&
74
- !isCasualBanter.test(youAreMatch[1]) &&
75
- !/^(?:called|named)\b/i.test(youAreMatch[1])
76
- ? cleanValue(youAreMatch[1].replace(/^(?:an?|the)\s+/i, ''), 100)
77
- : undefined;
78
- const rawEffectiveRole = roleMatch ? cleanValue(roleMatch[1], 100) : validYouAreRole;
79
- const effectiveRole = rawEffectiveRole ? rawEffectiveRole.replace(/^(?:an?|the)\s+/i, '') : undefined;
80
- if (effectiveRole) {
81
- claims.push({
82
- subject: `companion:${companionId}`,
83
- predicate: 'role',
84
- value: effectiveRole,
85
- content: `The companion's role is ${effectiveRole}.`,
86
- claimType: 'semantic',
87
- provenance: 'deterministic_teaching',
88
- sensitivity: 'public',
89
- sourceEventId,
90
- });
91
- behaviorProposals.push({
92
- directive: `Acknowledge role as ${effectiveRole}`,
93
- priority: 70,
94
- subject: `companion:${companionId}`,
95
- predicate: 'role',
96
- value: effectiveRole,
97
- memoryClass: 'identity',
98
- category: 'relational',
99
- sourceEventId,
100
- });
101
- }
102
- if (workMatch) {
103
- const workplace = cleanValue(workMatch[1], 80);
104
- claims.push({
105
- subject: `companion:${companionId}`,
106
- predicate: 'origin',
107
- value: workplace,
108
- content: `The companion works at ${workplace}.`,
109
- claimType: 'semantic',
110
- provenance: 'deterministic_teaching',
111
- sensitivity: 'public',
112
- sourceEventId,
113
- });
114
- behaviorProposals.push({
115
- directive: `Acknowledge workplace as ${workplace}`,
116
- priority: 70,
117
- subject: `companion:${companionId}`,
118
- predicate: 'origin',
119
- value: workplace,
120
- memoryClass: 'identity',
121
- category: 'relational',
122
- sourceEventId,
123
- });
124
- }
125
- if (createdByMatch) {
126
- const creator = cleanValue(createdByMatch[1], 80);
127
- claims.push({
128
- subject: `companion:${companionId}`,
129
- predicate: 'created_by',
130
- value: creator,
131
- content: `The companion was created by ${creator}.`,
132
- claimType: 'semantic',
133
- provenance: 'deterministic_teaching',
134
- sensitivity: 'public',
135
- sourceEventId,
136
- });
137
- behaviorProposals.push({
138
- directive: `Acknowledge creator as ${creator}`,
139
- priority: 75,
140
- subject: `companion:${companionId}`,
141
- predicate: 'created_by',
142
- value: creator,
143
- memoryClass: 'identity',
144
- category: 'relational',
145
- sourceEventId,
146
- });
147
- }
148
- if (fromMatch) {
149
- const origin = cleanValue(fromMatch[1], 80);
150
- claims.push({
151
- subject: `companion:${companionId}`,
152
- predicate: 'origin',
153
- value: origin,
154
- content: `The companion is from ${origin}.`,
155
- claimType: 'semantic',
156
- provenance: 'deterministic_teaching',
157
- sensitivity: 'public',
158
- sourceEventId,
159
- });
160
- behaviorProposals.push({
161
- directive: `Acknowledge origin as ${origin}`,
162
- priority: 70,
163
- subject: `companion:${companionId}`,
164
- predicate: 'origin',
165
- value: origin,
166
- memoryClass: 'identity',
167
- category: 'relational',
168
- sourceEventId,
169
- });
170
- }
171
- // 3. Actor's Name: "my name is X"
172
- const myNameMatch = cleanText.match(/\bmy name is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
173
- if (myNameMatch && !/\b(?:private|public|everywhere)\b/i.test(cleanText)) {
174
- const name = cleanValue(myNameMatch[1], 80);
175
- claims.push({
176
- subject: actorSubject,
177
- predicate: 'name',
178
- value: name,
179
- content: `The actor's name is ${name}.`,
180
- claimType: 'preference',
181
- provenance: 'deterministic_teaching',
182
- sensitivity,
183
- sourceEventId,
184
- });
185
- }
186
- // 4. Preferred Address / Call me X: "call me X"
187
- const callMeMatch = cleanText.match(/\b(?:(?:from now on|only),?\s*)?call me\s+(.+?)(?:\s+(?:in private|privately|in public|publicly|everywhere|in direct conversations))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
188
- if (callMeMatch) {
189
- const address = cleanValue(callMeMatch[1], 80);
190
- const directiveInstruction = `Address ${actorSubject} as ${address}`;
191
- claims.push({
192
- subject: actorSubject,
193
- predicate: 'preferred_address',
194
- value: address,
195
- content: `The actor's preferred address is ${address}.`,
196
- claimType: 'relationship',
197
- provenance: 'deterministic_teaching',
198
- sensitivity,
199
- sourceEventId,
200
- });
201
- behaviorProposals.push({
202
- directive: directiveInstruction,
203
- priority: 80,
204
- subject: actorSubject,
205
- predicate: 'preferred_address',
206
- value: address,
207
- memoryClass: 'behavioral',
208
- category: 'behavioral',
209
- sourceEventId,
210
- });
211
- }
212
- // 5. Stated relationship: "I am your creator, Kur Zagin" / "I am your X" / "I'm your creator"
213
- const relMatch = cleanText.match(/\b(?:i am|i'm)\s+your\s+([A-Za-z0-9_-]+)(?:,\s*([A-Za-z0-9_\s-]+?))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;]|$)/i);
214
- if (relMatch) {
215
- const relationship = cleanValue(relMatch[1], 60);
216
- const actorName = relMatch[2] ? cleanValue(relMatch[2], 80) : undefined;
217
- claims.push({
218
- subject: actorSubject,
219
- predicate: 'stated_relationship',
220
- value: relationship,
221
- content: `The actor stated their relationship as ${relationship}.`,
222
- claimType: 'relationship',
223
- provenance: 'deterministic_teaching',
224
- sensitivity: 'private',
225
- sourceEventId,
226
- });
227
- behaviorProposals.push({
228
- directive: `Recognize ${actorSubject} stated relationship as ${relationship}`,
229
- priority: 75,
230
- subject: actorSubject,
231
- predicate: 'stated_relationship',
232
- value: relationship,
233
- memoryClass: 'relationship',
234
- category: 'relational',
235
- sourceEventId,
236
- });
237
- if (actorName) {
238
- claims.push({
239
- subject: actorSubject,
240
- predicate: 'name',
241
- value: actorName,
242
- content: `The actor's name is ${actorName}.`,
243
- claimType: 'preference',
244
- provenance: 'deterministic_teaching',
245
- sensitivity,
246
- sourceEventId,
247
- });
248
- }
249
- }
250
- // 6. Behavioral rule / directive: "Remember this rule: X" / "Remember that rule: X" / "Rule: X" / "From now on, always X"
251
- const ruleMatch = cleanText.match(/\b(?:remember\s+(?:this|that)\s+rule:\s*|rule:\s*|(?:from now on,?\s+)?always\s+)(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;]|$)/i);
252
- if (ruleMatch) {
253
- let instruction = cleanValue(ruleMatch[1], 160);
254
- if (instruction) {
255
- instruction = instruction.charAt(0).toUpperCase() + instruction.slice(1);
256
- behaviorProposals.push({
257
- directive: instruction,
258
- priority: 60,
259
- subject: actorSubject,
260
- predicate: 'rule',
261
- value: instruction,
262
- category: 'behavioral',
263
- memoryClass: 'behavioral',
264
- sourceEventId,
265
- });
266
- claims.push({
267
- subject: actorSubject,
268
- predicate: 'behavioral_rule',
269
- value: instruction,
270
- content: `Behavioral rule: ${instruction}`,
271
- claimType: 'preference',
272
- provenance: 'deterministic_teaching',
273
- sensitivity,
274
- sourceEventId,
275
- });
276
- }
277
- }
278
- // Preference: "I prefer concise answers"
279
- const prefAnswersMatch = cleanText.match(/\b(?:i prefer|my preference is)\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
280
- if (prefAnswersMatch && !cleanText.match(/\bmy\s+preferred\s+([A-Za-z0-9_]+)\s+is\b/i)) {
281
- const prefVal = cleanValue(prefAnswersMatch[1], 100);
282
- const directive = `Prefer ${prefVal}`;
283
- behaviorProposals.push({
284
- directive,
285
- priority: 60,
286
- subject: actorSubject,
287
- predicate: 'preference',
288
- value: prefVal,
289
- category: 'behavioral',
290
- memoryClass: 'behavioral',
291
- sourceEventId,
292
- });
293
- claims.push({
294
- subject: actorSubject,
295
- predicate: 'preference',
296
- value: prefVal,
297
- content: `The actor prefers ${prefVal}.`,
298
- claimType: 'preference',
299
- provenance: 'deterministic_teaching',
300
- sensitivity,
301
- sourceEventId,
302
- });
303
- }
304
- // 7. Explicit Domain / Preference fact: "my preferred X is Y" / "my X is Y"
305
- const prefMatch = cleanText.match(/\bmy\s+preferred\s+([A-Za-z0-9_]+)\s+is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
306
- if (prefMatch) {
307
- const predicate = cleanValue(prefMatch[1], 40);
308
- const val = cleanValue(prefMatch[2], 100);
309
- claims.push({
310
- subject: actorSubject,
311
- predicate: `preferred_${predicate}`,
312
- value: val,
313
- content: `The actor's preferred ${predicate} is ${val}.`,
314
- claimType: 'preference',
315
- provenance: 'deterministic_teaching',
316
- sensitivity,
317
- sourceEventId,
318
- });
319
- }
320
- return { claims, behaviorProposals };
12
+ function extractDeterministicTeaching(_message, _context, _sourceEventId) {
13
+ return { claims: [], behaviorProposals: [] };
321
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/core",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Core runtime types, evidence protocol, action dispatcher, capability validation, and SiduriRuntime protocol",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {