@jskit-ai/assistant 0.1.4

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.
Files changed (57) hide show
  1. package/package.descriptor.mjs +284 -0
  2. package/package.json +31 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
  5. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  6. package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
  7. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
  8. package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
  9. package/src/client/index.js +12 -0
  10. package/src/client/lib/assistantApi.js +137 -0
  11. package/src/client/lib/assistantHttpClient.js +10 -0
  12. package/src/client/lib/markdownRenderer.js +31 -0
  13. package/src/client/providers/AssistantWebClientProvider.js +25 -0
  14. package/src/server/AssistantServiceProvider.js +179 -0
  15. package/src/server/actionIds.js +11 -0
  16. package/src/server/actions.js +191 -0
  17. package/src/server/diTokens.js +19 -0
  18. package/src/server/lib/aiClient.js +43 -0
  19. package/src/server/lib/ndjson.js +47 -0
  20. package/src/server/lib/providers/anthropicClient.js +375 -0
  21. package/src/server/lib/providers/common.js +158 -0
  22. package/src/server/lib/providers/deepSeekClient.js +22 -0
  23. package/src/server/lib/providers/openAiClient.js +13 -0
  24. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  25. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  26. package/src/server/lib/serviceToolCatalog.js +459 -0
  27. package/src/server/registerRoutes.js +384 -0
  28. package/src/server/repositories/assistantSettingsRepository.js +100 -0
  29. package/src/server/repositories/conversationsRepository.js +244 -0
  30. package/src/server/repositories/messagesRepository.js +154 -0
  31. package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
  32. package/src/server/services/assistantSettingsService.js +153 -0
  33. package/src/server/services/chatService.js +987 -0
  34. package/src/server/services/transcriptService.js +334 -0
  35. package/src/shared/assistantPaths.js +50 -0
  36. package/src/shared/assistantResource.js +323 -0
  37. package/src/shared/assistantSettingsResource.js +214 -0
  38. package/src/shared/index.js +39 -0
  39. package/src/shared/queryKeys.js +69 -0
  40. package/src/shared/settingsEvents.js +7 -0
  41. package/src/shared/streamEvents.js +31 -0
  42. package/src/shared/support/positiveInteger.js +9 -0
  43. package/templates/migrations/assistant_settings_initial.cjs +39 -0
  44. package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
  45. package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
  46. package/test/aiConfigValidation.test.js +15 -0
  47. package/test/assistantApiSurfaceHeader.test.js +64 -0
  48. package/test/assistantResource.test.js +53 -0
  49. package/test/assistantSettingsResource.test.js +48 -0
  50. package/test/assistantSettingsService.test.js +133 -0
  51. package/test/chatService.test.js +841 -0
  52. package/test/descriptorSurfaceOption.test.js +35 -0
  53. package/test/queryKeys.test.js +41 -0
  54. package/test/resolveWorkspaceSlug.test.js +83 -0
  55. package/test/routeInputContracts.test.js +287 -0
  56. package/test/serviceToolCatalog.test.js +1235 -0
  57. package/test/transcriptService.test.js +175 -0
