@siduri-x/api 2.0.1 → 2.0.3

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/src/app.ts CHANGED
@@ -2,49 +2,25 @@ import express, { Express } from 'express';
2
2
  import cors from 'cors';
3
3
  import { createCorsOptions } from './cors';
4
4
  import { SiduriRuntime, dispatchCompanionChat } from './runtime';
5
- import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
6
- import { SqliteMemoryStore } from '@siduri-x/memory';
7
- import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
8
- import { EKnowledgeAdapter, EKnowledgeConfig } from '@siduri-x/eknowledge';
9
- import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
10
- import { ActiveSelfCompiler, SelfPackageParser, SqliteSelfRepository, scanDirective } from '@siduri-x/self';
11
- import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
5
+ import { SelfPackageParser, SqliteSelfRepository, scanDirective } from '@siduri-x/self';
12
6
  import { FixtureObservationOrgan } from '@siduri-x/observation';
13
- import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@siduri-x/hands';
14
- import { DefaultEarOrgan, EarOrganConfig } from '@siduri-x/ear';
15
- import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@siduri-x/mouth';
16
- import { attachIdentity, requireAuth, Identity } from './auth';
7
+ import { attachIdentity, requireAuth, Identity, isLocalRequest } from './auth';
17
8
  import { mapRequestContext } from './context-mapper';
18
-
19
- export interface AppBrainConfig {
20
- provider?: 'openrouter' | 'openai-compatible' | string;
21
- model?: string;
22
- apiKey?: string;
23
- apiKeyEnv?: string;
24
- baseUrl?: string;
25
- timeoutMs?: number;
26
- [key: string]: unknown;
27
- }
28
-
29
- export interface AppBehaviorConfig {
30
- provider?: 'active_self' | 'none' | string;
31
- preset?: string;
32
- [key: string]: unknown;
33
- }
34
-
35
- export interface AppBootCompanionConfig {
36
- name: string;
37
- brain?: AppBrainConfig;
38
- voice?: VoiceConfig;
39
- knowledge?: EKnowledgeConfig;
40
- vision?: OpenRouterVisionConfig;
41
- behavior?: AppBehaviorConfig;
42
- body?: Live2DAdapterConfig;
43
- hands?: DefaultHandsOrganConfig;
44
- ear?: EarOrganConfig;
45
- mouth?: DefaultMouthOrganConfig;
46
- [key: string]: unknown;
47
- }
9
+ import {
10
+ bootCompanion,
11
+ AppBrainConfig,
12
+ AppBehaviorConfig,
13
+ AppBootCompanionConfig,
14
+ BootCompanionOptions,
15
+ } from './boot';
16
+
17
+ export {
18
+ AppBrainConfig,
19
+ AppBehaviorConfig,
20
+ AppBootCompanionConfig,
21
+ BootCompanionOptions,
22
+ bootCompanion,
23
+ };
48
24
 
