@siduri-x/api 1.0.0 → 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 +14 -13
- package/src/app.ts +445 -181
- package/src/b0-b6.test.ts +1 -1
- package/src/cors.ts +44 -0
- package/src/index.test.ts +54 -1
- package/src/index.ts +32 -23
- package/src/runtime.test.ts +55 -1
- package/src/t6-security.test.ts +210 -1
- package/dist/app.d.ts +0 -9
- package/dist/app.js +0 -436
- 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/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 -234
- package/dist/t7-release.test.d.ts +0 -1
- package/dist/t7-release.test.js +0 -119
package/src/app.ts
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import express, { Express } from 'express';
|
|
2
2
|
import cors from 'cors';
|
|
3
|
-
import {
|
|
3
|
+
import { createCorsOptions } from './cors';
|
|
4
|
+
import { SiduriRuntime, dispatchCompanionChat } from './runtime';
|
|
4
5
|
import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
|
|
5
6
|
import { PostgresMemoryOrgan } from '@siduri-x/memory';
|
|
6
|
-
import {
|
|
7
|
-
import { EKnowledgeAdapter } from '@siduri-x/knowledge';
|
|
8
|
-
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';
|
|
9
10
|
import { ActiveSelfCompiler } from '@siduri-x/behavior';
|
|
10
|
-
import { Live2DAdapter } from '@siduri-x/body';
|
|
11
|
+
import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
|
|
11
12
|
import { FixtureObservationOrgan } from '@siduri-x/observation';
|
|
12
|
-
import { DefaultHandsOrgan } from '@siduri-x/hands';
|
|
13
|
-
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';
|
|
14
16
|
import { attachIdentity, requireRole, Identity } from './auth';
|
|
15
17
|
import { mapRequestContext } from './context-mapper';
|
|
16
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
|
+
|
|
17
49
|
export interface AppInstance {
|
|
18
50
|
app: Express;
|
|
19
51
|
runtimes: Map<string, SiduriRuntime>;
|
|
@@ -22,67 +54,82 @@ export interface AppInstance {
|
|
|
22
54
|
|
|
23
55
|
export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): AppInstance {
|
|
24
56
|
const app: Express = express();
|
|
25
|
-
app.use(cors());
|
|
57
|
+
app.use(cors(createCorsOptions()));
|
|
26
58
|
app.use(express.json());
|
|
27
59
|
|
|
28
60
|
let observationOrgan: FixtureObservationOrgan | undefined;
|
|
29
61
|
|
|
30
|
-
function createBrain(config
|
|
31
|
-
const provider = config
|
|
62
|
+
function createBrain(config?: AppBrainConfig) {
|
|
63
|
+
const provider = config?.provider || 'openrouter';
|
|
32
64
|
const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
|
|
33
|
-
const apiKey = config
|
|
65
|
+
const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
|
|
34
66
|
if (provider === 'openai-compatible') {
|
|
35
67
|
return new OpenAICompatibleBrain({
|
|
36
68
|
apiKey,
|
|
37
|
-
model: config
|
|
38
|
-
baseUrl: config
|
|
69
|
+
model: config?.model || 'local-model',
|
|
70
|
+
baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
|
|
39
71
|
});
|
|
40
72
|
}
|
|
41
|
-
return new OpenRouterBrain({ apiKey, model: config
|
|
73
|
+
return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
function isDisabled(config
|
|
76
|
+
function isDisabled(config?: { provider?: string }): boolean {
|
|
45
77
|
return !config || config.provider === 'none';
|
|
46
78
|
}
|
|
47
79
|
|
|
48
|
-
function createVoice(config
|
|
80
|
+
function createVoice(config?: VoiceConfig) {
|
|
49
81
|
return isDisabled(config)
|
|
50
82
|
? undefined
|
|
51
|
-
: new
|
|
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
|
+
});
|
|
52
89
|
}
|
|
53
90
|
|
|
54
|
-
function createKnowledge(config
|
|
55
|
-
return isDisabled(config) ? undefined : new EKnowledgeAdapter(config);
|
|
91
|
+
function createKnowledge(config?: EKnowledgeConfig) {
|
|
92
|
+
return isDisabled(config) ? undefined : new EKnowledgeAdapter(config || {});
|
|
56
93
|
}
|
|
57
94
|
|
|
58
|
-
function createVision(config
|
|
95
|
+
function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
|
|
59
96
|
return isDisabled(config)
|
|
60
97
|
? undefined
|
|
61
|
-
: new OpenRouterVisionAdapter({
|
|
98
|
+
: new OpenRouterVisionAdapter({
|
|
99
|
+
apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
|
|
100
|
+
model: config?.model || 'gpt-4-vision',
|
|
101
|
+
...config,
|
|
102
|
+
});
|
|
62
103
|
}
|
|
63
104
|
|
|
64
|
-
function createBehavior(config
|
|
105
|
+
function createBehavior(config?: AppBehaviorConfig) {
|
|
65
106
|
return isDisabled(config) ? undefined : new ActiveSelfCompiler();
|
|
66
107
|
}
|
|
67
108
|
|
|
68
|
-
function createBody(config
|
|
109
|
+
function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
|
|
69
110
|
return isDisabled(config)
|
|
70
111
|
? undefined
|
|
71
112
|
: new Live2DAdapter(config);
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
function createHands(config
|
|
115
|
+
function createHands(config?: DefaultHandsOrganConfig & { provider?: string }) {
|
|
75
116
|
return isDisabled(config)
|
|
76
117
|
? new DefaultHandsOrgan()
|
|
77
118
|
: new DefaultHandsOrgan(config);
|
|
78
119
|
}
|
|
79
120
|
|
|
80
|
-
function createEar(config
|
|
121
|
+
function createEar(config?: EarOrganConfig & { provider?: string }) {
|
|
81
122
|
return isDisabled(config)
|
|
82
123
|
? new DefaultEarOrgan()
|
|
83
124
|
: new DefaultEarOrgan(config);
|
|
84
125
|
}
|
|
85
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
|
+
|
|
86
133
|
app.post('/boot', requireRole(['OWNER']), async (req, res) => {
|
|
87
134
|
try {
|
|
88
135
|
const { id, config } = req.body;
|
|
@@ -99,8 +146,21 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
99
146
|
const body = createBody(config.body);
|
|
100
147
|
const hands = createHands(config.hands);
|
|
101
148
|
const ear = createEar(config.ear);
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
});
|
|
104
164
|
await runtime.initialize();
|
|
105
165
|
|
|
106
166
|
runtimes.set(id, runtime);
|
|
@@ -113,11 +173,34 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
113
173
|
|
|
114
174
|
// STATUS / HEALTH ENDPOINTS
|
|
115
175
|
app.get('/health', (req, res) => res.json({ status: "ok" }));
|
|
116
|
-
app.get('/version', (req, res) => res.json({ name: "siduri-
|
|
117
|
-
app.get('/ready', (req, res) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
app.get('/version', (req, res) => res.json({ name: "siduri-x-api", version: "0.2.0-x" }));
|
|
177
|
+
app.get('/ready', (req, res) => {
|
|
178
|
+
const isReady = runtimes.size > 0;
|
|
179
|
+
res.status(isReady ? 200 : 503).json({
|
|
180
|
+
status: isReady ? "ready" : "not_ready",
|
|
181
|
+
companionCount: runtimes.size,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
app.get('/voice/health', (req, res) => {
|
|
185
|
+
const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
|
|
186
|
+
res.json({ provider: "siduri-voice", configured: hasVoice });
|
|
187
|
+
});
|
|
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
|
+
});
|
|
121
204
|
app.get('/me', attachIdentity, (req, res) => {
|
|
122
205
|
const identity = (req as any).identity as Identity;
|
|
123
206
|
res.json({
|
|
@@ -133,12 +216,20 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
133
216
|
const { id, message, history } = req.body;
|
|
134
217
|
const identity = (req as any).identity as Identity;
|
|
135
218
|
|
|
219
|
+
// Single-owner companion model: /chat defaults to OWNER identity.
|
|
220
|
+
// Explicit non-owner role in request body or context (e.g. adversarial test suites) is respected.
|
|
221
|
+
const effectiveRole = req.body?.role === 'VIEWER' || req.body?.context?.actor?.authorizationRole === 'viewer'
|
|
222
|
+
? 'VIEWER'
|
|
223
|
+
: (identity?.role === 'OPERATOR' ? 'OPERATOR' : 'OWNER');
|
|
224
|
+
const isOwner = effectiveRole === 'OWNER';
|
|
225
|
+
|
|
136
226
|
// Call context mapper at the API boundary
|
|
137
227
|
const mappingResult = mapRequestContext(
|
|
138
228
|
{
|
|
139
229
|
...req.body,
|
|
140
230
|
id: id || req.body.companionId,
|
|
141
|
-
role:
|
|
231
|
+
role: effectiveRole,
|
|
232
|
+
authenticated: isOwner,
|
|
142
233
|
generateCorrelationId: true,
|
|
143
234
|
},
|
|
144
235
|
{
|
|
@@ -159,21 +250,161 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
159
250
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
160
251
|
|
|
161
252
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const response = await runtime.handleUserMessage(message, legacyScope, history);
|
|
253
|
+
const response = await dispatchCompanionChat(runtime, {
|
|
254
|
+
id: companionId,
|
|
255
|
+
companionId,
|
|
256
|
+
message,
|
|
257
|
+
context: mappingResult.context,
|
|
258
|
+
history,
|
|
259
|
+
...(req.body?.medium ? { medium: req.body.medium } : {}),
|
|
260
|
+
});
|
|
171
261
|
res.json(response);
|
|
172
262
|
} catch (e: any) {
|
|
173
263
|
res.status(500).json({ error: e.message });
|
|
174
264
|
}
|
|
175
265
|
});
|
|
176
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
|
+
|
|
177
408
|
// MEMORY GETTERS
|
|
178
409
|
app.get('/memory/proposals', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
|
|
179
410
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
@@ -181,7 +412,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
181
412
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
182
413
|
if (!runtime.memory) return res.json({ proposals: [] });
|
|
183
414
|
try {
|
|
184
|
-
const proposals = await runtime.
|
|
415
|
+
const proposals = await runtime.getPendingClaims();
|
|
185
416
|
res.json({ proposals });
|
|
186
417
|
} catch (e: any) {
|
|
187
418
|
res.status(500).json({ error: e.message });
|
|
@@ -194,7 +425,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
194
425
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
195
426
|
if (!runtime.memory) return res.json({ items: [] });
|
|
196
427
|
try {
|
|
197
|
-
const items = await runtime.
|
|
428
|
+
const items = await runtime.getClaims();
|
|
198
429
|
res.json({ items });
|
|
199
430
|
} catch (e: any) {
|
|
200
431
|
res.status(500).json({ error: e.message });
|
|
@@ -207,7 +438,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
207
438
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
208
439
|
if (!runtime.memory) return res.json({ claims: [] });
|
|
209
440
|
try {
|
|
210
|
-
const claims = await runtime.
|
|
441
|
+
const claims = await runtime.getClaims();
|
|
211
442
|
res.json({ claims });
|
|
212
443
|
} catch (e: any) {
|
|
213
444
|
res.status(500).json({ error: e.message });
|
|
@@ -220,7 +451,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
220
451
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
221
452
|
if (!runtime.memory) return res.json({ directives: [] });
|
|
222
453
|
try {
|
|
223
|
-
const directives = await runtime.
|
|
454
|
+
const directives = await runtime.getDirectives();
|
|
224
455
|
res.json({ directives });
|
|
225
456
|
} catch (e: any) {
|
|
226
457
|
res.status(500).json({ error: e.message });
|
|
@@ -228,7 +459,24 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
228
459
|
});
|
|
229
460
|
|
|
230
461
|
// MEMORY MUTATIONS - PROPOSALS
|
|
231
|
-
app.post('/memory/proposals/update', requireRole(['OWNER', 'OPERATOR']), async (req, res) =>
|
|
462
|
+
app.post('/memory/proposals/update', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
|
|
463
|
+
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
464
|
+
const runtime = runtimes.get(id);
|
|
465
|
+
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
466
|
+
if (!runtime.memory || typeof runtime.memory.updateClaim !== 'function') {
|
|
467
|
+
return res.status(400).json({ error: "Memory organ does not support updating claims" });
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const claimId = req.body.id || req.body.claimId;
|
|
471
|
+
if (!claimId) {
|
|
472
|
+
return res.status(400).json({ error: "Missing required claim id" });
|
|
473
|
+
}
|
|
474
|
+
const updated = await runtime.updateClaim(claimId, req.body.updates || req.body);
|
|
475
|
+
res.json({ success: true, claim: updated });
|
|
476
|
+
} catch (e: any) {
|
|
477
|
+
res.status(500).json({ error: e.message });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
232
480
|
|
|
233
481
|
app.post('/memory/proposals/approve', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
|
|
234
482
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
@@ -236,7 +484,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
236
484
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
237
485
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
238
486
|
try {
|
|
239
|
-
await runtime.
|
|
487
|
+
await runtime.approveClaim(req.body.id);
|
|
240
488
|
res.json({ approved: true });
|
|
241
489
|
} catch (e: any) {
|
|
242
490
|
res.status(500).json({ error: e.message });
|
|
@@ -249,7 +497,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
249
497
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
250
498
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
251
499
|
try {
|
|
252
|
-
await runtime.
|
|
500
|
+
await runtime.rejectClaim(req.body.id);
|
|
253
501
|
res.json({ rejected: true });
|
|
254
502
|
} catch (e: any) {
|
|
255
503
|
res.status(500).json({ error: e.message });
|
|
@@ -263,7 +511,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
263
511
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
264
512
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
265
513
|
try {
|
|
266
|
-
await runtime.
|
|
514
|
+
await runtime.approveDirective(req.body.id);
|
|
267
515
|
res.json({ approved: true });
|
|
268
516
|
} catch (e: any) {
|
|
269
517
|
res.status(500).json({ error: e.message });
|
|
@@ -276,7 +524,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
276
524
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
277
525
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
278
526
|
try {
|
|
279
|
-
await runtime.
|
|
527
|
+
await runtime.rejectDirective(req.body.id);
|
|
280
528
|
res.json({ rejected: true });
|
|
281
529
|
} catch (e: any) {
|
|
282
530
|
res.status(500).json({ error: e.message });
|
|
@@ -289,7 +537,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
289
537
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
290
538
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
291
539
|
try {
|
|
292
|
-
await runtime.
|
|
540
|
+
await runtime.revokeDirective(req.body.id);
|
|
293
541
|
res.json({ revoked: true });
|
|
294
542
|
} catch (e: any) {
|
|
295
543
|
res.status(500).json({ error: e.message });
|
|
@@ -302,158 +550,174 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
302
550
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
303
551
|
if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
|
|
304
552
|
try {
|
|
305
|
-
await runtime.
|
|
553
|
+
await runtime.disableDirective(req.body.id);
|
|
306
554
|
res.json({ disabled: true });
|
|
307
555
|
} catch (e: any) {
|
|
308
556
|
res.status(500).json({ error: e.message });
|
|
309
557
|
}
|
|
310
558
|
});
|
|
311
559
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
560
|
+
const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
|
|
561
|
+
if (isDevMode) {
|
|
562
|
+
app.post('/dev/memory/reset', requireRole(['OWNER']), async (req, res) => {
|
|
563
|
+
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
564
|
+
const runtime = runtimes.get(id);
|
|
565
|
+
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
566
|
+
if (!runtime.memory || typeof runtime.memory.resetMemory !== 'function') {
|
|
567
|
+
return res.status(400).json({ error: "Memory organ does not support reset" });
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
await runtime.resetMemory();
|
|
571
|
+
res.json({ reset: true });
|
|
572
|
+
} catch (e: any) {
|
|
573
|
+
res.status(500).json({ error: e.message });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
319
576
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
577
|
+
app.post('/dev/mock-response', async (req, res) => {
|
|
578
|
+
const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
|
|
579
|
+
const runtime = runtimes.get(companionId);
|
|
580
|
+
if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
|
|
581
|
+
const staged = runtime.stageResponse({
|
|
582
|
+
requestContext: {
|
|
583
|
+
companionId,
|
|
584
|
+
actor: {
|
|
585
|
+
actorId: 'operator-a',
|
|
586
|
+
sessionId: 'sess-op',
|
|
587
|
+
authorizationRole: 'operator',
|
|
588
|
+
capabilities: ['chat:public', 'memory:approve'],
|
|
589
|
+
authenticated: true,
|
|
590
|
+
},
|
|
591
|
+
conversation: {
|
|
592
|
+
channel: 'public',
|
|
593
|
+
audienceId: 'audience-public',
|
|
594
|
+
correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
|
|
595
|
+
},
|
|
338
596
|
},
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
597
|
+
candidateSpeech: req.body?.speech || 'Mocked staged response for review',
|
|
598
|
+
candidateLanguage: req.body?.language || 'en',
|
|
599
|
+
requiresApproval: req.body?.requiresApproval ?? true,
|
|
600
|
+
});
|
|
601
|
+
res.json({
|
|
602
|
+
accepted: true,
|
|
603
|
+
staged: true,
|
|
604
|
+
status: staged.status,
|
|
605
|
+
response_id: staged.responseId,
|
|
606
|
+
correlation_id: staged.correlationId,
|
|
607
|
+
speech: staged.speech,
|
|
608
|
+
});
|
|
351
609
|
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
app.post('/dev/approve-response', async (req, res) => {
|
|
355
|
-
const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
|
|
356
|
-
const runtime = runtimes.get(companionId);
|
|
357
|
-
if (!runtime) return res.status(404).json({ approved: false, error: 'Companion not found' });
|
|
358
610
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
611
|
+
app.post('/dev/approve-response', async (req, res) => {
|
|
612
|
+
const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
|
|
613
|
+
const runtime = runtimes.get(companionId);
|
|
614
|
+
if (!runtime) return res.status(404).json({ approved: false, error: 'Companion not found' });
|
|
615
|
+
|
|
616
|
+
let responseId = req.body?.responseId;
|
|
617
|
+
let correlationId = req.body?.correlation_id;
|
|
618
|
+
|
|
619
|
+
if (!responseId && correlationId) {
|
|
620
|
+
const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
|
|
621
|
+
if (found) {
|
|
622
|
+
responseId = found.responseId;
|
|
623
|
+
}
|
|
624
|
+
} else if (responseId && !correlationId) {
|
|
625
|
+
const found = runtime.getStagedPlan(responseId);
|
|
626
|
+
if (found) {
|
|
627
|
+
correlationId = found.correlationId;
|
|
628
|
+
}
|
|
371
629
|
}
|
|
372
|
-
}
|
|
373
630
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
631
|
+
if (!responseId) {
|
|
632
|
+
return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
|
|
633
|
+
}
|
|
377
634
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
635
|
+
const result = runtime.approveResponse({
|
|
636
|
+
responseId,
|
|
637
|
+
companionId,
|
|
638
|
+
correlationId: correlationId || '',
|
|
639
|
+
audienceId: req.body?.audienceId,
|
|
640
|
+
});
|
|
384
641
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
642
|
+
if (!result.success) {
|
|
643
|
+
return res.status(400).json({ approved: false, error: result.reason });
|
|
644
|
+
}
|
|
388
645
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
646
|
+
// Now evaluated as approved
|
|
647
|
+
const evaluation = runtime.evaluateGate(result.plan!);
|
|
648
|
+
res.json({
|
|
649
|
+
approved: true,
|
|
650
|
+
status: evaluation.disposition,
|
|
651
|
+
response_id: result.plan!.responseId,
|
|
652
|
+
speech: result.plan!.speech,
|
|
653
|
+
language: result.plan!.language,
|
|
654
|
+
});
|
|
397
655
|
});
|
|
398
|
-
});
|
|
399
656
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
657
|
+
app.post('/dev/reject-response', async (req, res) => {
|
|
658
|
+
const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
|
|
659
|
+
const runtime = runtimes.get(companionId);
|
|
660
|
+
if (!runtime) return res.status(404).json({ rejected: false, error: 'Companion not found' });
|
|
661
|
+
|
|
662
|
+
let responseId = req.body?.responseId;
|
|
663
|
+
let correlationId = req.body?.correlation_id;
|
|
664
|
+
|
|
665
|
+
if (!responseId && correlationId) {
|
|
666
|
+
const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
|
|
667
|
+
if (found) {
|
|
668
|
+
responseId = found.responseId;
|
|
669
|
+
}
|
|
670
|
+
} else if (responseId && !correlationId) {
|
|
671
|
+
const found = runtime.getStagedPlan(responseId);
|
|
672
|
+
if (found) {
|
|
673
|
+
correlationId = found.correlationId;
|
|
674
|
+
}
|
|
417
675
|
}
|
|
418
|
-
}
|
|
419
676
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
677
|
+
if (!responseId) {
|
|
678
|
+
return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
|
|
679
|
+
}
|
|
423
680
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
681
|
+
const result = runtime.rejectResponse({
|
|
682
|
+
responseId,
|
|
683
|
+
companionId,
|
|
684
|
+
correlationId: correlationId || '',
|
|
685
|
+
reason: req.body?.reason,
|
|
686
|
+
});
|
|
430
687
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
688
|
+
if (!result.success) {
|
|
689
|
+
return res.status(400).json({ rejected: false, error: result.reason });
|
|
690
|
+
}
|
|
434
691
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
692
|
+
res.json({
|
|
693
|
+
rejected: true,
|
|
694
|
+
status: result.plan!.status,
|
|
695
|
+
response_id: result.plan!.responseId,
|
|
696
|
+
});
|
|
439
697
|
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
app.post('/dev/mock-observation', async (req, res) => {
|
|
443
|
-
if (!observationOrgan) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
|
|
444
|
-
const result = await observationOrgan.ingest(
|
|
445
|
-
new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
|
|
446
|
-
'fixture-observation',
|
|
447
|
-
'configured-vision',
|
|
448
|
-
);
|
|
449
|
-
if (!result.observation) return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
|
|
450
|
-
res.status(202).json({ accepted: true, observation: result.observation });
|
|
451
|
-
});
|
|
452
698
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
699
|
+
app.post('/dev/mock-observation', async (req, res) => {
|
|
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(
|
|
704
|
+
new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
|
|
705
|
+
'fixture-observation',
|
|
706
|
+
'configured-vision',
|
|
707
|
+
);
|
|
708
|
+
if (!result.observation) return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
|
|
709
|
+
res.status(202).json({ accepted: true, observation: result.observation });
|
|
710
|
+
});
|
|
711
|
+
}
|
|
457
712
|
|
|
458
|
-
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
|
+
};
|
|
459
723
|
}
|