@siduri-x/api 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
 
2
- > @siduri-x/api@2.0.2 build /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
2
+ > @siduri-x/api@2.0.3 build /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
3
  > tsc
4
4
 
@@ -1,13 +1,9 @@
1
1
 
2
- > @siduri-x/api@2.0.2 test /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
2
+ > @siduri-x/api@2.0.3 test /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
3
  > jest --config jest.config.json
4
4
 
5
- PASS src/boot.test.ts (12.448 s)
6
- PASS src/index.test.ts (12.646 s)
7
- PASS src/t7-release.test.ts (12.705 s)
8
- PASS src/knowledge.test.ts (12.82 s)
9
- PASS src/context-mapper.test.ts
10
- PASS src/runtime.test.ts
5
+ PASS src/context-mapper.test.ts (17.122 s)
6
+ PASS src/runtime.test.ts (17.986 s)
11
7
  ● Console
12
8
 
13
9
  console.error
@@ -28,16 +24,20 @@ PASS src/runtime.test.ts
28
24
  at PerceptionPipeline.execute (../../packages/core/dist/perception-pipeline.js:21:34)
29
25
  at Object.<anonymous> (src/runtime.test.ts:74:22)
30
26
 
31
- PASS src/auth.test.ts
27
+ PASS src/t5-experience.test.ts (19.901 s)
28
+ PASS src/t4-gating.test.ts (19.152 s)
29
+ PASS src/index.test.ts (19.326 s)
30
+ PASS src/teach-mode.test.ts (20.058 s)
31
+ PASS src/t7-release.test.ts
32
32
  PASS src/smoke.test.ts
33
- PASS src/teach-mode.test.ts
34
- PASS src/t5-experience.test.ts
35
- PASS src/t4-gating.test.ts (13.84 s)
36
- PASS src/t6-security.test.ts (14.624 s)
37
- PASS src/b0-b6.test.ts (13.796 s)
33
+ PASS src/auth.test.ts
34
+ PASS src/t6-security.test.ts (19.898 s)
35
+ PASS src/boot.test.ts
36
+ PASS src/b0-b6.test.ts
37
+ PASS src/knowledge.test.ts
38
38
  
39
39
  Test Suites: 13 passed, 13 total
40
- Tests: 86 passed, 86 total
40
+ Tests: 90 passed, 90 total
41
41
  Snapshots: 0 total
42
- Time: 17.47 s
42
+ Time: 23.887 s
43
43
  Ran all test suites.
package/dist/app.js CHANGED
@@ -114,12 +114,31 @@ function createApp(runtimes = new Map()) {
114
114
  companionId,
115
115
  name: manifest.identity.name,
116
116
  archetype: manifest.identity.archetype,
117
+ origin: manifest.identity.origin,
118
+ ethos: manifest.identity.ethos,
117
119
  version: manifest.version || '1.0.0',
118
120
  updatedAt: new Date().toISOString(),
119
121
  });
120
122
  if (manifest.personality) {
121
123
  await repo.setPersonality(companionId, manifest.personality);
122
124
  }
