@siduri-x/api 1.0.1 → 1.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 +11 -10
- package/src/app.ts +276 -49
- package/src/index.test.ts +53 -0
- package/src/index.ts +30 -21
- package/src/runtime.test.ts +55 -1
- package/dist/app.d.ts +0 -9
- package/dist/app.js +0 -480
- package/dist/auth.d.ts +0 -8
- package/dist/auth.js +0 -40
- package/dist/auth.test.d.ts +0 -1
- package/dist/auth.test.js +0 -48
- package/dist/b0-b6.test.d.ts +0 -1
- package/dist/b0-b6.test.js +0 -121
- package/dist/context-mapper.d.ts +0 -15
- package/dist/context-mapper.js +0 -287
- package/dist/context-mapper.test.d.ts +0 -1
- package/dist/context-mapper.test.js +0 -233
- package/dist/cors.d.ts +0 -3
- package/dist/cors.js +0 -42
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -167
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -115
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +0 -17
- package/dist/runtime.test.d.ts +0 -1
- package/dist/runtime.test.js +0 -240
- package/dist/smoke.test.d.ts +0 -0
- package/dist/smoke.test.js +0 -6
- package/dist/t4-gating.test.d.ts +0 -1
- package/dist/t4-gating.test.js +0 -193
- package/dist/t5-experience.test.d.ts +0 -1
- package/dist/t5-experience.test.js +0 -156
- package/dist/t6-security.test.d.ts +0 -1
- package/dist/t6-security.test.js +0 -424
- package/dist/t7-release.test.d.ts +0 -1
- package/dist/t7-release.test.js +0 -119
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siduri-x/api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"cors": "^2.8.5",
|
|
7
|
-
"dotenv": "^16.3.1",
|
|
8
|
-
"express": "^4.18.2",
|
|
9
6
|
"@siduri-x/behavior": "1.0.5",
|
|
10
7
|
"@siduri-x/body": "1.0.4",
|
|
11
|
-
"@siduri-x/brain": "1.0.
|
|
12
|
-
"@siduri-x/core": "1.0.
|
|
13
|
-
"@siduri-x/ear": "1.0.
|
|
14
|
-
"@siduri-x/hands": "1.0.
|
|
8
|
+
"@siduri-x/brain": "1.0.4",
|
|
9
|
+
"@siduri-x/core": "1.0.5",
|
|
10
|
+
"@siduri-x/ear": "1.0.3",
|
|
11
|
+
"@siduri-x/hands": "1.0.3",
|
|
15
12
|
"@siduri-x/knowledge": "1.0.2",
|
|
16
|
-
"@siduri-x/memory": "1.0.
|
|
13
|
+
"@siduri-x/memory": "1.0.4",
|
|
17
14
|
"@siduri-x/observation": "1.0.2",
|
|
18
15
|
"@siduri-x/vision": "1.0.2",
|
|
19
|
-
"@siduri-x/voice": "1.0.
|
|
16
|
+
"@siduri-x/voice": "1.0.6",
|
|
17
|
+
"@siduri-x/mouth": "1.0.0",
|
|
18
|
+
"cors": "^2.8.5",
|
|
19
|
+
"dotenv": "^16.3.1",
|
|
20
|
+
"express": "^4.18.2"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/cors": "^2.8.17",
|
package/src/app.ts
CHANGED
|
@@ -4,17 +4,48 @@ import { createCorsOptions } from './cors';
|
|
|
4
4
|
import { SiduriRuntime, dispatchCompanionChat } from './runtime';
|
|
5
5
|
import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
|
|
6
6
|
import { PostgresMemoryOrgan } from '@siduri-x/memory';
|
|
7
|
-
import { VoiceAdapter } from '@siduri-x/voice';
|
|
8
|
-
import { EKnowledgeAdapter } from '@siduri-x/knowledge';
|
|
9
|
-
import { OpenRouterVisionAdapter } from '@siduri-x/vision';
|
|
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
10
|
import { ActiveSelfCompiler } from '@siduri-x/behavior';
|
|
11
|
-
import { Live2DAdapter } from '@siduri-x/body';
|
|
11
|
+
import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
|
|
12
12
|
import { FixtureObservationOrgan } from '@siduri-x/observation';
|
|
13
|
-
import { DefaultHandsOrgan } from '@siduri-x/hands';
|
|
14
|
-
import { DefaultEarOrgan } from '@siduri-x/ear';
|
|
13
|
+
import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@siduri-x/hands';
|
|
14
|
+
import { DefaultEarOrgan, EarOrganConfig } from '@siduri-x/ear';
|
|
15
|
+
import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@siduri-x/mouth';
|
|
15
16
|
import { attachIdentity, requireRole, Identity } from './auth';
|
|
16
17
|
import { mapRequestContext } from './context-mapper';
|
|
17
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
|
+
}
|
|
48
|
+
|
|
18
49
|
export interface AppInstance {
|
|
19
50
|
app: Express;
|
|
20
51
|
runtimes: Map<string, SiduriRuntime>;
|
|
@@ -28,62 +59,77 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
28
59
|
|
|
29
60
|
let observationOrgan: FixtureObservationOrgan | undefined;
|
|
30
61
|
|
|
31
|
-
function createBrain(config
|
|
32
|
-
const provider = config
|
|
62
|
+
function createBrain(config?: AppBrainConfig) {
|
|
63
|
+
const provider = config?.provider || 'openrouter';
|
|
33
64
|
const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
|
|
34
|
-
const apiKey = config
|
|
65
|
+
const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
|
|
35
66
|
if (provider === 'openai-compatible') {
|
|
36
67
|
return new OpenAICompatibleBrain({
|
|
37
68
|
apiKey,
|
|
38
|
-
model: config
|
|
39
|
-
baseUrl: config
|
|
69
|
+
model: config?.model || 'local-model',
|
|
70
|
+
baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
|
|
40
71
|
});
|
|
41
72
|
}
|
|
42
|
-
return new OpenRouterBrain({ apiKey, model: config
|
|
73
|
+
return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
|
|
43
74
|
}
|
|
44
75
|
|
|
45
|
-
function isDisabled(config
|
|
76
|
+
function isDisabled(config?: { provider?: string }): boolean {
|
|
46
77
|
return !config || config.provider === 'none';
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
function createVoice(config
|
|
80
|
+
function createVoice(config?: VoiceConfig) {
|
|
50
81
|
return isDisabled(config)
|
|
51
82
|
? undefined
|
|
52
|
-
: new VoiceAdapter({
|
|
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
|
+
});
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
function createKnowledge(config
|
|
56
|
-
return isDisabled(config) ? undefined : new EKnowledgeAdapter(config);
|
|
91
|
+
function createKnowledge(config?: EKnowledgeConfig) {
|
|
92
|
+
return isDisabled(config) ? undefined : new EKnowledgeAdapter(config || {});
|
|
57
93
|
}
|
|
58
94
|
|
|
59
|
-
function createVision(config
|
|
95
|
+
function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
|
|
60
96
|
return isDisabled(config)
|
|
61
97
|
? undefined
|
|
62
|
-
: new OpenRouterVisionAdapter({
|
|
98
|
+
: new OpenRouterVisionAdapter({
|
|
99
|
+
apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
|
|
100
|
+
model: config?.model || 'gpt-4-vision',
|
|
101
|
+
...config,
|
|
102
|
+
});
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
function createBehavior(config
|
|
105
|
+
function createBehavior(config?: AppBehaviorConfig) {
|
|
66
106
|
return isDisabled(config) ? undefined : new ActiveSelfCompiler();
|
|
67
107
|
}
|
|
68
108
|
|
|
69
|
-
function createBody(config
|
|
109
|
+
function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
|
|
70
110
|
return isDisabled(config)
|
|
71
111
|
? undefined
|
|
72
112
|
: new Live2DAdapter(config);
|
|
73
113
|
}
|
|
74
114
|
|
|
75
|
-
function createHands(config
|
|
115
|
+
function createHands(config?: DefaultHandsOrganConfig & { provider?: string }) {
|
|
76
116
|
return isDisabled(config)
|
|
77
117
|
? new DefaultHandsOrgan()
|
|
78
118
|
: new DefaultHandsOrgan(config);
|
|
79
119
|
}
|
|
80
120
|
|
|
81
|
-
function createEar(config
|
|
121
|
+
function createEar(config?: EarOrganConfig & { provider?: string }) {
|
|
82
122
|
return isDisabled(config)
|
|
83
123
|
? new DefaultEarOrgan()
|
|
84
124
|
: new DefaultEarOrgan(config);
|
|
85
125
|
}
|
|
86
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
|
+
|
|
87
133
|
app.post('/boot', requireRole(['OWNER']), async (req, res) => {
|
|
88
134
|
try {
|
|
89
135
|
const { id, config } = req.body;
|
|
@@ -100,8 +146,21 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
100
146
|
const body = createBody(config.body);
|
|
101
147
|
const hands = createHands(config.hands);
|
|
102
148
|
const ear = createEar(config.ear);
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
});
|
|
105
164
|
await runtime.initialize();
|
|
106
165
|
|
|
107
166
|
runtimes.set(id, runtime);
|
|
@@ -126,7 +185,22 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
126
185
|
const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
|
|
127
186
|
res.json({ provider: "siduri-voice", configured: hasVoice });
|
|
128
187
|
});
|
|
129
|
-
app.get('/obs/health', (req, res) =>
|
|
188
|
+
app.get('/obs/health', (req, res) => {
|
|
189
|
+
const connected = Boolean(observationOrgan) || Array.from(runtimes.values()).some((r) => Boolean(r.observation));
|
|
190
|
+
res.json({ connected });
|
|
191
|
+
});
|
|
192
|
+
app.get('/mouth/health', (req, res) => {
|
|
193
|
+
const hasMouth = Array.from(runtimes.values()).some((r) => Boolean(r.mouth));
|
|
194
|
+
res.json({ provider: "siduri-mouth", configured: hasMouth });
|
|
195
|
+
});
|
|
196
|
+
app.get('/mouth/channels', (req, res) => {
|
|
197
|
+
const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
|
|
198
|
+
const runtime = runtimes.get(id);
|
|
199
|
+
if (!runtime || !runtime.mouth || typeof runtime.mouth.getRegisteredChannels !== 'function') {
|
|
200
|
+
return res.json({ channels: [] });
|
|
201
|
+
}
|
|
202
|
+
res.json({ channels: runtime.mouth.getRegisteredChannels() });
|
|
203
|
+
});
|
|
130
204
|
app.get('/me', attachIdentity, (req, res) => {
|
|
131
205
|
const identity = (req as any).identity as Identity;
|
|
132
206
|
res.json({
|
|
@@ -182,6 +256,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
182
256
|
message,
|
|
183
257
|
context: mappingResult.context,
|
|
184
258
|
history,
|
|
259
|
+
...(req.body?.medium ? { medium: req.body.medium } : {}),
|
|
185
260
|
});
|
|
186
261
|
res.json(response);
|
|
187
262
|
} catch (e: any) {
|
|
@@ -189,6 +264,147 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
189
264
|
}
|
|
190
265
|
});
|
|
191
266
|
|
|
267
|
+
// REAL-TIME SSE STREAMING (Mouth transport)
|
|
268
|
+
app.post('/chat/stream', attachIdentity, async (req, res) => {
|
|
269
|
+
const { id, message, history } = req.body;
|
|
270
|
+
const identity = (req as any).identity as Identity;
|
|
271
|
+
|
|
272
|
+
const effectiveRole = req.body?.role === 'VIEWER' || req.body?.context?.actor?.authorizationRole === 'viewer'
|
|
273
|
+
? 'VIEWER'
|
|
274
|
+
: (identity?.role === 'OPERATOR' ? 'OPERATOR' : 'OWNER');
|
|
275
|
+
const isOwner = effectiveRole === 'OWNER';
|
|
276
|
+
|
|
277
|
+
const mappingResult = mapRequestContext(
|
|
278
|
+
{
|
|
279
|
+
...req.body,
|
|
280
|
+
id: id || req.body.companionId,
|
|
281
|
+
role: effectiveRole,
|
|
282
|
+
authenticated: isOwner,
|
|
283
|
+
generateCorrelationId: true,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
endpointPolicy: 'public',
|
|
287
|
+
defaultPublicAudience: 'audience-public',
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!mappingResult.accepted) {
|
|
292
|
+
return res.status(400).json({
|
|
293
|
+
accepted: false,
|
|
294
|
+
error: mappingResult.error,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const companionId = mappingResult.context!.companionId;
|
|
299
|
+
const runtime = runtimes.get(companionId);
|
|
300
|
+
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
301
|
+
|
|
302
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
303
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
304
|
+
res.setHeader('Connection', 'keep-alive');
|
|
305
|
+
res.flushHeaders?.();
|
|
306
|
+
|
|
307
|
+
const abortController = new AbortController();
|
|
308
|
+
const onClose = () => {
|
|
309
|
+
abortController.abort('client_disconnect');
|
|
310
|
+
if (typeof runtime.interruptMouth === 'function') {
|
|
311
|
+
runtime.interruptMouth('client_disconnect');
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
req.on('close', onClose);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const response = await dispatchCompanionChat(runtime, {
|
|
318
|
+
id: companionId,
|
|
319
|
+
companionId,
|
|
320
|
+
message,
|
|
321
|
+
context: mappingResult.context,
|
|
322
|
+
history,
|
|
323
|
+
medium: 'web',
|
|
324
|
+
signal: abortController.signal,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
|
|
328
|
+
|
|
329
|
+
const avatarEvent = response.metadata?.events?.find(
|
|
330
|
+
(e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval)
|
|
331
|
+
);
|
|
332
|
+
if (avatarEvent) {
|
|
333
|
+
res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const speechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
|
|
337
|
+
const utterance = {
|
|
338
|
+
utteranceId: response.response_id || 'utt-stream',
|
|
339
|
+
companionId,
|
|
340
|
+
responseId: response.response_id,
|
|
341
|
+
correlationId: response.correlation_id,
|
|
342
|
+
text: speechText,
|
|
343
|
+
medium: 'web' as const,
|
|
344
|
+
expression: avatarEvent?.expression,
|
|
345
|
+
action: avatarEvent?.action,
|
|
346
|
+
signal: abortController.signal,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (runtime.mouth && typeof runtime.mouth.stream === 'function') {
|
|
350
|
+
for await (const chunk of runtime.mouth.stream(utterance)) {
|
|
351
|
+
if (abortController.signal.aborted) {
|
|
352
|
+
res.write(`event: chunk\ndata: ${JSON.stringify({ ...chunk, interrupted: true })}\n\n`);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
res.write(`event: chunk\ndata: ${JSON.stringify(chunk)}\n\n`);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
res.write(`event: chunk\ndata: ${JSON.stringify({ utteranceId: utterance.utteranceId, index: 1, deltaText: speechText, isComplete: true, medium: 'web' })}\n\n`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
res.write(`event: done\ndata: ${JSON.stringify(response)}\n\n`);
|
|
362
|
+
res.end();
|
|
363
|
+
} catch (e: any) {
|
|
364
|
+
if (abortController.signal.aborted) {
|
|
365
|
+
res.write(`event: interrupted\ndata: ${JSON.stringify({ reason: abortController.signal.reason })}\n\n`);
|
|
366
|
+
} else {
|
|
367
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: e.message })}\n\n`);
|
|
368
|
+
}
|
|
369
|
+
res.end();
|
|
370
|
+
} finally {
|
|
371
|
+
req.removeListener('close', onClose);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// INTERRUPTION / BARGE-IN
|
|
376
|
+
app.post('/chat/interrupt', attachIdentity, (req, res) => {
|
|
377
|
+
const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
|
|
378
|
+
const runtime = companionId ? runtimes.get(companionId) : undefined;
|
|
379
|
+
const reason = req.body?.reason || 'user_barge_in';
|
|
380
|
+
|
|
381
|
+
if (runtime) {
|
|
382
|
+
runtime.interruptMouth(reason);
|
|
383
|
+
return res.json({ success: true, interrupted: true, companionId, reason });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const r of runtimes.values()) {
|
|
387
|
+
r.interruptMouth(reason);
|
|
388
|
+
}
|
|
389
|
+
return res.json({ success: true, interrupted: true, reason });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
app.post('/mouth/interrupt', attachIdentity, (req, res) => {
|
|
393
|
+
const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
|
|
394
|
+
const runtime = companionId ? runtimes.get(companionId) : undefined;
|
|
395
|
+
const reason = req.body?.reason || 'user_barge_in';
|
|
396
|
+
|
|
397
|
+
if (runtime) {
|
|
398
|
+
runtime.interruptMouth(reason);
|
|
399
|
+
return res.json({ success: true, interrupted: true, companionId, reason });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const r of runtimes.values()) {
|
|
403
|
+
r.interruptMouth(reason);
|
|
404
|
+
}
|
|
405
|
+
return res.json({ success: true, interrupted: true, reason });
|
|
406
|
+
});
|
|
407
|
+
|
|
192
408
|
// MEMORY GETTERS
|
|
193
409
|
app.get('/memory/proposals', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
|
|
194
410
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
@@ -196,7 +412,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
196
412
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
197
413
|
if (!runtime.memory) return res.json({ proposals: [] });
|
|
198
414
|
try {
|
|
199
|
-
const proposals = await runtime.
|
|
415
|
+
const proposals = await runtime.getPendingClaims();
|
|
200
416
|
res.json({ proposals });
|
|
201
417
|
} catch (e: any) {
|
|
202
418
|
res.status(500).json({ error: e.message });
|
|
@@ -209,7 +425,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
209
425
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
210
426
|
if (!runtime.memory) return res.json({ items: [] });
|
|
211
427
|
try {
|
|
212
|
-
const items = await runtime.
|
|
428
|
+
const items = await runtime.getClaims();
|
|
213
429
|
res.json({ items });
|
|
214
430
|
} catch (e: any) {
|
|
215
431
|
res.status(500).json({ error: e.message });
|
|
@@ -222,7 +438,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
222
438
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
223
439
|
if (!runtime.memory) return res.json({ claims: [] });
|
|
224
440
|
try {
|
|
225
|
-
const claims = await runtime.
|
|
441
|
+
const claims = await runtime.getClaims();
|
|
226
442
|
res.json({ claims });
|
|
227
443
|
} catch (e: any) {
|
|
228
444
|
res.status(500).json({ error: e.message });
|
|
@@ -235,7 +451,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
235
451
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
236
452
|
if (!runtime.memory) return res.json({ directives: [] });
|
|
237
453
|
try {
|
|
238
|
-
const directives = await runtime.
|
|
454
|
+
const directives = await runtime.getDirectives();
|
|
239
455
|
res.json({ directives });
|
|
240
456
|
} catch (e: any) {
|
|
241
457
|
res.status(500).json({ error: e.message });
|
|
@@ -255,7 +471,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
255
471
|
if (!claimId) {
|
|
256
472
|
return res.status(400).json({ error: "Missing required claim id" });
|
|
257
473
|
}
|
|
258
|
-
const updated = await runtime.
|
|
474
|
+
const updated = await runtime.updateClaim(claimId, req.body.updates || req.body);
|
|
259
475
|
res.json({ success: true, claim: updated });
|
|
260
476
|
} catch (e: any) {
|
|
261
477
|
res.status(500).json({ error: e.message });
|
|
@@ -268,7 +484,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
268
484
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
269
485
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
270
486
|
try {
|
|
271
|
-
await runtime.
|
|
487
|
+
await runtime.approveClaim(req.body.id);
|
|
272
488
|
res.json({ approved: true });
|
|
273
489
|
} catch (e: any) {
|
|
274
490
|
res.status(500).json({ error: e.message });
|
|
@@ -281,7 +497,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
281
497
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
282
498
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
283
499
|
try {
|
|
284
|
-
await runtime.
|
|
500
|
+
await runtime.rejectClaim(req.body.id);
|
|
285
501
|
res.json({ rejected: true });
|
|
286
502
|
} catch (e: any) {
|
|
287
503
|
res.status(500).json({ error: e.message });
|
|
@@ -295,7 +511,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
295
511
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
296
512
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
297
513
|
try {
|
|
298
|
-
await runtime.
|
|
514
|
+
await runtime.approveDirective(req.body.id);
|
|
299
515
|
res.json({ approved: true });
|
|
300
516
|
} catch (e: any) {
|
|
301
517
|
res.status(500).json({ error: e.message });
|
|
@@ -308,7 +524,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
308
524
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
309
525
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
310
526
|
try {
|
|
311
|
-
await runtime.
|
|
527
|
+
await runtime.rejectDirective(req.body.id);
|
|
312
528
|
res.json({ rejected: true });
|
|
313
529
|
} catch (e: any) {
|
|
314
530
|
res.status(500).json({ error: e.message });
|
|
@@ -321,7 +537,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
321
537
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
322
538
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
323
539
|
try {
|
|
324
|
-
await runtime.
|
|
540
|
+
await runtime.revokeDirective(req.body.id);
|
|
325
541
|
res.json({ revoked: true });
|
|
326
542
|
} catch (e: any) {
|
|
327
543
|
res.status(500).json({ error: e.message });
|
|
@@ -334,7 +550,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
334
550
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
335
551
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
336
552
|
try {
|
|
337
|
-
await runtime.
|
|
553
|
+
await runtime.disableDirective(req.body.id);
|
|
338
554
|
res.json({ disabled: true });
|
|
339
555
|
} catch (e: any) {
|
|
340
556
|
res.status(500).json({ error: e.message });
|
|
@@ -351,7 +567,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
351
567
|
return res.status(400).json({ error: "Memory organ does not support reset" });
|
|
352
568
|
}
|
|
353
569
|
try {
|
|
354
|
-
await runtime.
|
|
570
|
+
await runtime.resetMemory();
|
|
355
571
|
res.json({ reset: true });
|
|
356
572
|
} catch (e: any) {
|
|
357
573
|
res.status(500).json({ error: e.message });
|
|
@@ -362,7 +578,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
362
578
|
const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
|
|
363
579
|
const runtime = runtimes.get(companionId);
|
|
364
580
|
if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
|
|
365
|
-
const staged = runtime.
|
|
581
|
+
const staged = runtime.stageResponse({
|
|
366
582
|
requestContext: {
|
|
367
583
|
companionId,
|
|
368
584
|
actor: {
|
|
@@ -401,12 +617,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
401
617
|
let correlationId = req.body?.correlation_id;
|
|
402
618
|
|
|
403
619
|
if (!responseId && correlationId) {
|
|
404
|
-
const found = runtime.
|
|
620
|
+
const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
|
|
405
621
|
if (found) {
|
|
406
622
|
responseId = found.responseId;
|
|
407
623
|
}
|
|
408
624
|
} else if (responseId && !correlationId) {
|
|
409
|
-
const found = runtime.
|
|
625
|
+
const found = runtime.getStagedPlan(responseId);
|
|
410
626
|
if (found) {
|
|
411
627
|
correlationId = found.correlationId;
|
|
412
628
|
}
|
|
@@ -416,7 +632,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
416
632
|
return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
|
|
417
633
|
}
|
|
418
634
|
|
|
419
|
-
const result = runtime.
|
|
635
|
+
const result = runtime.approveResponse({
|
|
420
636
|
responseId,
|
|
421
637
|
companionId,
|
|
422
638
|
correlationId: correlationId || '',
|
|
@@ -428,7 +644,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
428
644
|
}
|
|
429
645
|
|
|
430
646
|
// Now evaluated as approved
|
|
431
|
-
const evaluation = runtime.
|
|
647
|
+
const evaluation = runtime.evaluateGate(result.plan!);
|
|
432
648
|
res.json({
|
|
433
649
|
approved: true,
|
|
434
650
|
status: evaluation.disposition,
|
|
@@ -447,12 +663,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
447
663
|
let correlationId = req.body?.correlation_id;
|
|
448
664
|
|
|
449
665
|
if (!responseId && correlationId) {
|
|
450
|
-
const found = runtime.
|
|
666
|
+
const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
|
|
451
667
|
if (found) {
|
|
452
668
|
responseId = found.responseId;
|
|
453
669
|
}
|
|
454
670
|
} else if (responseId && !correlationId) {
|
|
455
|
-
const found = runtime.
|
|
671
|
+
const found = runtime.getStagedPlan(responseId);
|
|
456
672
|
if (found) {
|
|
457
673
|
correlationId = found.correlationId;
|
|
458
674
|
}
|
|
@@ -462,7 +678,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
462
678
|
return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
|
|
463
679
|
}
|
|
464
680
|
|
|
465
|
-
const result = runtime.
|
|
681
|
+
const result = runtime.rejectResponse({
|
|
466
682
|
responseId,
|
|
467
683
|
companionId,
|
|
468
684
|
correlationId: correlationId || '',
|
|
@@ -481,8 +697,10 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
481
697
|
});
|
|
482
698
|
|
|
483
699
|
app.post('/dev/mock-observation', async (req, res) => {
|
|
484
|
-
|
|
485
|
-
const
|
|
700
|
+
const activeRuntime = Array.from(runtimes.values())[0];
|
|
701
|
+
const targetObs = activeRuntime?.observation || observationOrgan;
|
|
702
|
+
if (!targetObs) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
|
|
703
|
+
const result = await targetObs.ingest(
|
|
486
704
|
new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
|
|
487
705
|
'fixture-observation',
|
|
488
706
|
'configured-vision',
|
|
@@ -492,5 +710,14 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
492
710
|
});
|
|
493
711
|
}
|
|
494
712
|
|
|
495
|
-
return {
|
|
713
|
+
return {
|
|
714
|
+
app,
|
|
715
|
+
runtimes,
|
|
716
|
+
setObservationOrgan: (org: FixtureObservationOrgan) => {
|
|
717
|
+
observationOrgan = org;
|
|
718
|
+
for (const r of runtimes.values()) {
|
|
719
|
+
r.observation = org;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
};
|
|
496
723
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -126,4 +126,57 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
|
|
|
126
126
|
expect(res.body.error.code).toBe('FORBIDDEN_CONTEXT');
|
|
127
127
|
expect(fakeRuntime.handleUserMessage).not.toHaveBeenCalled();
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
test('streams response chunks via POST /chat/stream', async () => {
|
|
131
|
+
fakeRuntime.mouth = {
|
|
132
|
+
stream: async function* () {
|
|
133
|
+
yield { utteranceId: 'utt-1', index: 1, deltaText: 'Hello', isComplete: false, medium: 'web' };
|
|
134
|
+
yield { utteranceId: 'utt-1', index: 2, deltaText: ' world', isComplete: false, medium: 'web' };
|
|
135
|
+
yield { utteranceId: 'utt-1', index: 3, deltaText: '', isComplete: true, medium: 'web' };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const res = await request(app)
|
|
140
|
+
.post('/chat/stream')
|
|
141
|
+
.send({
|
|
142
|
+
id: 'companion-a',
|
|
143
|
+
message: 'Stream me',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
expect(res.headers['content-type']).toContain('text/event-stream');
|
|
148
|
+
expect(res.text).toContain('event: staged');
|
|
149
|
+
expect(res.text).toContain('event: chunk');
|
|
150
|
+
expect(res.text).toContain('event: done');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('handles barge-in interruption via POST /chat/interrupt', async () => {
|
|
154
|
+
fakeRuntime.interruptMouth = jest.fn();
|
|
155
|
+
|
|
156
|
+
const res = await request(app)
|
|
157
|
+
.post('/chat/interrupt')
|
|
158
|
+
.send({
|
|
159
|
+
companionId: 'companion-a',
|
|
160
|
+
reason: 'user_stop',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(res.status).toBe(200);
|
|
164
|
+
expect(res.body.interrupted).toBe(true);
|
|
165
|
+
expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_stop');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('handles mouth interruption via POST /mouth/interrupt', async () => {
|
|
169
|
+
fakeRuntime.interruptMouth = jest.fn();
|
|
170
|
+
|
|
171
|
+
const res = await request(app)
|
|
172
|
+
.post('/mouth/interrupt')
|
|
173
|
+
.send({
|
|
174
|
+
companionId: 'companion-a',
|
|
175
|
+
reason: 'user_barge_in',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(res.status).toBe(200);
|
|
179
|
+
expect(res.body.interrupted).toBe(true);
|
|
180
|
+
expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_barge_in');
|
|
181
|
+
});
|
|
129
182
|
});
|