@siduri-x/api 1.0.5 → 2.0.2

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/package.json CHANGED
@@ -1,36 +1,40 @@
1
1
  {
2
2
  "name": "@siduri-x/api",
3
- "version": "1.0.5",
3
+ "version": "2.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "main": "dist/index.js",
8
+ "engines": {
9
+ "node": ">=22.16.0"
10
+ },
8
11
  "dependencies": {
9
- "cors": "^2.8.5",
10
- "dotenv": "^16.3.1",
11
- "express": "^4.18.2",
12
- "@siduri-x/behavior": "1.0.6",
13
- "@siduri-x/body": "1.0.4",
14
- "@siduri-x/brain": "1.0.5",
15
- "@siduri-x/core": "1.0.9",
16
- "@siduri-x/ear": "1.0.3",
17
- "@siduri-x/hands": "1.0.4",
18
- "@siduri-x/knowledge": "1.0.2",
19
- "@siduri-x/memory": "1.0.7",
20
- "@siduri-x/observation": "1.0.3",
21
- "@siduri-x/vision": "1.0.2",
22
- "@siduri-x/voice": "1.0.6",
23
- "@siduri-x/mouth": "1.0.0"
12
+ "cors": "^2.8.6",
13
+ "dotenv": "^17.4.2",
14
+ "express": "^5.2.1",
15
+ "@siduri-x/body": "2.0.1",
16
+ "@siduri-x/brain": "2.0.1",
17
+ "@siduri-x/core": "2.0.2",
18
+ "@siduri-x/ear": "2.0.1",
19
+ "@siduri-x/eknowledge": "2.0.1",
20
+ "@siduri-x/hands": "2.0.1",
21
+ "@siduri-x/knowledge": "2.0.1",
22
+ "@siduri-x/memory": "2.0.1",
23
+ "@siduri-x/mouth": "2.0.1",
24
+ "@siduri-x/observation": "2.0.1",
25
+ "@siduri-x/self": "2.0.1",
26
+ "@siduri-x/vision": "2.0.1",
27
+ "@siduri-x/voice": "2.0.1"
24
28
  },
25
29
  "devDependencies": {
26
- "@types/cors": "^2.8.17",
27
- "@types/express": "^4.17.21",
28
- "@types/jest": "^29.5.14",
29
- "@types/supertest": "^6.0.2",
30
- "jest": "^29.7.0",
31
- "supertest": "^6.3.4",
30
+ "@types/cors": "^2.8.19",
31
+ "@types/express": "^5.0.6",
32
+ "@types/jest": "^30.0.0",
33
+ "@types/supertest": "^7.2.1",
34
+ "jest": "^30.5.1",
35
+ "supertest": "^7.2.2",
32
36
  "ts-jest": "^29.4.12",
33
- "typescript": "^5.3.3"
37
+ "typescript": "^5.9.3"
34
38
  },
35
39
  "scripts": {
36
40
  "build": "tsc",
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 { PostgresMemoryOrgan } from '@siduri-x/memory';
7
- import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
8
- import { EKnowledgeAdapter, EKnowledgeConfig } from '@siduri-x/knowledge';
9
- import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
10
- import { ActiveSelfCompiler } from '@siduri-x/behavior';
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,32 +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 brain = createBrain(config.brain);
141
- const memory = new PostgresMemoryOrgan({ connectionString: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/siduri' });
142
- const voice = createVoice(config.voice);
143
- const knowledge = createKnowledge(config.knowledge);
144
- const vision = createVision(config.vision);
145
- const behavior = createBehavior(config.behavior);
146
- const body = createBody(config.body);
147
- const hands = createHands(config.hands);
148
- const ear = createEar(config.ear);
149
- const mouth = createMouth(config.mouth, voice);
150
-
151
- const runtime = new SiduriRuntime(id, config, {
152
- brain,
153
- memory,
154
- voice,
155
- knowledge,
156
- vision,
157
- behavior,
158
- body,
159
- hands,
160
- ear,
161
- mouth,
162
- observation: observationOrgan,
163
- });
164
- await runtime.initialize();
165
-
45
+ const runtime = await bootCompanion(id, config, { observationOrgan });
166
46
  runtimes.set(id, runtime);
167
47
 
168
48
  res.json({ success: true, id });
@@ -211,17 +91,90 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
211
91
  });
212
92
  app.put('/me', requireAuth, (req, res) => res.json({ success: true }));
213
93
 
94
+ // TEACH MODE ENDPOINTS
95
+ app.post('/teach/upload-self', requireAuth, async (req, res) => {
96
+ try {
97
+ const { content } = req.body;
98
+ if (!content) return res.status(400).json({ error: "Missing content" });
99
+ const parsed = SelfPackageParser.parse(content);
100
+ res.json(parsed);
101
+ } catch (e: any) {
102
+ res.status(500).json({ error: e.message });
103
+ }
104
+ });
105
+
106
+ app.post('/teach/install-self', requireAuth, async (req, res) => {
107
+ try {
108
+ const { companionId, manifest, approvedDirectiveIds } = req.body;
109
+ if (!companionId || !manifest || !Array.isArray(approvedDirectiveIds)) {
110
+ return res.status(400).json({ error: "Missing required fields" });
111
+ }
112
+
113
+ if (!manifest.identity || !manifest.identity.name) {
114
+ return res.status(400).json({ error: "Invalid manifest: missing identity.name" });
115
+ }
116
+
117
+ const directivesToCommit = manifest.directives?.filter((d: any) => approvedDirectiveIds.includes(d.id)) || [];
118
+ for (const d of directivesToCommit) {
119
+ if (!d || typeof d.directive !== 'string') {
120
+ return res.status(400).json({ error: "Invalid directive entry: missing directive string" });
121
+ }
122
+ const scan = scanDirective(d.directive);
123
+ if (!scan.safe) {
124
+ return res.status(400).json({
125
+ error: `Safety check failed for directive: ${scan.reason}`,
126
+ directiveId: d.id,
127
+ reason: scan.reason,
128
+ });
129
+ }
130
+ }
131
+
132
+ const repo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
133
+
134
+ try {
135
+ await repo.setIdentity({
136
+ companionId,
137
+ name: manifest.identity.name,
138
+ archetype: manifest.identity.archetype,
139
+ version: manifest.version || '1.0.0',
140
+ updatedAt: new Date().toISOString(),
141
+ });
142
+
143
+ if (manifest.personality) {
144
+ await repo.setPersonality(companionId, manifest.personality);
145
+ }
146
+
147
+ if (directivesToCommit.length > 0) {
148
+ await repo.commitDirectives(companionId, directivesToCommit);
149
+ }
150
+ } finally {
151
+ repo.close();
152
+ }
153
+
154
+ res.json({ success: true });
155
+ } catch (e: any) {
156
+ res.status(500).json({ error: e.message });
157
+ }
158
+ });
159
+
214
160
  // CHAT (API context boundary validation)
215
161
  app.post('/chat', attachIdentity, async (req, res) => {
216
162
  const { id, message, history } = req.body;
217
163
  const identity = (req as any).identity as Identity;
164
+ const local = isLocalRequest(req);
165
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
166
+ const isLocalDirectOwner = local && !hasConfiguredToken;
218
167
 
219
- // Single-owner companion model: map request context directly
168
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
169
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
170
+
171
+ // Single-owner companion model: map request context directly with server identity enforcement
220
172
  const mappingResult = mapRequestContext({
221
173
  ...req.body,
222
174
  id: id || req.body.companionId,
223
- authenticated: identity?.authenticated ?? true,
224
- source: identity?.source ?? 'local',
175
+ authenticated,
176
+ serverRole,
177
+ source: identity?.source ?? (local ? 'local' : 'external'),
225
178
  generateCorrelationId: true,
226
179
  });
227
180
 
@@ -255,12 +208,19 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
255
208
  app.post('/chat/stream', attachIdentity, async (req, res) => {
256
209
  const { id, message, history } = req.body;
257
210
  const identity = (req as any).identity as Identity;
211
+ const local = isLocalRequest(req);
212
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
213
+ const isLocalDirectOwner = local && !hasConfiguredToken;
214
+
215
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
216
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
258
217
 
259
218
  const mappingResult = mapRequestContext({
260
219
  ...req.body,
261
220
  id: id || req.body.companionId,
262
- authenticated: identity?.authenticated ?? true,
263
- source: identity?.source ?? 'local',
221
+ authenticated,
222
+ serverRole,
223
+ source: identity?.source ?? (local ? 'local' : 'external'),
264
224
  generateCorrelationId: true,
265
225
  });
266
226
 
@@ -283,8 +243,8 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
283
243
  const abortController = new AbortController();
284
244
  const onClose = () => {
285
245
  abortController.abort('client_disconnect');
286
- if (typeof runtime.interruptMouth === 'function') {
287
- runtime.interruptMouth('client_disconnect');
246
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
247
+ runtime.mouth.interrupt('client_disconnect');
288
248
  }
289
249
  };
290
250
  req.on('close', onClose);
@@ -309,7 +269,9 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
309
269
  res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
310
270
  }
311
271
 
312
- const speechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
272
+ const maxStreamLen = Number(process.env.SIDURI_MAX_RESPONSE_CHARS || 8000);
273
+ const rawSpeechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
274
+ const speechText = rawSpeechText.slice(0, maxStreamLen);
313
275
  const utterance = {
314
276
  utteranceId: response.response_id || 'utt-stream',
315
277
  companionId,
@@ -355,12 +317,16 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
355
317
  const reason = req.body?.reason || 'user_barge_in';
356
318
 
357
319
  if (runtime) {
358
- runtime.interruptMouth(reason);
320
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
321
+ runtime.mouth.interrupt(reason);
322
+ }
359
323
  return res.json({ success: true, interrupted: true, companionId, reason });
360
324
  }
361
325
 
362
326
  for (const r of runtimes.values()) {
363
- r.interruptMouth(reason);
327
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
328
+ r.mouth.interrupt(reason);
329
+ }
364
330
  }
365
331
  return res.json({ success: true, interrupted: true, reason });
366
332
  });
@@ -371,12 +337,16 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
371
337
  const reason = req.body?.reason || 'user_barge_in';
372
338
 
373
339
  if (runtime) {
374
- runtime.interruptMouth(reason);
340
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
341
+ runtime.mouth.interrupt(reason);
342
+ }
375
343
  return res.json({ success: true, interrupted: true, companionId, reason });
376
344
  }
377
345
 
378
346
  for (const r of runtimes.values()) {
379
- r.interruptMouth(reason);
347
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
348
+ r.mouth.interrupt(reason);
349
+ }
380
350
  }