125
+ if (Array.isArray(manifest.relationships)) {
126
+ for (const rel of manifest.relationships) {
127
+ if (rel && rel.entityId) {
128
+ await repo.updateRelationship(companionId, {
129
+ companionId,
130
+ entityId: rel.entityId,
131
+ entityType: 'human',
132
+ role: rel.role || 'user',
133
+ stance: rel.stance || 'neutral',
134
+ interactionConventions: rel.conventions || [],
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
140
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
141
+ }
123
142
  if (directivesToCommit.length > 0) {
124
143
  await repo.commitDirectives(companionId, directivesToCommit);
125
144
  }
@@ -225,7 +244,7 @@ function createApp(runtimes = new Map()) {
225
244
  medium: 'web',
226
245
  signal: abortController.signal,
227
246
  });
228
- res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
247
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
229
248
  const avatarEvent = response.metadata?.events?.find((e) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval));
230
249
  if (avatarEvent) {
231
250
  res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
@@ -115,6 +115,7 @@ function mapRequestContext(input, options = {}) {
115
115
  },
116
116
  source: input.source || rawCtx.source || (isAuthenticated ? 'local' : 'external'),
117
117
  subject: rawCtx.subject,
118
+ mode: rawCtx.mode || input.mode,
118
119
  };
119
120
  const validated = (0, core_1.validateRequestContext)(constructed);
120
121
  if (!validated.accepted) {
@@ -218,6 +219,7 @@ function mapRequestContext(input, options = {}) {
218
219
  },
219
220
  source: input.source || (isAuthenticated ? 'local' : 'external'),
220
221
  subject: input.subject,
222
+ mode: input.mode || input.conversation?.mode,
221
223
  };
222
224
  const validated = (0, core_1.validateRequestContext)(mappedContext);
223
225
  if (!validated.accepted) {
@@ -249,4 +249,39 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
249
249
  expect(attenuatedResult.context?.actor.authorizationRole).toBe('viewer');
250
250
  expect(attenuatedResult.diagnostics).not.toContain('role_escalation_attempt_suppressed');
251
251
  });
252
+ describe('Mode Mapping (Casual, Teach, Hybrid)', () => {
253
+ test('maps explicit mode parameter to RequestContext', () => {
254
+ const casualResult = (0, context_mapper_1.mapRequestContext)({
255
+ companionId: 'comp-1',
256
+ mode: 'casual',
257
+ generateCorrelationId: true,
258
+ });
259
+ expect(casualResult.accepted).toBe(true);
260
+ expect(casualResult.context?.mode).toBe('casual');
261
+ const teachResult = (0, context_mapper_1.mapRequestContext)({
262
+ companionId: 'comp-1',
263
+ mode: 'teach',
264
+ generateCorrelationId: true,
265
+ });
266
+ expect(teachResult.accepted).toBe(true);
267
+ expect(teachResult.context?.mode).toBe('teach');
268
+ const hybridResult = (0, context_mapper_1.mapRequestContext)({
269
+ companionId: 'comp-1',
270
+ mode: 'hybrid',
271
+ generateCorrelationId: true,
272
+ });
273
+ expect(hybridResult.accepted).toBe(true);
274
+ expect(hybridResult.context?.mode).toBe('hybrid');
275
+ });
276
+ test('rejects invalid interaction mode', () => {
277
+ const invalidResult = (0, context_mapper_1.mapRequestContext)({
278
+ companionId: 'comp-1',
279
+ mode: 'super_turbo',
280
+ generateCorrelationId: true,
281
+ });
282
+ expect(invalidResult.accepted).toBe(false);
283
+ expect(invalidResult.error?.code).toBe('INVALID_CONTEXT');
284
+ expect(invalidResult.error?.field).toBe('mode');
285
+ });
286
+ });
252
287
  });
@@ -134,4 +134,18 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
134
134
  expect(res.body.interrupted).toBe(true);
135
135
  expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_barge_in');
136
136
  });
137
+ test('POST /chat passes explicit interaction mode (casual, teach, hybrid) to runtime', async () => {
138
+ const res = await (0, supertest_1.default)(app)
139
+ .post('/chat')
140
+ .send({
141
+ id: 'companion-a',
142
+ message: 'Casual banter',
143
+ mode: 'casual',
144
+ });
145
+ expect(res.status).toBe(200);
146
+ expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Casual banter', expect.objectContaining({
147
+ companionId: 'companion-a',
148
+ mode: 'casual',
149
+ }), []);
150
+ });
137
151
  });
@@ -14,6 +14,8 @@ jest.mock('@siduri-x/self', () => {
14
14
  SqliteSelfRepository: jest.fn().mockImplementation(() => ({
15
15
  setIdentity: jest.fn().mockResolvedValue(undefined),
16
16
  setPersonality: jest.fn().mockResolvedValue(undefined),
17
+ updateRelationship: jest.fn().mockResolvedValue(undefined),
18
+ setExemplars: jest.fn().mockResolvedValue(undefined),
17
19
  commitDirectives: jest.fn().mockResolvedValue(undefined),
18
20
  close: jest.fn(),
19
21
  })),
@@ -133,4 +135,69 @@ kind: "other"
133
135
  const MockRepo = self_1.SqliteSelfRepository;
134
136
  expect(MockRepo.mock.instances.length).toBe(0);
135
137
  });
