@jskit-ai/assistant-core 0.1.135 → 0.1.136

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