381
351
  return res.json({ success: true, interrupted: true, reason });
382
352
  });
@@ -388,7 +358,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
388
358
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
389
359
  if (!runtime.memory) return res.json({ proposals: [] });
390
360
  try {
391
- const proposals = await runtime.getPendingClaims();
361
+ const proposals = await runtime.memory.getPendingClaims();
392
362
  res.json({ proposals });
393
363
  } catch (e: any) {
394
364
  res.status(500).json({ error: e.message });
@@ -401,7 +371,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
401
371
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
402
372
  if (!runtime.memory) return res.json({ items: [] });
403
373
  try {
404
- const items = await runtime.getClaims();
374
+ const items = await runtime.memory.getClaims();
405
375
  res.json({ items });
406
376
  } catch (e: any) {
407
377
  res.status(500).json({ error: e.message });
@@ -414,7 +384,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
414
384
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
415
385
  if (!runtime.memory) return res.json({ claims: [] });
416
386
  try {
417
- const claims = await runtime.getClaims();
387
+ const claims = await runtime.memory.getClaims();
418
388
  res.json({ claims });
419
389
  } catch (e: any) {
420
390
  res.status(500).json({ error: e.message });
@@ -427,13 +397,96 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
427
397
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
428
398
  if (!runtime.memory) return res.json({ directives: [] });
429
399
  try {
430
- const directives = await runtime.getDirectives();
400
+ const directives = await runtime.memory.getDirectives();
431
401
  res.json({ directives });
432
402
  } catch (e: any) {
433
403
  res.status(500).json({ error: e.message });
434
404
  }
435
405
  });
436
406
 
407
+ // KNOWLEDGE / LIFE DB GETTERS
408
+ app.get('/knowledge/life', requireAuth, async (req, res) => {
409
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
410
+ const runtime = runtimes.get(id);
411
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
412
+ const query = (req.query.q as string) || '';
413
+ if (!runtime.knowledge || typeof (runtime.knowledge as any).queryLifeContext !== 'function') {
414
+ return res.json({ matchedInventory: [], recentFinances: [], upcomingSchedule: [], preferences: [], formattedContext: '' });
415
+ }
416
+ try {
417
+ const result = await (runtime.knowledge as any).queryLifeContext(id, query);
418
+ res.json(result);
419
+ } catch (e: any) {
420
+ res.status(500).json({ error: e.message });
421
+ }
422
+ });
423
+
424
+ app.get('/knowledge/inventory', requireAuth, async (req, res) => {
425
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
426
+ const runtime = runtimes.get(id);
427
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
428
+ const domain = req.query.domain as string | undefined;
429
+ if (!runtime.knowledge || !(runtime.knowledge as any).inventory) {
430
+ return res.json({ items: [] });
431
+ }
432
+ try {
433
+ const items = await (runtime.knowledge as any).inventory.getItems(id, domain);
434
+ res.json({ items });
435
+ } catch (e: any) {
436
+ res.status(500).json({ error: e.message });
437
+ }
438
+ });
439
+
440
+ app.get('/knowledge/finance', requireAuth, async (req, res) => {
441
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
442
+ const runtime = runtimes.get(id);
443
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
444
+ if (!runtime.knowledge || !(runtime.knowledge as any).finance) {
445
+ return res.json({ summary: null, entries: [] });
446
+ }
447
+ try {
448
+ const limit = Number(req.query.limit || 20);
449
+ const [summary, entries] = await Promise.all([
450
+ (runtime.knowledge as any).finance.getSummary(id),
451
+ (runtime.knowledge as any).finance.getEntries(id, limit),
452
+ ]);
453
+ res.json({ summary, entries });
454
+ } catch (e: any) {
455
+ res.status(500).json({ error: e.message });
456
+ }
457
+ });
458
+
459
+ app.get('/knowledge/schedule', requireAuth, async (req, res) => {
460
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
461
+ const runtime = runtimes.get(id);
462
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
463
+ if (!runtime.knowledge || !(runtime.knowledge as any).schedule) {
464
+ return res.json({ items: [] });
465
+ }
466
+ try {
467
+ const items = await (runtime.knowledge as any).schedule.getUpcoming(id);
468
+ res.json({ items });
469
+ } catch (e: any) {
470
+ res.status(500).json({ error: e.message });
471
+ }
472
+ });
473
+
474
+ app.get('/knowledge/preferences', requireAuth, async (req, res) => {
475
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
476
+ const runtime = runtimes.get(id);
477
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
478
+ const category = req.query.category as string | undefined;
479
+ if (!runtime.knowledge || !(runtime.knowledge as any).preferences) {
480
+ return res.json({ preferences: [] });
481
+ }
482
+ try {
483
+ const preferences = await (runtime.knowledge as any).preferences.getPreferences(id, category);
484
+ res.json({ preferences });
485
+ } catch (e: any) {
486
+ res.status(500).json({ error: e.message });
487
+ }
488
+ });
489
+
437
490
  // MEMORY MUTATIONS - PROPOSALS
438
491
  app.post('/memory/proposals/update', requireAuth, async (req, res) => {
439
492
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
@@ -447,7 +500,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
447
500
  if (!claimId) {
448
501
  return res.status(400).json({ error: "Missing required claim id" });
449
502
  }
450
- const updated = await runtime.updateClaim(claimId, req.body.updates || req.body);
503
+ const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
451
504
  res.json({ success: true, claim: updated });
452
505
  } catch (e: any) {
453
506
  res.status(500).json({ error: e.message });
@@ -460,7 +513,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
460
513
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
461
514
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
462
515
  try {
463
- await runtime.approveClaim(req.body.id);
516
+ await runtime.memory.approveClaim(req.body.id);
464
517
  res.json({ approved: true });
465
518
  } catch (e: any) {
466
519
  res.status(500).json({ error: e.message });
@@ -473,7 +526,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
473
526
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
474
527
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
475
528
  try {
476
- await runtime.rejectClaim(req.body.id);
529
+ await runtime.memory.rejectClaim(req.body.id);
477
530
  res.json({ rejected: true });
478
531
  } catch (e: any) {
479
532
  res.status(500).json({ error: e.message });
@@ -487,7 +540,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
487
540
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
488
541
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
489
542
  try {
490
- await runtime.approveDirective(req.body.id);
543
+ await runtime.memory.approveDirective(req.body.id);
491
544
  res.json({ approved: true });
492
545
  } catch (e: any) {
493
546
  res.status(500).json({ error: e.message });
@@ -500,7 +553,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
500
553
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
501
554
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
502
555
  try {
503
- await runtime.rejectDirective(req.body.id);
556
+ await runtime.memory.rejectDirective(req.body.id);
504
557
  res.json({ rejected: true });
505
558
  } catch (e: any) {
506
559
  res.status(500).json({ error: e.message });
@@ -513,7 +566,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
513
566
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
514
567
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
515
568
  try {
516
- await runtime.revokeDirective(req.body.id);
569
+ await runtime.memory.revokeDirective(req.body.id);
517
570
  res.json({ revoked: true });
518
571
  } catch (e: any) {
519
572
  res.status(500).json({ error: e.message });
@@ -526,7 +579,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
526
579
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
527
580
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
528
581
  try {
529
- await runtime.disableDirective(req.body.id);
582
+ await runtime.memory.disableDirective(req.body.id);
530
583
  res.json({ disabled: true });
531
584
  } catch (e: any) {
532
585
  res.status(500).json({ error: e.message });
@@ -543,7 +596,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
543
596
  return res.status(400).json({ error: "Memory organ does not support reset" });
544
597
  }
545
598
  try {
546
- await runtime.resetMemory();
599
+ await runtime.memory.resetMemory();
547
600
  res.json({ reset: true });
548
601
  } catch (e: any) {
549
602
  res.status(500).json({ error: e.message });
@@ -554,7 +607,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
554
607
  const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
555
608
  const runtime = runtimes.get(companionId);
556
609
  if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
557
- const staged = runtime.stageResponse({
610
+ const staged = runtime.gating.stageResponse({
558
611
  requestContext: {
559
612
  companionId,
560
613
  actor: {
@@ -590,12 +643,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
590
643
  let correlationId = req.body?.correlation_id;
591
644
 
592
645
  if (!responseId && correlationId) {
593
- const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
646
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
594
647
  if (found) {
595
648
  responseId = found.responseId;
596
649
  }
597
650
  } else if (responseId && !correlationId) {
598
- const found = runtime.getStagedPlan(responseId);
651
+ const found = runtime.gating.getStagedPlan(responseId);
599
652
  if (found) {
600
653
  correlationId = found.correlationId;
601
654
  }
@@ -605,7 +658,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
605
658
  return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
606
659
  }
607
660
 
608
- const result = runtime.approveResponse({
661
+ const result = runtime.gating.approveResponse({
609
662
  responseId,
610
663
  companionId,
611
664
  correlationId: correlationId || '',
@@ -616,7 +669,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
616
669
  }
617
670
 
618
671
  // Now evaluated as approved
619
- const evaluation = runtime.evaluateGate(result.plan!);
672
+ const evaluation = runtime.gating.evaluateGate(result.plan!);
620
673
  res.json({
621
674
  approved: true,
622
675
  status: evaluation.disposition,
@@ -635,12 +688,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
635
688
  let correlationId = req.body?.correlation_id;
636
689
 
637
690
  if (!responseId && correlationId) {
638
- const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
691
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
639
692
  if (found) {
640
693
  responseId = found.responseId;
641
694
  }
642
695
  } else if (responseId && !correlationId) {
643
- const found = runtime.getStagedPlan(responseId);
696
+ const found = runtime.gating.getStagedPlan(responseId);
644
697
  if (found) {
645
698
  correlationId = found.correlationId;
646
699
  }
@@ -650,7 +703,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
650
703
  return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
651
704
  }
652
705
 
653
- const result = runtime.rejectResponse({
706
+ const result = runtime.gating.rejectResponse({
654
707
  responseId,
655
708
  companionId,
656
709
  correlationId: correlationId || '',