138
+ it('POST /teach/install-self installs v2.0 manifest with ethos, relationships, and exemplars', async () => {
139
+ const v2Manifest = {
140
+ specVersion: '2.0.0',
141
+ identity: {
142
+ name: 'Siduri',
143
+ archetype: 'System Sentinel',
144
+ origin: 'Ancient mythos',
145
+ ethos: 'Protector of the realm and loyal companion to creator',
146
+ },
147
+ version: '2.0.0',
148
+ relationships: [
149
+ {
150
+ entityId: 'actor:zagin',
151
+ role: 'creator',
152
+ stance: 'familiar_loyal',
153
+ conventions: ['Direct communication', 'Highest administrative trust'],
154
+ },
155
+ ],
156
+ dialogueExamples: [
157
+ {
158
+ user: 'Deploy current branch',
159
+ assistant: 'Deploying to staging now, boss.',
160
+ },
161
+ ],
162
+ directives: [
163
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
164
+ ],
165
+ };
166
+ const res = await (0, supertest_1.default)(app)
167
+ .post('/teach/install-self')
168
+ .set(mockAuthHeader)
169
+ .send({
170
+ companionId: 'comp-v2',
171
+ manifest: v2Manifest,
172
+ approvedDirectiveIds: ['dir-rel-1'],
173
+ });
174
+ expect(res.status).toBe(200);
175
+ expect(res.body.success).toBe(true);
176
+ const MockRepo = self_1.SqliteSelfRepository;
177
+ const repoInstance = MockRepo.mock.results[MockRepo.mock.results.length - 1].value;
178
+ expect(repoInstance.setIdentity).toHaveBeenCalledWith(expect.objectContaining({
179
+ companionId: 'comp-v2',
180
+ name: 'Siduri',
181
+ archetype: 'System Sentinel',
182
+ origin: 'Ancient mythos',
183
+ ethos: 'Protector of the realm and loyal companion to creator',
184
+ }));
185
+ expect(repoInstance.updateRelationship).toHaveBeenCalledWith('comp-v2', {
186
+ companionId: 'comp-v2',
187
+ entityId: 'actor:zagin',
188
+ entityType: 'human',
189
+ role: 'creator',
190
+ stance: 'familiar_loyal',
191
+ interactionConventions: ['Direct communication', 'Highest administrative trust'],
192
+ });
193
+ expect(repoInstance.setExemplars).toHaveBeenCalledWith('comp-v2', [
194
+ {
195
+ user: 'Deploy current branch',
196
+ assistant: 'Deploying to staging now, boss.',
197
+ },
198
+ ]);
199
+ expect(repoInstance.commitDirectives).toHaveBeenCalledWith('comp-v2', [
200
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
201
+ ]);
202
+ });
136
203
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/api",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -13,8 +13,8 @@
13
13
  "dotenv": "^17.4.2",
14
14
  "express": "^5.2.1",
15
15
  "@siduri-x/body": "2.0.1",
16
+ "@siduri-x/core": "2.0.3",
16
17
  "@siduri-x/brain": "2.0.1",
17
- "@siduri-x/core": "2.0.2",
18
18
  "@siduri-x/ear": "2.0.1",
19
19
  "@siduri-x/eknowledge": "2.0.1",
20
20
  "@siduri-x/hands": "2.0.1",
@@ -22,7 +22,7 @@
22
22
  "@siduri-x/memory": "2.0.1",
23
23
  "@siduri-x/mouth": "2.0.1",
24
24
  "@siduri-x/observation": "2.0.1",
25
- "@siduri-x/self": "2.0.1",
25
+ "@siduri-x/self": "2.0.2",
26
26
  "@siduri-x/vision": "2.0.1",
27
27
  "@siduri-x/voice": "2.0.1"
28
28
  },
package/siduri.sqlite ADDED
Binary file
package/src/app.ts CHANGED
@@ -136,6 +136,8 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
136
136
  companionId,
137
137
  name: manifest.identity.name,
138
138
  archetype: manifest.identity.archetype,
139
+ origin: manifest.identity.origin,
140
+ ethos: manifest.identity.ethos,
139
141
  version: manifest.version || '1.0.0',