49
25
  export interface AppInstance {
50
26
  app: Express;
@@ -59,77 +35,6 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
59
35
 
60
36
  let observationOrgan: FixtureObservationOrgan | undefined;
61
37
 
62
- function createBrain(config?: AppBrainConfig) {
63
- const provider = config?.provider || 'openrouter';
64
- const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
65
- const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
66
- if (provider === 'openai-compatible') {
67
- return new OpenAICompatibleBrain({
68
- apiKey,
69
- model: config?.model || 'local-model',
70
- baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
71
- });
72
- }
73
- return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
74
- }
75
-
76
- function isDisabled(config?: { provider?: string }): boolean {
77
- return !config || config.provider === 'none';
78
- }
79
-
80
- function createVoice(config?: VoiceConfig) {
81
- return isDisabled(config)
82
- ? undefined
83
- : new VoiceAdapter({
84
- provider: (config?.provider as any) || 'voicevox',
85
- baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
86
- speakerId: config?.speakerId || 1,
87
- ...config,
88
- });
89
- }
90
-
91
- function createKnowledge(config?: EKnowledgeConfig) {
92
- return isDisabled(config) ? undefined : new EKnowledgeAdapter(config || {});
93
- }
94
-
95
- function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
96
- return isDisabled(config)
97
- ? undefined
98
- : new OpenRouterVisionAdapter({
99
- apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
100
- model: config?.model || 'gpt-4-vision',
101
- ...config,
102
- });
103
- }
104
-
105
- function createBehavior(config?: AppBehaviorConfig) {
106
- return isDisabled(config) ? undefined : new ActiveSelfCompiler();
107
- }
108
-
109
- function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
110
- return isDisabled(config)
111
- ? undefined
112
- : new Live2DAdapter(config);
113
- }
114
-
115
- function createHands(config?: DefaultHandsOrganConfig & { provider?: string }) {
116
- return isDisabled(config)
117
- ? new DefaultHandsOrgan()
118
- : new DefaultHandsOrgan(config);
119
- }
120
-
121
- function createEar(config?: EarOrganConfig & { provider?: string }) {
122
- return isDisabled(config)
123
- ? new DefaultEarOrgan()
124
- : new DefaultEarOrgan(config);
125
- }
126
-
127
- function createMouth(config?: DefaultMouthOrganConfig & { provider?: string }, voice?: any) {
128
- return isDisabled(config)
129
- ? new DefaultMouthOrgan({ voice })
130
- : new DefaultMouthOrgan({ ...config, voice });
131
- }
132
-
133
38
  app.post('/boot', requireAuth, async (req, res) => {
134
39
  try {
135
40
  const { id, config } = req.body;
@@ -137,36 +42,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
137
42
  return res.status(400).json({ error: "Already booted" });
138
43
  }
139
44
 
140
- const organs = config?.organs || {};
141
- const brain = createBrain(organs.brain || config?.brain);
142
- const memory = new SqliteMemoryStore({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
143
- const selfRepo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
144
- const voice = createVoice(organs.voice || config?.voice);
145
- const knowledge = createKnowledge(organs.knowledge || config?.knowledge);
146
- const vision = createVision(organs.vision || config?.vision);
147
- const behavior = createBehavior(organs.behavior || config?.behavior);
148
- const body = createBody(organs.body || config?.body);
149
- const hands = createHands(organs.hands || config?.hands);
150
- const ear = createEar(organs.ear || config?.ear);
151
- const mouth = createMouth(organs.mouth || config?.mouth, voice);
152
-
153
- const runtime = new SiduriRuntime(id, config, {
154
- brain,
155
- memory,
156
- voice,
157
- knowledge,
158
- vision,
159
- behavior,
160
- body,
161
- hands,
162
- ear,
163
- mouth,
164
- observation: observationOrgan,
165
- self: selfRepo,
166
- externalKnowledge: knowledge,
167
- });
168
- await runtime.initialize();
169
-
45
+ const runtime = await bootCompanion(id, config, { observationOrgan });
170
46
  runtimes.set(id, runtime);
171
47
 
172
48
  res.json({ success: true, id });
@@ -260,6 +136,8 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
260
136
  companionId,
261
137
  name: manifest.identity.name,
262
138
  archetype: manifest.identity.archetype,
139
+ origin: manifest.identity.origin,
140
+ ethos: manifest.identity.ethos,
263
141
  version: manifest.version || '1.0.0',
264
142
  updatedAt: new Date().toISOString(),
265
143
  });
@@ -268,6 +146,25 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
268
146
  await repo.setPersonality(companionId, manifest.personality);
269
147
  }
270
148
 
149
+ if (Array.isArray(manifest.relationships)) {
150
+ for (const rel of manifest.relationships) {
151
+ if (rel && rel.entityId) {
152
+ await repo.updateRelationship(companionId, {
153
+ companionId,
154
+ entityId: rel.entityId,
155
+ entityType: 'human',
156
+ role: rel.role || 'user',
157
+ stance: rel.stance || 'neutral',
158
+ interactionConventions: rel.conventions || [],
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
165
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
166
+ }
167
+
271
168
  if (directivesToCommit.length > 0) {
272
169
  await repo.commitDirectives(companionId, directivesToCommit);
273
170
  }
@@ -285,13 +182,20 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
285
182
  app.post('/chat', attachIdentity, async (req, res) => {
286
183
  const { id, message, history } = req.body;
287
184
  const identity = (req as any).identity as Identity;
185
+ const local = isLocalRequest(req);
186
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
187
+ const isLocalDirectOwner = local && !hasConfiguredToken;
188
+
189
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
190
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
288
191
 
289
- // Single-owner companion model: map request context directly
192
+ // Single-owner companion model: map request context directly with server identity enforcement
290
193
  const mappingResult = mapRequestContext({
291
194
  ...req.body,
292
195
  id: id || req.body.companionId,
293
- authenticated: identity?.authenticated ?? true,
294
- source: identity?.source ?? 'local',
196
+ authenticated,
197
+ serverRole,
198
+ source: identity?.source ?? (local ? 'local' : 'external'),
295
199
  generateCorrelationId: true,
296
200
  });
297
201
 
@@ -325,12 +229,19 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
325
229
  app.post('/chat/stream', attachIdentity, async (req, res) => {
326
230
  const { id, message, history } = req.body;
327
231
  const identity = (req as any).identity as Identity;
232
+ const local = isLocalRequest(req);
233
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
234
+ const isLocalDirectOwner = local && !hasConfiguredToken;
235
+
236
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
237
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
328
238
 
329
239
  const mappingResult = mapRequestContext({
330
240
  ...req.body,
331
241
  id: id || req.body.companionId,
332
- authenticated: identity?.authenticated ?? true,
333
- source: identity?.source ?? 'local',
242
+ authenticated,
243
+ serverRole,
244
+ source: identity?.source ?? (local ? 'local' : 'external'),
334
245
  generateCorrelationId: true,
335
246
  });
336
247
 
@@ -353,8 +264,8 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
353
264
  const abortController = new AbortController();
354
265
  const onClose = () => {
355
266
  abortController.abort('client_disconnect');
356
- if (typeof runtime.interruptMouth === 'function') {
357
- runtime.interruptMouth('client_disconnect');
267
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
268
+ runtime.mouth.interrupt('client_disconnect');
358
269
  }
359
270
  };
360
271
  req.on('close', onClose);
@@ -370,7 +281,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
370
281
  signal: abortController.signal,
371
282
  });
372
283
 
373
- res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
284
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
374
285
 
375
286
  const avatarEvent = response.metadata?.events?.find(
376
287
  (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval)
@@ -427,12 +338,16 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
427
338
  const reason = req.body?.reason || 'user_barge_in';
428
339
 
429
340
  if (runtime) {
430
- runtime.interruptMouth(reason);
341
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
342
+ runtime.mouth.interrupt(reason);
343
+ }
431
344
  return res.json({ success: true, interrupted: true, companionId, reason });
432
345
  }
433
346
 
434
347
  for (const r of runtimes.values()) {
435
- r.interruptMouth(reason);
348
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
349
+ r.mouth.interrupt(reason);
350
+ }
436
351
  }
437
352
  return res.json({ success: true, interrupted: true, reason });
438
353
  });
@@ -443,12 +358,16 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
443
358
  const reason = req.body?.reason || 'user_barge_in';
444
359
 
445
360
  if (runtime) {
446
- runtime.interruptMouth(reason);
361
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
362
+ runtime.mouth.interrupt(reason);
363
+ }
447
364
  return res.json({ success: true, interrupted: true, companionId, reason });
448
365
  }
449
366
 
450
367
  for (const r of runtimes.values()) {
451
- r.interruptMouth(reason);
368
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
369
+ r.mouth.interrupt(reason);
370
+ }
452
371
  }
453
372
  return res.json({ success: true, interrupted: true, reason });
454
373
  });