@@ -0,0 +1,1235 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Type } from "typebox";
4
+ import { createContainer } from "@jskit-ai/kernel/_testable";
5
+ import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
6
+ import { installServiceRegistrationApi } from "@jskit-ai/kernel/server/runtime";
7
+ import { createServiceToolCatalog } from "../src/server/lib/serviceToolCatalog.js";
8
+
9
+ function createApp() {
10
+ const app = createContainer();
11
+ app.singleton("domainEvents", () => ({
12
+ async publish() {
13
+ return null;
14
+ }
15
+ }));
16
+ installServiceRegistrationApi(app);
17
+ return app;
18
+ }
19
+
20
+ test("service tool catalog hides methods user cannot execute", () => {
21
+ const app = createApp();
22
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
23
+ actionRuntimeProvider.register(app);
24
+
25
+ app.service(
26
+ "demo.customers.service",
27
+ () => ({
28
+ listRecords() {
29
+ return [];
30
+ },
31
+ deleteRecord() {
32
+ return { ok: true };
33
+ }
34
+ })
35
+ );
36
+
37
+ app.actions([
38
+ {
39
+ id: "demo.customers.list",
40
+ domain: "demo",
41
+ version: 1,
42
+ kind: "query",
43
+ channels: ["automation"],
44
+ surfaces: ["admin"],
45
+ permission: {
46
+ require: "authenticated"
47
+ },
48
+ dependencies: {
49
+ customersService: "demo.customers.service"
50
+ },
51
+ inputValidator: {
52
+ schema: { type: "object", additionalProperties: true }
53
+ },
54
+ outputValidator: {
55
+ schema: {
56
+ type: "array",
57
+ items: {
58
+ type: "object",
59
+ additionalProperties: true
60
+ }
61
+ }
62
+ },
63
+ idempotency: "none",
64
+ audit: {
65
+ actionName: "demo.customers.list"
66
+ },
67
+ observability: {},
68
+ async execute(input, _context, deps) {
69
+ return deps.customersService.listRecords(input);
70
+ }
71
+ },
72
+ {
73
+ id: "demo.customers.delete",
74
+ domain: "demo",
75
+ version: 1,
76
+ kind: "command",
77
+ channels: ["automation"],
78
+ surfaces: ["admin"],
79
+ permission: {
80
+ require: "all",
81
+ permissions: ["customers.delete"]
82
+ },
83
+ dependencies: {
84
+ customersService: "demo.customers.service"
85
+ },
86
+ inputValidator: {
87
+ schema: { type: "object", additionalProperties: true }
88
+ },
89
+ outputValidator: {
90
+ schema: {
91
+ type: "object",
92
+ additionalProperties: true
93
+ }
94
+ },
95
+ idempotency: "optional",
96
+ audit: {
97
+ actionName: "demo.customers.delete"
98
+ },
99
+ observability: {},
100
+ async execute(input, _context, deps) {
101
+ return deps.customersService.deleteRecord(input);
102
+ }
103
+ }
104
+ ]);
105
+
106
+ const internalContext = {
107
+ channel: "internal",
108
+ surface: "admin"
109
+ };
110
+
111
+ const catalog = createServiceToolCatalog(app, {
112
+ skipActionPrefixes: []
113
+ });
114
+
115
+ const unauthenticatedTools = catalog.resolveToolSet({
116
+ ...internalContext,
117
+ permissions: []
118
+ }).tools;
119
+ assert.equal(unauthenticatedTools.length, 0);
120
+
121
+ const authenticatedTools = catalog.resolveToolSet({
122
+ ...internalContext,
123
+ actor: { id: 9 },
124
+ permissions: []
125
+ }).tools;
126
+ assert.equal(authenticatedTools.length, 1);
127
+ assert.equal(authenticatedTools[0].actionId, "demo.customers.list");
128
+
129
+ const privilegedTools = catalog.resolveToolSet({
130
+ ...internalContext,
131
+ actor: { id: 9 },
132
+ permissions: ["customers.delete"]
133
+ }).tools;
134
+ assert.equal(privilegedTools.length, 2);
135
+ });
136
+
137
+ test("service tool catalog does not expose non-action-backed service methods", async () => {
138
+ const app = createApp();
139
+
140
+ app.service(
141
+ "demo.profile.service",
142
+ () => ({
143
+ updateProfile(patch = {}, options = {}) {
144
+ return {
145
+ patch,
146
+ actorId: Number(options?.context?.actor?.id || 0),
147
+ source: String(options?.source || "")
148
+ };
149
+ }
150
+ })
151
+ );
152
+
153
+ const catalog = createServiceToolCatalog(app, {
154
+ skipActionPrefixes: []
155
+ });
156
+
157
+ const context = {
158
+ actor: {
159
+ id: 22
160
+ },
161
+ permissions: []
162
+ };
163
+ const toolSet = catalog.resolveToolSet(context);
164
+ assert.equal(toolSet.tools.length, 0);
165
+
166
+ const execution = await catalog.executeToolCall({
167
+ toolName: "demo_profile_service_updateprofile",
168
+ argumentsText: JSON.stringify({
169
+ args: [{ displayName: "Merc" }],
170
+ options: {
171
+ source: "assistant"
172
+ }
173
+ }),
174
+ context,
175
+ toolSet
176
+ });
177
+
178
+ assert.equal(execution.ok, false);
179
+ assert.deepEqual(execution.error, {
180
+ code: "assistant_tool_unknown",
181
+ message: "Unknown tool."
182
+ });
183
+ });
184
+
185
+ test("service tool catalog hides actions that are not automation-enabled", () => {
186
+ const app = createApp();
187
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
188
+ actionRuntimeProvider.register(app);
189
+
190
+ app.service(
191
+ "demo.non_automation.service",
192
+ () => ({
193
+ listRecords() {
194
+ return [];
195
+ }
196
+ })
197
+ );
198
+
199
+ app.actions([
200
+ {
201
+ id: "demo.non_automation.list",
202
+ domain: "demo",
203
+ version: 1,
204
+ kind: "query",
205
+ channels: ["internal"],
206
+ surfaces: ["admin"],
207
+ permission: {
208
+ require: "authenticated"
209
+ },
210
+ dependencies: {
211
+ nonAutomationService: "demo.non_automation.service"
212
+ },
213
+ inputValidator: {
214
+ schema: Type.Object({}, { additionalProperties: false })
215
+ },
216
+ outputValidator: {
217
+ schema: Type.Array(Type.Object({}, { additionalProperties: true }))
218
+ },
219
+ idempotency: "none",
220
+ audit: {
221
+ actionName: "demo.non_automation.list"
222
+ },
223
+ observability: {},
224
+ async execute(input, _context, deps) {
225
+ return deps.nonAutomationService.listRecords(input);
226
+ }
227
+ }
228
+ ]);
229
+
230
+ const catalog = createServiceToolCatalog(app, {
231
+ skipActionPrefixes: []
232
+ });
233
+ const toolSet = catalog.resolveToolSet({
234
+ actor: { id: 1 },
235
+ permissions: [],
236
+ channel: "internal",
237
+ surface: "admin"
238
+ });
239
+
240
+ assert.equal(toolSet.tools.length, 0);
241
+ });
242
+
243
+ test("service tool catalog honors barred action ids", () => {
244
+ const app = createApp();
245
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
246
+ actionRuntimeProvider.register(app);
247
+
248
+ app.service(
249
+ "demo.audit.service",
250
+ () => ({
251
+ listEntries() {
252
+ return [];
253
+ }
254
+ })
255
+ );
256
+
257
+ app.actions([
258
+ {
259
+ id: "demo.audit.list",
260
+ domain: "demo",
261
+ version: 1,
262
+ kind: "query",
263
+ channels: ["automation"],
264
+ surfaces: ["admin"],
265
+ permission: {
266
+ require: "authenticated"
267
+ },
268
+ dependencies: {
269
+ auditService: "demo.audit.service"
270
+ },
271
+ inputValidator: {
272
+ schema: Type.Object({}, { additionalProperties: false })
273
+ },
274
+ outputValidator: {
275
+ schema: Type.Array(Type.Object({}, { additionalProperties: true }))
276
+ },
277
+ idempotency: "none",
278
+ audit: {
279
+ actionName: "demo.audit.list"
280
+ },
281
+ observability: {},
282
+ async execute(input, _context, deps) {
283
+ return deps.auditService.listEntries(input);
284
+ }
285
+ }
286
+ ]);
287
+
288
+ const catalog = createServiceToolCatalog(app, {
289
+ skipActionPrefixes: [],
290
+ barredActionIds: ["demo.audit.list"]
291
+ });
292
+
293
+ const toolSet = catalog.resolveToolSet({ actor: { id: 1 }, permissions: [], channel: "internal", surface: "admin" });
294
+ assert.equal(toolSet.tools.length, 0);
295
+ });
296
+
297
+ test("service tool catalog materializes action tools once and filters per request", () => {
298
+ const app = createApp();
299
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
300
+ actionRuntimeProvider.register(app);
301
+ let factoryCalls = 0;
302
+
303
+ app.service(
304
+ "demo.cached.service",
305
+ () => {
306
+ factoryCalls += 1;
307
+ return {
308
+ listRecords() {
309
+ return [];
310
+ }
311
+ };
312
+ }
313
+ );
314
+
315
+ app.actions([
316
+ {
317
+ id: "demo.cached.list",
318
+ domain: "demo",
319
+ version: 1,
320
+ kind: "query",
321
+ channels: ["automation"],
322
+ surfaces: ["admin"],
323
+ permission: {
324
+ require: "authenticated"
325
+ },
326
+ dependencies: {
327
+ cachedService: "demo.cached.service"
328
+ },
329
+ inputValidator: {
330
+ schema: { type: "object", additionalProperties: true }
331
+ },
332
+ idempotency: "none",
333
+ audit: {
334
+ actionName: "demo.cached.list"
335
+ },
336
+ observability: {},
337
+ async execute(input, _context, deps) {
338
+ return deps.cachedService.listRecords(input);
339
+ }
340
+ }
341
+ ]);
342
+
343
+ const internalContext = {
344
+ channel: "internal",
345
+ surface: "admin"
346
+ };
347
+
348
+ const catalog = createServiceToolCatalog(app, {
349
+ skipActionPrefixes: []
350
+ });
351
+
352
+ assert.equal(factoryCalls, 0);
353
+
354
+ catalog.resolveToolSet({
355
+ ...internalContext,
356
+ permissions: []
357
+ });
358
+ catalog.resolveToolSet({
359
+ ...internalContext,
360
+ actor: { id: 1 },
361
+ permissions: []
362
+ });
363
+ catalog.resolveToolSet({
364
+ ...internalContext,
365
+ actor: { id: 2 },
366
+ permissions: ["demo.read"]
367
+ });
368
+
369
+ assert.equal(factoryCalls, 1);
370
+ });
371
+
372
+ test("service tool catalog uses action-backed schemas for tool contracts", () => {
373
+ const app = createApp();
374
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
375
+ actionRuntimeProvider.register(app);
376
+
377
+ const inputSchema = Object.freeze({
378
+ type: "object",
379
+ properties: {
380
+ args: {
381
+ type: "array",
382
+ prefixItems: [
383
+ {
384
+ type: "object",
385
+ properties: {
386
+ displayName: {
387
+ type: "string"
388
+ }
389
+ },
390
+ required: ["displayName"],
391
+ additionalProperties: false
392
+ }
393
+ ],
394
+ minItems: 1,
395
+ maxItems: 1
396
+ },
397
+ options: {
398
+ type: "object",
399
+ additionalProperties: true
400
+ }
401
+ },
402
+ additionalProperties: false
403
+ });
404
+ const outputSchema = Object.freeze({
405
+ type: "object",
406
+ properties: {
407
+ ok: {
408
+ type: "boolean"
409
+ }
410
+ },
411
+ required: ["ok"],
412
+ additionalProperties: false
413
+ });
414
+
415
+ app.service(
416
+ "demo.schemas.service",
417
+ () => ({
418
+ updateRecord(payload = {}) {
419
+ return {
420
+ ok: Boolean(payload?.displayName)
421
+ };
422
+ }
423
+ })
424
+ );
425
+
426
+ app.actions([
427
+ {
428
+ id: "demo.schemas.update",
429
+ domain: "demo",
430
+ version: 1,
431
+ kind: "command",
432
+ channels: ["automation"],
433
+ surfaces: ["admin"],
434
+ permission: {
435
+ require: "authenticated"
436
+ },
437
+ dependencies: {
438
+ schemasService: "demo.schemas.service"
439
+ },
440
+ inputValidator: {
441
+ schema: inputSchema
442
+ },
443
+ outputValidator: {
444
+ schema: outputSchema
445
+ },
446
+ idempotency: "optional",
447
+ audit: {
448
+ actionName: "demo.schemas.update"
449
+ },
450
+ observability: {},
451
+ extensions: {
452
+ assistant: {
453
+ description: "Update profile display name."
454
+ }
455
+ },
456
+ async execute(input, _context, deps) {
457
+ return deps.schemasService.updateRecord(input);
458
+ }
459
+ }
460
+ ]);
461
+
462
+ const catalog = createServiceToolCatalog(app, {
463
+ skipActionPrefixes: []
464
+ });
465
+ const toolSet = catalog.resolveToolSet({
466
+ actor: { id: 1 },
467
+ permissions: []
468
+ });
469
+
470
+ assert.equal(toolSet.tools.length, 1);
471
+ assert.equal(toolSet.tools[0].description, "Update profile display name.");
472
+ assert.equal(toolSet.tools[0].parameters, inputSchema);
473
+ assert.equal(toolSet.tools[0].outputSchema, outputSchema);
474
+ });
475
+
476
+ test("service tool catalog rejects legacy assistantTool field at assistant layer", () => {
477
+ const app = createApp();
478
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
479
+ actionRuntimeProvider.register(app);
480
+
481
+ app.service(
482
+ "demo.legacy_assistant.service",
483
+ () => ({
484
+ createLegacy(input = {}) {
485
+ return {
486
+ ok: Boolean(input)
487
+ };
488
+ },
489
+ createModern(input = {}) {
490
+ return {
491
+ ok: Boolean(input)
492
+ };
493
+ }
494
+ })
495
+ );
496
+
497
+ const schema = Object.freeze({
498
+ type: "object",
499
+ additionalProperties: false
500
+ });
501
+ const outputSchema = Object.freeze({
502
+ type: "object",
503
+ properties: {
504
+ ok: {
505
+ type: "boolean"
506
+ }
507
+ },
508
+ required: ["ok"],
509
+ additionalProperties: false
510
+ });
511
+
512
+ app.actions([
513
+ {
514
+ id: "demo.legacy_assistant.create",
515
+ domain: "demo",
516
+ version: 1,
517
+ kind: "command",
518
+ channels: ["automation"],
519
+ surfaces: ["admin"],
520
+ permission: {
521
+ require: "authenticated"
522
+ },
523
+ dependencies: {
524
+ legacyAssistantService: "demo.legacy_assistant.service"
525
+ },
526
+ inputValidator: {
527
+ schema
528
+ },
529
+ outputValidator: {
530
+ schema: outputSchema
531
+ },
532
+ idempotency: "optional",
533
+ audit: {
534
+ actionName: "demo.legacy_assistant.create"
535
+ },
536
+ observability: {},
537
+ assistantTool: {
538
+ description: "Legacy assistant tool metadata."
539
+ },
540
+ async execute(input, _context, deps) {
541
+ return deps.legacyAssistantService.createLegacy(input);
542
+ }
543
+ },
544
+ {
545
+ id: "demo.modern_assistant.create",
546
+ domain: "demo",
547
+ version: 1,
548
+ kind: "command",
549
+ channels: ["automation"],
550
+ surfaces: ["admin"],
551
+ permission: {
552
+ require: "authenticated"
553
+ },
554
+ dependencies: {
555
+ legacyAssistantService: "demo.legacy_assistant.service"
556
+ },
557
+ inputValidator: {
558
+ schema
559
+ },
560
+ outputValidator: {
561
+ schema: outputSchema
562
+ },
563
+ idempotency: "optional",
564
+ audit: {
565
+ actionName: "demo.modern_assistant.create"
566
+ },
567
+ observability: {},
568
+ extensions: {
569
+ assistant: {
570
+ description: "Modern assistant extension metadata."
571
+ }
572
+ },
573
+ async execute(input, _context, deps) {
574
+ return deps.legacyAssistantService.createModern(input);
575
+ }
576
+ }
577
+ ]);
578
+
579
+ const catalog = createServiceToolCatalog(app, {
580
+ skipActionPrefixes: []
581
+ });
582
+ const toolSet = catalog.resolveToolSet({
583
+ actor: { id: 1 },
584
+ permissions: []
585
+ });
586
+ const actionIds = toolSet.tools.map((tool) => tool.actionId).sort();
587
+
588
+ assert.deepEqual(actionIds, ["demo.modern_assistant.create"]);
589
+ });
590
+
591
+ test("service tool catalog can require input/output schemas for tool exposure", () => {
592
+ const app = createApp();
593
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
594
+ actionRuntimeProvider.register(app);
595
+
596
+ app.service(
597
+ "demo.strict.service",
598
+ () => ({
599
+ noSchema() {
600
+ return {
601
+ ok: true
602
+ };
603
+ },
604
+ withSchema() {
605
+ return {
606
+ ok: true
607
+ };
608
+ }
609
+ })
610
+ );
611
+
612
+ app.actions([
613
+ {
614
+ id: "demo.strict.with_schema",
615
+ domain: "demo",
616
+ version: 1,
617
+ kind: "query",
618
+ channels: ["automation"],
619
+ surfaces: ["admin"],
620
+ permission: {
621
+ require: "authenticated"
622
+ },
623
+ dependencies: {
624
+ strictService: "demo.strict.service"
625
+ },
626
+ inputValidator: {
627
+ schema: {
628
+ type: "object",
629
+ additionalProperties: false
630
+ }
631
+ },
632
+ outputValidator: {
633
+ schema: {
634
+ type: "object",
635
+ properties: {
636
+ ok: {
637
+ type: "boolean"
638
+ }
639
+ },
640
+ required: ["ok"],
641
+ additionalProperties: false
642
+ }
643
+ },
644
+ idempotency: "none",
645
+ audit: {
646
+ actionName: "demo.strict.with_schema"
647
+ },
648
+ observability: {},
649
+ async execute(_input, _context, deps) {
650
+ return deps.strictService.withSchema();
651
+ }
652
+ }
653
+ ]);
654
+
655
+ const catalog = createServiceToolCatalog(app, {
656
+ skipActionPrefixes: []
657
+ });
658
+ const toolSet = catalog.resolveToolSet({
659
+ actor: { id: 1 },
660
+ permissions: []
661
+ });
662
+
663
+ assert.equal(toolSet.tools.length, 1);
664
+ assert.equal(toolSet.tools[0].actionId, "demo.strict.with_schema");
665
+ });
666
+
667
+ test("service tool catalog derives tool schemas from action contributors", () => {
668
+ const app = createApp();
669
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
670
+ actionRuntimeProvider.register(app);
671
+
672
+ const inputSchema = Object.freeze({
673
+ type: "object",
674
+ properties: {
675
+ workspaceSlug: {
676
+ type: "string"
677
+ },
678
+ name: {
679
+ type: "string"
680
+ },
681
+ surname: {
682
+ type: "string"
683
+ }
684
+ },
685
+ additionalProperties: false
686
+ });
687
+ const outputSchema = Object.freeze({
688
+ type: "object",
689
+ properties: {
690
+ id: {
691
+ type: "integer"
692
+ }
693
+ },
694
+ required: ["id"],
695
+ additionalProperties: false
696
+ });
697
+
698
+ app.service(
699
+ "demo.customers.service",
700
+ () => ({
701
+ createRecord(payload = {}) {
702
+ return {
703
+ id: 1,
704
+ ...payload
705
+ };
706
+ }
707
+ })
708
+ );
709
+
710
+ app.actions([
711
+ {
712
+ id: "demo.customers.create",
713
+ domain: "demo",
714
+ version: 1,
715
+ kind: "command",
716
+ channels: ["automation"],
717
+ surfaces: ["admin"],
718
+ permission: {
719
+ require: "authenticated"
720
+ },
721
+ dependencies: {
722
+ customersService: "demo.customers.service"
723
+ },
724
+ inputValidator: {
725
+ schema: inputSchema
726
+ },
727
+ outputValidator: {
728
+ schema: outputSchema
729
+ },
730
+ idempotency: "optional",
731
+ audit: {
732
+ actionName: "demo.customers.create"
733
+ },
734
+ observability: {},
735
+ async execute(input, _context, deps) {
736
+ return deps.customersService.createRecord(input);
737
+ }
738
+ }
739
+ ]);
740
+
741
+ const catalog = createServiceToolCatalog(app, {
742
+ skipActionPrefixes: []
743
+ });
744
+ const toolSet = catalog.resolveToolSet({
745
+ actor: {
746
+ id: 1
747
+ },
748
+ permissions: []
749
+ });
750
+ const createTool = toolSet.tools.find((tool) => tool.actionId === "demo.customers.create");
751
+
752
+ assert.ok(createTool);
753
+ assert.equal(createTool.parameters, inputSchema);
754
+ assert.equal(createTool.outputSchema, outputSchema);
755
+ });
756
+
757
+ test("service tool catalog derives input schema from array action validators", () => {
758
+ const app = createApp();
759
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
760
+ actionRuntimeProvider.register(app);
761
+
762
+ app.service(
763
+ "demo.array_schema.service",
764
+ () => ({
765
+ createRecord(payload = {}) {
766
+ return {
767
+ id: 1,
768
+ ...payload
769
+ };
770
+ }
771
+ })
772
+ );
773
+
774
+ app.actions([
775
+ {
776
+ id: "demo.array_schema.create",
777
+ domain: "demo",
778
+ version: 1,
779
+ kind: "command",
780
+ channels: ["automation"],
781
+ surfaces: ["admin"],
782
+ permission: {
783
+ require: "authenticated"
784
+ },
785
+ dependencies: {
786
+ arraySchemaService: "demo.array_schema.service"
787
+ },
788
+ inputValidator: [
789
+ {
790
+ schema: Type.Object(
791
+ {
792
+ workspaceSlug: Type.String({ minLength: 1 })
793
+ },
794
+ { additionalProperties: false }
795
+ )
796
+ },
797
+ {
798
+ schema: Type.Object(
799
+ {
800
+ name: Type.String({ minLength: 1 }),
801
+ surname: Type.String({ minLength: 1 })
802
+ },
803
+ { additionalProperties: false }
804
+ )
805
+ }
806
+ ],
807
+ outputValidator: {
808
+ schema: Type.Object(
809
+ {
810
+ id: Type.Integer()
811
+ },
812
+ { additionalProperties: true }
813
+ )
814
+ },
815
+ idempotency: "optional",
816
+ audit: {
817
+ actionName: "demo.array_schema.create"
818
+ },
819
+ observability: {},
820
+ async execute(input, _context, deps) {
821
+ return deps.arraySchemaService.createRecord(input);
822
+ }
823
+ }
824
+ ]);
825
+
826
+ const catalog = createServiceToolCatalog(app, {
827
+ skipActionPrefixes: []
828
+ });
829
+ const toolSet = catalog.resolveToolSet({
830
+ actor: {
831
+ id: 1
832
+ },
833
+ permissions: [],
834
+ channel: "internal",
835
+ surface: "admin"
836
+ });
837
+ const createTool = toolSet.tools.find((tool) => tool.actionId === "demo.array_schema.create");
838
+
839
+ assert.ok(createTool);
840
+ assert.equal(createTool.parameters?.type, "object");
841
+ assert.equal(typeof createTool.parameters?.properties?.workspaceSlug, "object");
842
+ assert.equal(typeof createTool.parameters?.properties?.name, "object");
843
+ assert.equal(typeof createTool.parameters?.properties?.surname, "object");
844
+ });
845
+
846
+ test("service tool catalog preserves section-map validators in tool schemas", () => {
847
+ const app = createApp();
848
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
849
+ actionRuntimeProvider.register(app);
850
+
851
+ app.service(
852
+ "demo.workspace_settings.service",
853
+ () => ({
854
+ updateSettings(input = {}) {
855
+ return input;
856
+ }
857
+ })
858
+ );
859
+
860
+ const patchValidator = Object.freeze({
861
+ schema: Type.Object(
862
+ {
863
+ name: Type.String({ minLength: 1 })
864
+ },
865
+ { additionalProperties: false }
866
+ )
867
+ });
868
+
869
+ app.actions([
870
+ {
871
+ id: "demo.workspace.settings.update",
872
+ domain: "demo",
873
+ version: 1,
874
+ kind: "command",
875
+ channels: ["automation"],
876
+ surfaces: ["admin"],
877
+ permission: {
878
+ require: "authenticated"
879
+ },
880
+ dependencies: {
881
+ workspaceSettingsService: "demo.workspace_settings.service"
882
+ },
883
+ inputValidator: [
884
+ {
885
+ schema: Type.Object(
886
+ {
887
+ workspaceSlug: Type.String({ minLength: 1 })
888
+ },
889
+ { additionalProperties: false }
890
+ )
891
+ },
892
+ {
893
+ patch: patchValidator
894
+ }
895
+ ],
896
+ outputValidator: {
897
+ schema: Type.Object(
898
+ {
899
+ ok: Type.Boolean()
900
+ },
901
+ { additionalProperties: false }
902
+ )
903
+ },
904
+ idempotency: "optional",
905
+ audit: {
906
+ actionName: "demo.workspace.settings.update"
907
+ },
908
+ observability: {},
909
+ extensions: {
910
+ assistant: {}
911
+ },
912
+ async execute(input, _context, deps) {
913
+ const result = deps.workspaceSettingsService.updateSettings(input);
914
+ return {
915
+ ok: Boolean(result)
916
+ };
917
+ }
918
+ }
919
+ ]);
920
+
921
+ const catalog = createServiceToolCatalog(app, {
922
+ skipActionPrefixes: []
923
+ });
924
+ const toolSet = catalog.resolveToolSet({
925
+ actor: { id: 1 },
926
+ permissions: [],
927
+ channel: "internal",
928
+ surface: "admin",
929
+ requestMeta: {
930
+ resolvedWorkspaceContext: {
931
+ workspace: {
932
+ slug: "tonymobily3"
933
+ }
934
+ }
935
+ }
936
+ });
937
+ const updateTool = toolSet.tools.find((tool) => tool.actionId === "demo.workspace.settings.update");
938
+
939
+ assert.ok(updateTool);
940
+ assert.equal(updateTool.parameters?.type, "object");
941
+ assert.equal(Object.hasOwn(updateTool.parameters?.properties || {}, "workspaceSlug"), false);
942
+ assert.equal(typeof updateTool.parameters?.properties?.patch, "object");
943
+ assert.equal(updateTool.parameters?.properties?.patch.type, "object");
944
+ assert.equal(typeof updateTool.parameters?.properties?.patch.properties?.name, "object");
945
+ });
946
+
947
+ test("service tool catalog hides workspaceSlug parameter when workspace context is already resolved", () => {
948
+ const app = createApp();
949
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
950
+ actionRuntimeProvider.register(app);
951
+
952
+ app.service(
953
+ "demo.workspace_scope.service",
954
+ () => ({
955
+ createRecord(payload = {}) {
956
+ return payload;
957
+ }
958
+ })
959
+ );
960
+
961
+ app.actions([
962
+ {
963
+ id: "demo.workspace_scope.create",
964
+ domain: "demo",
965
+ version: 1,
966
+ kind: "command",
967
+ channels: ["automation"],
968
+ surfaces: ["admin"],
969
+ permission: {
970
+ require: "authenticated"
971
+ },
972
+ dependencies: {
973
+ workspaceScopeService: "demo.workspace_scope.service"
974
+ },
975
+ inputValidator: {
976
+ schema: Type.Object(
977
+ {
978
+ workspaceSlug: Type.String({ minLength: 1 }),
979
+ name: Type.String({ minLength: 1 })
980
+ },
981
+ { additionalProperties: false }
982
+ )
983
+ },
984
+ outputValidator: {
985
+ schema: Type.Object(
986
+ {
987
+ workspaceSlug: Type.String({ minLength: 1 }),
988
+ name: Type.String({ minLength: 1 })
989
+ },
990
+ { additionalProperties: false }
991
+ )
992
+ },
993
+ idempotency: "optional",
994
+ audit: {
995
+ actionName: "demo.workspace_scope.create"
996
+ },
997
+ observability: {},
998
+ async execute(input, _context, deps) {
999
+ return deps.workspaceScopeService.createRecord(input);
1000
+ }
1001
+ }
1002
+ ]);
1003
+
1004
+ const catalog = createServiceToolCatalog(app, {
1005
+ skipActionPrefixes: []
1006
+ });
1007
+ const toolSet = catalog.resolveToolSet({
1008
+ actor: {
1009
+ id: 1
1010
+ },
1011
+ permissions: [],
1012
+ channel: "internal",
1013
+ surface: "admin",
1014
+ requestMeta: {
1015
+ resolvedWorkspaceContext: {
1016
+ workspace: {
1017
+ slug: "tonymobily3"
1018
+ }
1019
+ }
1020
+ }
1021
+ });
1022
+ const createTool = toolSet.tools.find(
1023
+ (tool) => tool.actionId === "demo.workspace_scope.create"
1024
+ );
1025
+
1026
+ assert.ok(createTool);
1027
+ assert.equal(Object.hasOwn(createTool.parameters.properties, "workspaceSlug"), false);
1028
+ assert.equal(typeof createTool.parameters.properties.name, "object");
1029
+ });
1030
+
1031
+ test("service tool catalog injects workspaceSlug from requestMeta request params", async () => {
1032
+ const app = createApp();
1033
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
1034
+ actionRuntimeProvider.register(app);
1035
+
1036
+ app.service(
1037
+ "demo.workspace_injection.service",
1038
+ () => ({
1039
+ createRecord(payload = {}) {
1040
+ return payload;
1041
+ }
1042
+ })
1043
+ );
1044
+
1045
+ app.actions([
1046
+ {
1047
+ id: "demo.workspace_injection.create",
1048
+ domain: "demo",
1049
+ version: 1,
1050
+ kind: "command",
1051
+ channels: ["automation"],
1052
+ surfaces: ["admin"],
1053
+ permission: {
1054
+ require: "authenticated"
1055
+ },
1056
+ dependencies: {
1057
+ workspaceInjectionService: "demo.workspace_injection.service"
1058
+ },
1059
+ inputValidator: {
1060
+ schema: Type.Object(
1061
+ {
1062
+ workspaceSlug: Type.String({ minLength: 1 }),
1063
+ name: Type.String({ minLength: 1 })
1064
+ },
1065
+ { additionalProperties: false }
1066
+ )
1067
+ },
1068
+ outputValidator: {
1069
+ schema: Type.Object(
1070
+ {
1071
+ workspaceSlug: Type.String({ minLength: 1 }),
1072
+ name: Type.String({ minLength: 1 })
1073
+ },
1074
+ { additionalProperties: false }
1075
+ )
1076
+ },
1077
+ idempotency: "optional",
1078
+ audit: {
1079
+ actionName: "demo.workspace_injection.create"
1080
+ },
1081
+ observability: {},
1082
+ async execute(input, _context, deps) {
1083
+ return deps.workspaceInjectionService.createRecord(input);
1084
+ }
1085
+ }
1086
+ ]);
1087
+
1088
+ const catalog = createServiceToolCatalog(app, {
1089
+ skipActionPrefixes: []
1090
+ });
1091
+ const context = {
1092
+ actor: {
1093
+ id: 1
1094
+ },
1095
+ permissions: [],
1096
+ channel: "internal",
1097
+ surface: "admin",
1098
+ requestMeta: {
1099
+ request: {
1100
+ input: {
1101
+ params: {
1102
+ workspaceSlug: "tonymobily3"
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ };
1108
+ const toolSet = catalog.resolveToolSet(context);
1109
+ const createTool = toolSet.tools.find(
1110
+ (tool) => tool.actionId === "demo.workspace_injection.create"
1111
+ );
1112
+ assert.ok(createTool);
1113
+
1114
+ const execution = await catalog.executeToolCall({
1115
+ toolName: createTool.name,
1116
+ argumentsText: JSON.stringify({
1117
+ name: "Merc"
1118
+ }),
1119
+ context,
1120
+ toolSet
1121
+ });
1122
+
1123
+ assert.equal(execution.ok, true);
1124
+ assert.deepEqual(execution.result, {
1125
+ workspaceSlug: "tonymobily3",
1126
+ name: "Merc"
1127
+ });
1128
+ });
1129
+
1130
+ test("service tool catalog executes action-backed tools with object payloads", async () => {
1131
+ const app = createApp();
1132
+ const actionRuntimeProvider = new ActionRuntimeServiceProvider();
1133
+ actionRuntimeProvider.register(app);
1134
+
1135
+ app.service(
1136
+ "demo.customers.service",
1137
+ () => ({
1138
+ updateRecord(recordId, payload = {}) {
1139
+ return {
1140
+ id: Number(recordId),
1141
+ payload
1142
+ };
1143
+ }
1144
+ })
1145
+ );
1146
+
1147
+ app.actions([
1148
+ {
1149
+ id: "demo.customers.update",
1150
+ domain: "demo",
1151
+ version: 1,
1152
+ kind: "command",
1153
+ channels: ["automation"],
1154
+ surfaces: ["admin"],
1155
+ permission: {
1156
+ require: "authenticated"
1157
+ },
1158
+ dependencies: {
1159
+ customersService: "demo.customers.service"
1160
+ },
1161
+ inputValidator: {
1162
+ schema: {
1163
+ type: "object",
1164
+ properties: {
1165
+ recordId: {
1166
+ type: "integer",
1167
+ minimum: 1
1168
+ },
1169
+ name: {
1170
+ type: "string"
1171
+ }
1172
+ },
1173
+ required: ["recordId"],
1174
+ additionalProperties: false
1175
+ }
1176
+ },
1177
+ outputValidator: {
1178
+ schema: {
1179
+ type: "object",
1180
+ properties: {
1181
+ id: {
1182
+ type: "integer"
1183
+ }
1184
+ },
1185
+ required: ["id"],
1186
+ additionalProperties: true
1187
+ }
1188
+ },
1189
+ idempotency: "optional",
1190
+ audit: {
1191
+ actionName: "demo.customers.update"
1192
+ },
1193
+ observability: {},
1194
+ async execute(input, context, deps) {
1195
+ const { recordId, ...patch } = input;
1196
+ return deps.customersService.updateRecord(recordId, patch, {
1197
+ context
1198
+ });
1199
+ }
1200
+ }
1201
+ ]);
1202
+
1203
+ const catalog = createServiceToolCatalog(app, {
1204
+ skipActionPrefixes: []
1205
+ });
1206
+ const context = {
1207
+ actor: {
1208
+ id: 1
1209
+ },
1210
+ permissions: [],
1211
+ channel: "internal",
1212
+ surface: "admin"
1213
+ };
1214
+ const toolSet = catalog.resolveToolSet(context);
1215
+ const updateTool = toolSet.tools.find((tool) => tool.actionId === "demo.customers.update");
1216
+ assert.ok(updateTool);
1217
+
1218
+ const execution = await catalog.executeToolCall({
1219
+ toolName: updateTool.name,
1220
+ argumentsText: JSON.stringify({
1221
+ recordId: 7,
1222
+ name: "Merc"
1223
+ }),
1224
+ context,
1225
+ toolSet
1226
+ });
1227
+
1228
+ assert.equal(execution.ok, true);
1229
+ assert.deepEqual(execution.result, {
1230
+ id: 7,
1231
+ payload: {
1232
+ name: "Merc"
1233
+ }
1234
+ });
1235
+ });