140
142
  updatedAt: new Date().toISOString(),
141
143
  });
@@ -144,6 +146,25 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
144
146
  await repo.setPersonality(companionId, manifest.personality);
145
147
  }
146
148
 
149
+ if (Array.isArray(manifest.relationships)) {
150
+ for (const rel of manifest.relationships) {
151
+ if (rel && rel.entityId) {
152
+ await repo.updateRelationship(companionId, {
153
+ companionId,
154
+ entityId: rel.entityId,
155
+ entityType: 'human',
156
+ role: rel.role || 'user',
157
+ stance: rel.stance || 'neutral',
158
+ interactionConventions: rel.conventions || [],
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
165
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
166
+ }
167
+
147
168
  if (directivesToCommit.length > 0) {
148
169
  await repo.commitDirectives(companionId, directivesToCommit);
149
170
  }
@@ -260,7 +281,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
260
281
  signal: abortController.signal,
261
282
  });
262
283
 
263
- res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
284
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
264
285
 
265
286
  const avatarEvent = response.metadata?.events?.find(
266
287
  (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval)
@@ -277,4 +277,43 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
277
277
  expect(attenuatedResult.context?.actor.authorizationRole).toBe('viewer');
278
278
  expect(attenuatedResult.diagnostics).not.toContain('role_escalation_attempt_suppressed');
279
279
  });
280
+
281
+ describe('Mode Mapping (Casual, Teach, Hybrid)', () => {
282
+ test('maps explicit mode parameter to RequestContext', () => {
283
+ const casualResult = mapRequestContext({
284
+ companionId: 'comp-1',
285
+ mode: 'casual',
286
+ generateCorrelationId: true,
287
+ });
288
+ expect(casualResult.accepted).toBe(true);
289
+ expect(casualResult.context?.mode).toBe('casual');
290
+
291
+ const teachResult = mapRequestContext({
292
+ companionId: 'comp-1',
293
+ mode: 'teach',
294
+ generateCorrelationId: true,
295
+ });
296
+ expect(teachResult.accepted).toBe(true);
297
+ expect(teachResult.context?.mode).toBe('teach');
298
+
299
+ const hybridResult = mapRequestContext({
300
+ companionId: 'comp-1',
301
+ mode: 'hybrid',
302
+ generateCorrelationId: true,
303
+ });
304
+ expect(hybridResult.accepted).toBe(true);
305
+ expect(hybridResult.context?.mode).toBe('hybrid');
306
+ });
307
+
308
+ test('rejects invalid interaction mode', () => {
309
+ const invalidResult = mapRequestContext({
310
+ companionId: 'comp-1',
311
+ mode: 'super_turbo',
312
+ generateCorrelationId: true,
313
+ });
314
+ expect(invalidResult.accepted).toBe(false);
315
+ expect(invalidResult.error?.code).toBe('INVALID_CONTEXT');
316
+ expect(invalidResult.error?.field).toBe('mode');
317
+ });
318
+ });
280
319
  });
@@ -145,6 +145,7 @@ export function mapRequestContext(
145
145
  },
146
146
  source: input.source || rawCtx.source || (isAuthenticated ? 'local' : 'external'),
147
147
  subject: rawCtx.subject,
148
+ mode: rawCtx.mode || input.mode,
148
149
  };
149
150
 
150
151
  const validated = validateRequestContext(constructed);
@@ -262,6 +263,7 @@ export function mapRequestContext(
262
263
  },
263
264
  source: input.source || (isAuthenticated ? 'local' : 'external'),
264
265
  subject: input.subject,
266
+ mode: input.mode || input.conversation?.mode,
265
267
  };
266
268
 
267
269
  const validated = validateRequestContext(mappedContext);
package/src/index.test.ts CHANGED
@@ -155,4 +155,24 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
155
155
  expect(res.body.interrupted).toBe(true);
156
156
  expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_barge_in');
157
157
  });