@@ -460,7 +379,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
460
379
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
461
380
  if (!runtime.memory) return res.json({ proposals: [] });
462
381
  try {
463
- const proposals = await runtime.getPendingClaims();
382
+ const proposals = await runtime.memory.getPendingClaims();
464
383
  res.json({ proposals });
465
384
  } catch (e: any) {
466
385
  res.status(500).json({ error: e.message });
@@ -473,7 +392,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
473
392
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
474
393
  if (!runtime.memory) return res.json({ items: [] });
475
394
  try {
476
- const items = await runtime.getClaims();
395
+ const items = await runtime.memory.getClaims();
477
396
  res.json({ items });
478
397
  } catch (e: any) {
479
398
  res.status(500).json({ error: e.message });
@@ -486,7 +405,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
486
405
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
487
406
  if (!runtime.memory) return res.json({ claims: [] });
488
407
  try {
489
- const claims = await runtime.getClaims();
408
+ const claims = await runtime.memory.getClaims();
490
409
  res.json({ claims });
491
410
  } catch (e: any) {
492
411
  res.status(500).json({ error: e.message });
@@ -499,13 +418,96 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
499
418
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
500
419
  if (!runtime.memory) return res.json({ directives: [] });
501
420
  try {
502
- const directives = await runtime.getDirectives();
421
+ const directives = await runtime.memory.getDirectives();
503
422
  res.json({ directives });
504
423
  } catch (e: any) {
505
424
  res.status(500).json({ error: e.message });
506
425
  }
507
426
  });
508
427
 
428
+ // KNOWLEDGE / LIFE DB GETTERS
429
+ app.get('/knowledge/life', requireAuth, async (req, res) => {
430
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
431
+ const runtime = runtimes.get(id);
432
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
433
+ const query = (req.query.q as string) || '';
434
+ if (!runtime.knowledge || typeof (runtime.knowledge as any).queryLifeContext !== 'function') {
435
+ return res.json({ matchedInventory: [], recentFinances: [], upcomingSchedule: [], preferences: [], formattedContext: '' });
436
+ }
437
+ try {
438
+ const result = await (runtime.knowledge as any).queryLifeContext(id, query);
439
+ res.json(result);
440
+ } catch (e: any) {
441
+ res.status(500).json({ error: e.message });
442
+ }
443
+ });
444
+
445
+ app.get('/knowledge/inventory', requireAuth, async (req, res) => {
446
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
447
+ const runtime = runtimes.get(id);
448
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
449
+ const domain = req.query.domain as string | undefined;
450
+ if (!runtime.knowledge || !(runtime.knowledge as any).inventory) {
451
+ return res.json({ items: [] });
452
+ }
453
+ try {
454
+ const items = await (runtime.knowledge as any).inventory.getItems(id, domain);
455
+ res.json({ items });
456
+ } catch (e: any) {
457
+ res.status(500).json({ error: e.message });
458
+ }
459
+ });
460
+
461
+ app.get('/knowledge/finance', requireAuth, async (req, res) => {
462
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
463
+ const runtime = runtimes.get(id);
464
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
465
+ if (!runtime.knowledge || !(runtime.knowledge as any).finance) {
466
+ return res.json({ summary: null, entries: [] });
467
+ }
468
+ try {
469
+ const limit = Number(req.query.limit || 20);
470
+ const [summary, entries] = await Promise.all([
471
+ (runtime.knowledge as any).finance.getSummary(id),
472
+ (runtime.knowledge as any).finance.getEntries(id, limit),
473
+ ]);
474
+ res.json({ summary, entries });
475
+ } catch (e: any) {
476
+ res.status(500).json({ error: e.message });
477
+ }
478
+ });
479
+
480
+ app.get('/knowledge/schedule', requireAuth, async (req, res) => {
481
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
482
+ const runtime = runtimes.get(id);
483
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
484
+ if (!runtime.knowledge || !(runtime.knowledge as any).schedule) {
485
+ return res.json({ items: [] });
486
+ }
487
+ try {
488
+ const items = await (runtime.knowledge as any).schedule.getUpcoming(id);
489
+ res.json({ items });
490
+ } catch (e: any) {
491
+ res.status(500).json({ error: e.message });
492
+ }
493
+ });
494
+
495
+ app.get('/knowledge/preferences', requireAuth, async (req, res) => {
496
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
497
+ const runtime = runtimes.get(id);
498
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
499
+ const category = req.query.category as string | undefined;
500
+ if (!runtime.knowledge || !(runtime.knowledge as any).preferences) {
501
+ return res.json({ preferences: [] });
502
+ }
503
+ try {
504
+ const preferences = await (runtime.knowledge as any).preferences.getPreferences(id, category);
505
+ res.json({ preferences });
506
+ } catch (e: any) {
507
+ res.status(500).json({ error: e.message });
508
+ }
509
+ });
510
+
509
511
  // MEMORY MUTATIONS - PROPOSALS
510
512
  app.post('/memory/proposals/update', requireAuth, async (req, res) => {
511
513
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
@@ -519,7 +521,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
519
521
  if (!claimId) {
520
522
  return res.status(400).json({ error: "Missing required claim id" });
521
523
  }
522
- const updated = await runtime.updateClaim(claimId, req.body.updates || req.body);
524
+ const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
523
525
  res.json({ success: true, claim: updated });
524
526
  } catch (e: any) {
525
527
  res.status(500).json({ error: e.message });
@@ -532,7 +534,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
532
534
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
533
535
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
534
536
  try {
535
- await runtime.approveClaim(req.body.id);
537
+ await runtime.memory.approveClaim(req.body.id);
536
538
  res.json({ approved: true });
537
539
  } catch (e: any) {
538
540
  res.status(500).json({ error: e.message });
@@ -545,7 +547,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
545
547
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
546
548
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
547
549
  try {
548
- await runtime.rejectClaim(req.body.id);
550
+ await runtime.memory.rejectClaim(req.body.id);
549
551
  res.json({ rejected: true });
550
552
  } catch (e: any) {
551
553
  res.status(500).json({ error: e.message });
@@ -559,7 +561,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
559
561
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
560
562
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
561
563
  try {
562
- await runtime.approveDirective(req.body.id);
564
+ await runtime.memory.approveDirective(req.body.id);
563
565
  res.json({ approved: true });
564
566
  } catch (e: any) {
565
567
  res.status(500).json({ error: e.message });
@@ -572,7 +574,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
572
574
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
573
575
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
574
576
  try {
575
- await runtime.rejectDirective(req.body.id);
577
+ await runtime.memory.rejectDirective(req.body.id);
576
578
  res.json({ rejected: true });
577
579
  } catch (e: any) {
578
580
  res.status(500).json({ error: e.message });
@@ -585,7 +587,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
585
587
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
586
588
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
587
589
  try {
588
- await runtime.revokeDirective(req.body.id);
590
+ await runtime.memory.revokeDirective(req.body.id);
589
591
  res.json({ revoked: true });
590
592
  } catch (e: any) {
591
593
  res.status(500).json({ error: e.message });
@@ -598,7 +600,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
598
600
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
599
601
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
600
602
  try {
601
- await runtime.disableDirective(req.body.id);
603
+ await runtime.memory.disableDirective(req.body.id);
602
604
  res.json({ disabled: true });
603
605
  } catch (e: any) {
604
606
  res.status(500).json({ error: e.message });
@@ -615,7 +617,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
615
617
  return res.status(400).json({ error: "Memory organ does not support reset" });
616
618
  }
617
619
  try {
618
- await runtime.resetMemory();
620
+ await runtime.memory.resetMemory();
619
621
  res.json({ reset: true });
620
622
  } catch (e: any) {
621
623
  res.status(500).json({ error: e.message });
@@ -626,7 +628,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
626
628
  const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
627
629
  const runtime = runtimes.get(companionId);
628
630
  if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
629
- const staged = runtime.stageResponse({
631
+ const staged = runtime.gating.stageResponse({
630
632
  requestContext: {
631
633
  companionId,
632
634
  actor: {
@@ -662,12 +664,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
662
664
  let correlationId = req.body?.correlation_id;
663
665
 
664
666
  if (!responseId && correlationId) {
665
- const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
667
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
666
668
  if (found) {
667
669
  responseId = found.responseId;
668
670
  }
669
671
  } else if (responseId && !correlationId) {
670
- const found = runtime.getStagedPlan(responseId);
672
+ const found = runtime.gating.getStagedPlan(responseId);
671
673
  if (found) {
672
674
  correlationId = found.correlationId;
673
675
  }
@@ -677,7 +679,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
677
679
  return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
678
680
  }
679
681
 
680
- const result = runtime.approveResponse({
682
+ const result = runtime.gating.approveResponse({
681
683
  responseId,
682
684
  companionId,
683
685
  correlationId: correlationId || '',
@@ -688,7 +690,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
688
690
  }
689
691
 
690
692
  // Now evaluated as approved
691
- const evaluation = runtime.evaluateGate(result.plan!);
693
+ const evaluation = runtime.gating.evaluateGate(result.plan!);
692
694
  res.json({
693
695
  approved: true,
694
696
  status: evaluation.disposition,
@@ -707,12 +709,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
707
709
  let correlationId = req.body?.correlation_id;
708
710
 
709
711
  if (!responseId && correlationId) {
710
- const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
712
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
711
713
  if (found) {
712
714
  responseId = found.responseId;
713
715
  }
714
716
  } else if (responseId && !correlationId) {
715
- const found = runtime.getStagedPlan(responseId);
717
+ const found = runtime.gating.getStagedPlan(responseId);
716
718
  if (found) {
717
719
  correlationId = found.correlationId;
718
720
  }
@@ -722,7 +724,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
722
724
  return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
723
725
  }
724
726
 
725
- const result = runtime.rejectResponse({
727
+ const result = runtime.gating.rejectResponse({
726
728
  responseId,
727
729
  companionId,
728
730
  correlationId: correlationId || '',
@@ -0,0 +1,103 @@
1
+ import { bootCompanion, isDisabled, createBrain, createMemory, createVoice, createKnowledge, createVision, createBehavior, createBody, createHands, createEar, createMouth, createSelf, createObservation } from './boot';
2
+ import { FixtureObservationOrgan } from '@siduri-x/observation';
3
+
4
+ describe('Canonical bootCompanion & Organ Factory Suite', () => {
5
+ const originalEnv = process.env;
6
+
7
+ beforeEach(() => {
8
+ process.env = { ...originalEnv };
9
+ });
10
+
11
+ afterAll(() => {
12
+ process.env = originalEnv;
13
+ });
14
+
15
+ test('isDisabled correctly identifies undefined or none provider', () => {
16
+ expect(isDisabled(undefined)).toBe(true);
17
+ expect(isDisabled({ provider: 'none' })).toBe(true);
18
+ expect(isDisabled({ provider: 'openrouter' })).toBe(false);
19
+ expect(isDisabled({ provider: 'sqlite' })).toBe(false);
20
+ });
21
+
22
+ test('creates all standard organs with fallback configurations', () => {
23
+ const brain = createBrain({ provider: 'openai-compatible', baseUrl: 'http://localhost:11434/v1', apiKey: 'test' });
24
+ expect(brain).toBeDefined();
25
+
26
+ const voice = createVoice({ provider: 'none' });
27
+ expect(voice).toBeUndefined();
28
+
29
+ const memory = createMemory({ provider: 'sqlite', dbPath: ':memory:' });
30
+ expect(memory).toBeDefined();
31
+
32
+ const selfRepo = createSelf({ dbPath: ':memory:' });
33
+ expect(selfRepo).toBeDefined();
34
+
35
+ const vision = createVision({ provider: 'none' });
36
+ expect(vision).toBeUndefined();
37
+
38
+ const behavior = createBehavior({ provider: 'active_self' });
39
+ expect(behavior).toBeDefined();
40
+
41
+ const body = createBody({ provider: 'none' });
42
+ expect(body).toBeUndefined();
43
+
44
+ const hands = createHands();
45
+ expect(hands).toBeDefined();
46
+
47
+ const ear = createEar();
48
+ expect(ear).toBeDefined();
49
+
50
+ const mouth = createMouth(undefined, voice);
51
+ expect(mouth).toBeDefined();
52
+
53
+ const observation = createObservation();
54
+ expect(observation).toBeInstanceOf(FixtureObservationOrgan);
55
+ });
56
+
57
+ test('bootCompanion wires all organs and runs migrations', async () => {
58
+ let migrationsRun = false;
59
+ const testConfig = {
60
+ name: 'Test Companion',
61
+ brain: { provider: 'openrouter', apiKey: 'mock-key', model: 'mock-model' },
62
+ memory: { provider: 'none' }, // will use custom mock
63
+ voice: { provider: 'none' },
64
+ vision: { provider: 'none' },
65
+ behavior: { provider: 'none' },
66
+ body: { provider: 'none' },
67
+ hands: { provider: 'none' },
68
+ ear: { provider: 'none' },
69
+ mouth: { provider: 'none' },
70
+ knowledge: { provider: 'none' },
71
+ };
72
+
73
+ const mockObservation = new FixtureObservationOrgan({ analyze: async () => JSON.stringify({ readings: [] }) });
74
+
75
+ const runtime = await bootCompanion('test-comp-1', testConfig as any, {
76
+ observationOrgan: mockObservation,
77
+ });
78
+
79
+ expect(runtime).toBeDefined();
80
+ expect(runtime.id).toBe('test-comp-1');
81
+ expect(runtime.observation).toBe(mockObservation);
82
+ expect(runtime.hands).toBeDefined();
83
+ expect(runtime.ear).toBeDefined();
84
+ expect(runtime.mouth).toBeDefined();
85
+ });
86
+
87
+ test('bootCompanion handles nested organs configuration structure', async () => {
88
+ const nestedConfig = {
89
+ name: 'Nested Companion',
90
+ organs: {
91
+ brain: { provider: 'openrouter', apiKey: 'mock' },
92
+ memory: { provider: 'none' },
93
+ knowledge: { provider: 'none' },
94
+ voice: { provider: 'none' },
95
+ },
96
+ };
97
+
98
+ const runtime = await bootCompanion('nested-comp', nestedConfig as any);
99
+ expect(runtime).toBeDefined();
100
+ expect(runtime.id).toBe('nested-comp');
101
+ expect(runtime.brain).toBeDefined();
102
+ });
103
+ });