158
+
159
+ test('POST /chat passes explicit interaction mode (casual, teach, hybrid) to runtime', async () => {
160
+ const res = await request(app)
161
+ .post('/chat')
162
+ .send({
163
+ id: 'companion-a',
164
+ message: 'Casual banter',
165
+ mode: 'casual',
166
+ });
167
+
168
+ expect(res.status).toBe(200);
169
+ expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
170
+ 'Casual banter',
171
+ expect.objectContaining({
172
+ companionId: 'companion-a',
173
+ mode: 'casual',
174
+ }),
175
+ []
176
+ );
177
+ });
158
178
  });
@@ -10,6 +10,8 @@ jest.mock('@siduri-x/self', () => {
10
10
  SqliteSelfRepository: jest.fn().mockImplementation(() => ({
11
11
  setIdentity: jest.fn().mockResolvedValue(undefined),
12
12
  setPersonality: jest.fn().mockResolvedValue(undefined),
13
+ updateRelationship: jest.fn().mockResolvedValue(undefined),
14
+ setExemplars: jest.fn().mockResolvedValue(undefined),
13
15
  commitDirectives: jest.fn().mockResolvedValue(undefined),
14
16
  close: jest.fn(),
15
17
  })),
@@ -149,4 +151,79 @@ kind: "other"
149
151
  const MockRepo = SqliteSelfRepository as jest.MockedClass<typeof SqliteSelfRepository>;
150
152
  expect(MockRepo.mock.instances.length).toBe(0);
151
153
  });
154
+
155
+ it('POST /teach/install-self installs v2.0 manifest with ethos, relationships, and exemplars', async () => {
156
+ const v2Manifest = {
157
+ specVersion: '2.0.0',
158
+ identity: {
159
+ name: 'Siduri',
160
+ archetype: 'System Sentinel',
161
+ origin: 'Ancient mythos',
162
+ ethos: 'Protector of the realm and loyal companion to creator',
163
+ },
164
+ version: '2.0.0',
165
+ relationships: [
166
+ {
167
+ entityId: 'actor:zagin',
168
+ role: 'creator',
169
+ stance: 'familiar_loyal',
170
+ conventions: ['Direct communication', 'Highest administrative trust'],
171
+ },
172
+ ],
173
+ dialogueExamples: [
174
+ {
175
+ user: 'Deploy current branch',
176
+ assistant: 'Deploying to staging now, boss.',
177
+ },
178
+ ],
179
+ directives: [
180
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
181
+ ],
182
+ };
183
+
184
+ const res = await request(app)
185
+ .post('/teach/install-self')
186
+ .set(mockAuthHeader)
187
+ .send({
188
+ companionId: 'comp-v2',
189
+ manifest: v2Manifest,
190
+ approvedDirectiveIds: ['dir-rel-1'],
191
+ });
192
+
193
+ expect(res.status).toBe(200);
194
+ expect(res.body.success).toBe(true);
195
+
196
+ const MockRepo = SqliteSelfRepository as jest.MockedClass<typeof SqliteSelfRepository>;
197
+ const repoInstance = MockRepo.mock.results[MockRepo.mock.results.length - 1].value;
198
+
199
+ expect(repoInstance.setIdentity).toHaveBeenCalledWith(
200
+ expect.objectContaining({
201
+ companionId: 'comp-v2',
202
+ name: 'Siduri',
203
+ archetype: 'System Sentinel',
204
+ origin: 'Ancient mythos',
205
+ ethos: 'Protector of the realm and loyal companion to creator',
206
+ })
207
+ );
208
+
209
+ expect(repoInstance.updateRelationship).toHaveBeenCalledWith('comp-v2', {
210
+ companionId: 'comp-v2',
211
+ entityId: 'actor:zagin',
212
+ entityType: 'human',
213
+ role: 'creator',
214
+ stance: 'familiar_loyal',
215
+ interactionConventions: ['Direct communication', 'Highest administrative trust'],
216
+ });
217
+
218
+ expect(repoInstance.setExemplars).toHaveBeenCalledWith('comp-v2', [
219
+ {
220
+ user: 'Deploy current branch',
221
+ assistant: 'Deploying to staging now, boss.',
222
+ },
223
+ ]);
224
+
225
+ expect(repoInstance.commitDirectives).toHaveBeenCalledWith('comp-v2', [
226
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
227
+ ]);
228
+ });
152
229
  });