@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,841 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createChatService } from "../src/server/services/chatService.js";
4
+
5
+ function createAssistantSettingsServiceStub({ prompt = "" } = {}) {
6
+ return {
7
+ async resolveSystemPrompt() {
8
+ return String(prompt || "");
9
+ }
10
+ };
11
+ }
12
+
13
+ test("chat service system prompt includes current workspace slug from resolved workspace context", async () => {
14
+ let capturedMessages = null;
15
+
16
+ const aiClient = {
17
+ enabled: true,
18
+ provider: "openai",
19
+ defaultModel: "gpt-test",
20
+ async createChatCompletionStream(payload = {}) {
21
+ capturedMessages = Array.isArray(payload.messages) ? payload.messages : null;
22
+
23
+ return (async function* generate() {
24
+ yield {
25
+ choices: [
26
+ {
27
+ delta: {
28
+ content: "Hello"
29
+ }
30
+ }
31
+ ]
32
+ };
33
+ })();
34
+ }
35
+ };
36
+
37
+ const transcriptService = {
38
+ async createConversationForTurn() {
39
+ return {
40
+ conversation: {
41
+ id: 42
42
+ }
43
+ };
44
+ },
45
+ async appendMessage() {
46
+ return null;
47
+ },
48
+ async completeConversation() {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ const serviceToolCatalog = {
54
+ resolveToolSet() {
55
+ return {
56
+ tools: [],
57
+ byName: new Map()
58
+ };
59
+ },
60
+ toOpenAiToolSchema() {
61
+ return {
62
+ type: "function",
63
+ function: {
64
+ name: "noop",
65
+ description: "noop",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {},
69
+ additionalProperties: false
70
+ }
71
+ }
72
+ };
73
+ }
74
+ };
75
+
76
+ const streamWriter = {
77
+ sendMeta() {},
78
+ sendAssistantDelta() {},
79
+ sendAssistantMessage() {},
80
+ sendToolCall() {},
81
+ sendToolResult() {},
82
+ sendError() {},
83
+ sendDone() {}
84
+ };
85
+
86
+ const chatService = createChatService({
87
+ aiClient,
88
+ transcriptService,
89
+ serviceToolCatalog,
90
+ assistantSettingsService: createAssistantSettingsServiceStub({
91
+ prompt: "Always answer in short bullet points."
92
+ })
93
+ });
94
+
95
+ await chatService.streamChat(
96
+ {
97
+ messageId: "msg_1",
98
+ input: "Hi assistant"
99
+ },
100
+ {
101
+ context: {
102
+ actor: {
103
+ id: 7
104
+ },
105
+ workspace: {
106
+ id: 1,
107
+ slug: "tonymobily3"
108
+ },
109
+ requestMeta: {
110
+ resolvedWorkspaceContext: {
111
+ workspace: {
112
+ slug: "tonymobily3"
113
+ }
114
+ }
115
+ }
116
+ },
117
+ streamWriter
118
+ }
119
+ );
120
+
121
+ assert.ok(Array.isArray(capturedMessages));
122
+ assert.equal(capturedMessages[0]?.role, "system");
123
+ assert.match(String(capturedMessages[0]?.content || ""), /Current workspace slug: tonymobily3\./);
124
+ assert.match(String(capturedMessages[0]?.content || ""), /Always answer in short bullet points\./);
125
+ });
126
+
127
+ test("chat service recovers with a final assistant answer when a tool call fails", async () => {
128
+ const completionPayloads = [];
129
+ const appendedMessages = [];
130
+ const completedStatuses = [];
131
+ const emittedAssistantMessages = [];
132
+ const emittedDone = [];
133
+ let streamCall = 0;
134
+
135
+ const aiClient = {
136
+ enabled: true,
137
+ provider: "openai",
138
+ defaultModel: "gpt-test",
139
+ async createChatCompletionStream(payload = {}) {
140
+ completionPayloads.push(payload);
141
+ streamCall += 1;
142
+
143
+ if (streamCall === 1) {
144
+ return (async function* generateToolCall() {
145
+ yield {
146
+ choices: [
147
+ {
148
+ delta: {
149
+ tool_calls: [
150
+ {
151
+ index: 0,
152
+ id: "tool_1",
153
+ function: {
154
+ name: "users_accountprofile_service_getforuser",
155
+ arguments: "{}"
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ }
161
+ ]
162
+ };
163
+ })();
164
+ }
165
+
166
+ return (async function* generateRecoveryAnswer() {
167
+ yield {
168
+ choices: [
169
+ {
170
+ delta: {
171
+ content: "Recovered answer."
172
+ }
173
+ }
174
+ ]
175
+ };
176
+ })();
177
+ }
178
+ };
179
+
180
+ const transcriptService = {
181
+ async createConversationForTurn() {
182
+ return {
183
+ conversation: {
184
+ id: 77
185
+ }
186
+ };
187
+ },
188
+ async appendMessage(_conversationId, payload = {}) {
189
+ appendedMessages.push(payload);
190
+ return null;
191
+ },
192
+ async completeConversation(_conversationId, payload = {}) {
193
+ completedStatuses.push(String(payload.status || ""));
194
+ return null;
195
+ }
196
+ };
197
+
198
+ const serviceToolCatalog = {
199
+ resolveToolSet() {
200
+ const descriptor = {
201
+ name: "users_accountprofile_service_getforuser",
202
+ actionId: "settings.read",
203
+ actionVersion: 1,
204
+ description: "Read account profile.",
205
+ parameters: {
206
+ type: "object",
207
+ properties: {},
208
+ additionalProperties: false
209
+ },
210
+ outputSchema: {
211
+ type: "object",
212
+ properties: {},
213
+ additionalProperties: true
214
+ }
215
+ };
216
+
217
+ return {
218
+ tools: [descriptor],
219
+ byName: new Map([[descriptor.name, descriptor]])
220
+ };
221
+ },
222
+ toOpenAiToolSchema(tool = {}) {
223
+ return {
224
+ type: "function",
225
+ function: {
226
+ name: tool.name,
227
+ description: tool.description,
228
+ parameters: tool.parameters
229
+ }
230
+ };
231
+ },
232
+ async executeToolCall() {
233
+ return {
234
+ ok: false,
235
+ error: {
236
+ code: "ACTION_SURFACE_FORBIDDEN",
237
+ message: "Forbidden.",
238
+ status: 403
239
+ }
240
+ };
241
+ }
242
+ };
243
+
244
+ const streamWriter = {
245
+ sendMeta() {},
246
+ sendAssistantDelta() {},
247
+ sendAssistantMessage(payload = {}) {
248
+ emittedAssistantMessages.push(String(payload.text || ""));
249
+ },
250
+ sendToolCall() {},
251
+ sendToolResult() {},
252
+ sendError() {},
253
+ sendDone(payload = {}) {
254
+ emittedDone.push(String(payload.status || ""));
255
+ }
256
+ };
257
+
258
+ const chatService = createChatService({
259
+ aiClient,
260
+ transcriptService,
261
+ serviceToolCatalog,
262
+ assistantSettingsService: createAssistantSettingsServiceStub()
263
+ });
264
+
265
+ const result = await chatService.streamChat(
266
+ {
267
+ messageId: "msg_recovery",
268
+ input: "Know everything."
269
+ },
270
+ {
271
+ context: {
272
+ actor: {
273
+ id: 7
274
+ },
275
+ workspace: {
276
+ id: 1,
277
+ slug: "tonymobily3"
278
+ },
279
+ surface: "admin"
280
+ },
281
+ streamWriter
282
+ }
283
+ );
284
+
285
+ assert.equal(result.status, "completed");
286
+ assert.equal(streamCall, 2);
287
+ assert.equal(Array.isArray(completionPayloads[1]?.tools), true);
288
+ assert.equal(completionPayloads[1].tools.length, 0);
289
+ assert.ok(
290
+ appendedMessages.some((entry) => entry.role === "assistant" && entry.kind === "chat" && entry.contentText === "Recovered answer.")
291
+ );
292
+ assert.deepEqual(completedStatuses, ["completed"]);
293
+ assert.deepEqual(emittedAssistantMessages, ["Recovered answer."]);
294
+ assert.deepEqual(emittedDone, ["completed"]);
295
+ });
296
+
297
+ test("chat service strips raw tool-call markup from recovery assistant output", async () => {
298
+ const emittedAssistantMessages = [];
299
+ let streamCall = 0;
300
+
301
+ const aiClient = {
302
+ enabled: true,
303
+ provider: "openai",
304
+ defaultModel: "gpt-test",
305
+ async createChatCompletionStream() {
306
+ streamCall += 1;
307
+
308
+ if (streamCall === 1) {
309
+ return (async function* generateToolCall() {
310
+ yield {
311
+ choices: [
312
+ {
313
+ delta: {
314
+ tool_calls: [
315
+ {
316
+ index: 0,
317
+ id: "tool_1",
318
+ function: {
319
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
320
+ arguments: "{}"
321
+ }
322
+ }
323
+ ]
324
+ }
325
+ }
326
+ ]
327
+ };
328
+ })();
329
+ }
330
+
331
+ return (async function* generateRecoveryAnswer() {
332
+ yield {
333
+ choices: [
334
+ {
335
+ delta: {
336
+ content:
337
+ "<|DSML|function_calls>\n<|DSML|invoke name=\"users_console_settings_service_getsettings\"></|DSML|invoke>\n</|DSML|function_calls>"
338
+ }
339
+ }
340
+ ]
341
+ };
342
+ })();
343
+ }
344
+ };
345
+
346
+ const transcriptService = {
347
+ async createConversationForTurn() {
348
+ return {
349
+ conversation: {
350
+ id: 99
351
+ }
352
+ };
353
+ },
354
+ async appendMessage() {
355
+ return null;
356
+ },
357
+ async completeConversation() {
358
+ return null;
359
+ }
360
+ };
361
+
362
+ const serviceToolCatalog = {
363
+ resolveToolSet() {
364
+ const descriptor = {
365
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
366
+ actionId: "workspace.list",
367
+ actionVersion: 1,
368
+ description: "List workspaces for actor.",
369
+ parameters: {
370
+ type: "object",
371
+ properties: {},
372
+ additionalProperties: false
373
+ },
374
+ outputSchema: {
375
+ type: "object",
376
+ properties: {},
377
+ additionalProperties: true
378
+ }
379
+ };
380
+
381
+ return {
382
+ tools: [descriptor],
383
+ byName: new Map([[descriptor.name, descriptor]])
384
+ };
385
+ },
386
+ toOpenAiToolSchema(tool = {}) {
387
+ return {
388
+ type: "function",
389
+ function: {
390
+ name: tool.name,
391
+ description: tool.description,
392
+ parameters: tool.parameters
393
+ }
394
+ };
395
+ },
396
+ async executeToolCall() {
397
+ return {
398
+ ok: false,
399
+ error: {
400
+ code: "assistant_tool_failed",
401
+ message: "Tool failed.",
402
+ status: 500
403
+ }
404
+ };
405
+ }
406
+ };
407
+
408
+ const streamWriter = {
409
+ sendMeta() {},
410
+ sendAssistantDelta() {},
411
+ sendAssistantMessage(payload = {}) {
412
+ emittedAssistantMessages.push(String(payload.text || ""));
413
+ },
414
+ sendToolCall() {},
415
+ sendToolResult() {},
416
+ sendError() {},
417
+ sendDone() {}
418
+ };
419
+
420
+ const chatService = createChatService({
421
+ aiClient,
422
+ transcriptService,
423
+ serviceToolCatalog,
424
+ assistantSettingsService: createAssistantSettingsServiceStub()
425
+ });
426
+
427
+ await chatService.streamChat(
428
+ {
429
+ messageId: "msg_markup",
430
+ input: "Tell me about this workspace."
431
+ },
432
+ {
433
+ context: {
434
+ actor: {
435
+ id: 7
436
+ },
437
+ workspace: {
438
+ id: 1,
439
+ slug: "tonymobily3"
440
+ },
441
+ surface: "admin"
442
+ },
443
+ streamWriter
444
+ }
445
+ );
446
+
447
+ assert.equal(emittedAssistantMessages.length, 1);
448
+ assert.equal(
449
+ emittedAssistantMessages[0],
450
+ "I could not gather additional information from successful operations."
451
+ );
452
+ });
453
+
454
+ test("chat service retries plain-language recovery before fallback when post-failure completion text is empty", async () => {
455
+ const completionPayloads = [];
456
+ const emittedAssistantMessages = [];
457
+ let streamCall = 0;
458
+
459
+ const aiClient = {
460
+ enabled: true,
461
+ provider: "openai",
462
+ defaultModel: "gpt-test",
463
+ async createChatCompletionStream(payload = {}) {
464
+ completionPayloads.push(payload);
465
+ streamCall += 1;
466
+
467
+ if (streamCall === 1) {
468
+ return (async function* generateToolCall() {
469
+ yield {
470
+ choices: [
471
+ {
472
+ delta: {
473
+ tool_calls: [
474
+ {
475
+ index: 0,
476
+ id: "tool_1",
477
+ function: {
478
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
479
+ arguments: "{}"
480
+ }
481
+ }
482
+ ]
483
+ }
484
+ }
485
+ ]
486
+ };
487
+ })();
488
+ }
489
+
490
+ if (streamCall === 2) {
491
+ return (async function* generateEmptyAssistantText() {
492
+ yield {
493
+ choices: [
494
+ {
495
+ delta: {
496
+ content: ""
497
+ }
498
+ }
499
+ ]
500
+ };
501
+ })();
502
+ }
503
+
504
+ return (async function* generateRecoveryAnswer() {
505
+ yield {
506
+ choices: [
507
+ {
508
+ delta: {
509
+ content: "I could not run one workspace operation, but I can continue with the others."
510
+ }
511
+ }
512
+ ]
513
+ };
514
+ })();
515
+ }
516
+ };
517
+
518
+ const transcriptService = {
519
+ async createConversationForTurn() {
520
+ return {
521
+ conversation: {
522
+ id: 122
523
+ }
524
+ };
525
+ },
526
+ async appendMessage() {
527
+ return null;
528
+ },
529
+ async completeConversation() {
530
+ return null;
531
+ }
532
+ };
533
+
534
+ const serviceToolCatalog = {
535
+ resolveToolSet() {
536
+ const descriptor = {
537
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
538
+ actionId: "workspace.list",
539
+ actionVersion: 1,
540
+ description: "List workspaces for actor.",
541
+ parameters: {
542
+ type: "object",
543
+ properties: {},
544
+ additionalProperties: false
545
+ },
546
+ outputSchema: {
547
+ type: "object",
548
+ properties: {},
549
+ additionalProperties: true
550
+ }
551
+ };
552
+
553
+ return {
554
+ tools: [descriptor],
555
+ byName: new Map([[descriptor.name, descriptor]])
556
+ };
557
+ },
558
+ toOpenAiToolSchema(tool = {}) {
559
+ return {
560
+ type: "function",
561
+ function: {
562
+ name: tool.name,
563
+ description: tool.description,
564
+ parameters: tool.parameters
565
+ }
566
+ };
567
+ },
568
+ async executeToolCall() {
569
+ return {
570
+ ok: false,
571
+ error: {
572
+ code: "assistant_tool_failed",
573
+ message: "Tool failed.",
574
+ status: 500
575
+ }
576
+ };
577
+ }
578
+ };
579
+
580
+ const streamWriter = {
581
+ sendMeta() {},
582
+ sendAssistantDelta() {},
583
+ sendAssistantMessage(payload = {}) {
584
+ emittedAssistantMessages.push(String(payload.text || ""));
585
+ },
586
+ sendToolCall() {},
587
+ sendToolResult() {},
588
+ sendError() {},
589
+ sendDone() {}
590
+ };
591
+
592
+ const chatService = createChatService({
593
+ aiClient,
594
+ transcriptService,
595
+ serviceToolCatalog,
596
+ assistantSettingsService: createAssistantSettingsServiceStub()
597
+ });
598
+
599
+ await chatService.streamChat(
600
+ {
601
+ messageId: "msg_empty_after_failure",
602
+ input: "Tell me everything."
603
+ },
604
+ {
605
+ context: {
606
+ actor: {
607
+ id: 7
608
+ },
609
+ workspace: {
610
+ id: 1,
611
+ slug: "tonymobily3"
612
+ },
613
+ surface: "admin"
614
+ },
615
+ streamWriter
616
+ }
617
+ );
618
+
619
+ assert.equal(streamCall, 3);
620
+ assert.equal(Array.isArray(completionPayloads[2]?.tools), true);
621
+ assert.equal(completionPayloads[2].tools.length, 0);
622
+ assert.equal(
623
+ emittedAssistantMessages[0],
624
+ "I could not run one workspace operation, but I can continue with the others."
625
+ );
626
+ });
627
+
628
+ test("chat service recovery streams sanitized text without DSML tag leakage", async () => {
629
+ const emittedAssistantMessages = [];
630
+ const streamedDeltas = [];
631
+ let streamCall = 0;
632
+
633
+ const aiClient = {
634
+ enabled: true,
635
+ provider: "openai",
636
+ defaultModel: "gpt-test",
637
+ async createChatCompletionStream() {
638
+ streamCall += 1;
639
+
640
+ if (streamCall === 1) {
641
+ return (async function* generateInitialToolCall() {
642
+ yield {
643
+ choices: [
644
+ {
645
+ delta: {
646
+ tool_calls: [
647
+ {
648
+ index: 0,
649
+ id: "tool_1",
650
+ function: {
651
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
652
+ arguments: "{}"
653
+ }
654
+ }
655
+ ]
656
+ }
657
+ }
658
+ ]
659
+ };
660
+ })();
661
+ }
662
+
663
+ if (streamCall === 2) {
664
+ return (async function* generateRecoveryDsmlAndText() {
665
+ yield {
666
+ choices: [
667
+ {
668
+ delta: {
669
+ content:
670
+ "<|DSML|function_calls>\n<|DSML|invoke name=\"users_console_settings_service_getsettings\"></|DSML|invoke>\n</|DSML|function_calls>Interim notes from successful operations."
671
+ }
672
+ }
673
+ ]
674
+ };
675
+ })();
676
+ }
677
+
678
+ return (async function* generateFinalRecoveryAnswer() {
679
+ yield {
680
+ choices: [
681
+ {
682
+ delta: {
683
+ content: "Final response from available data."
684
+ }
685
+ }
686
+ ]
687
+ };
688
+ })();
689
+ }
690
+ };
691
+
692
+ const transcriptService = {
693
+ async createConversationForTurn() {
694
+ return {
695
+ conversation: {
696
+ id: 200
697
+ }
698
+ };
699
+ },
700
+ async appendMessage() {
701
+ return null;
702
+ },
703
+ async completeConversation() {
704
+ return null;
705
+ }
706
+ };
707
+
708
+ const serviceToolCatalog = {
709
+ resolveToolSet() {
710
+ const first = {
711
+ name: "users_workspace_service_listworkspacesforauthenticatedus",
712
+ actionId: "workspace.list",
713
+ actionVersion: 1,
714
+ description: "List workspaces for actor.",
715
+ parameters: {
716
+ type: "object",
717
+ properties: {},
718
+ additionalProperties: false
719
+ },
720
+ outputSchema: {
721
+ type: "object",
722
+ properties: {},
723
+ additionalProperties: true
724
+ }
725
+ };
726
+ const second = {
727
+ name: "users_console_settings_service_getsettings",
728
+ actionId: "console.settings.get",
729
+ actionVersion: 1,
730
+ description: "Read console settings.",
731
+ parameters: {
732
+ type: "object",
733
+ properties: {},
734
+ additionalProperties: false
735
+ },
736
+ outputSchema: {
737
+ type: "object",
738
+ properties: {},
739
+ additionalProperties: true
740
+ }
741
+ };
742
+
743
+ return {
744
+ tools: [first, second],
745
+ byName: new Map([
746
+ [first.name, first],
747
+ [second.name, second]
748
+ ])
749
+ };
750
+ },
751
+ toOpenAiToolSchema(tool = {}) {
752
+ return {
753
+ type: "function",
754
+ function: {
755
+ name: tool.name,
756
+ description: tool.description,
757
+ parameters: tool.parameters
758
+ }
759
+ };
760
+ },
761
+ async executeToolCall({ toolName = "" } = {}) {
762
+ if (toolName === "users_workspace_service_listworkspacesforauthenticatedus") {
763
+ return {
764
+ ok: false,
765
+ error: {
766
+ code: "assistant_tool_failed",
767
+ message: "Workspace listing failed.",
768
+ status: 500
769
+ }
770
+ };
771
+ }
772
+
773
+ if (toolName === "users_console_settings_service_getsettings") {
774
+ return {
775
+ ok: true,
776
+ result: {
777
+ locale: "en"
778
+ }
779
+ };
780
+ }
781
+
782
+ return {
783
+ ok: false,
784
+ error: {
785
+ code: "assistant_tool_unknown",
786
+ message: "Unknown tool.",
787
+ status: 400
788
+ }
789
+ };
790
+ }
791
+ };
792
+
793
+ const streamWriter = {
794
+ sendMeta() {},
795
+ sendAssistantDelta(payload = {}) {
796
+ streamedDeltas.push(String(payload.delta || ""));
797
+ },
798
+ sendAssistantMessage(payload = {}) {
799
+ emittedAssistantMessages.push(String(payload.text || ""));
800
+ },
801
+ sendToolCall() {},
802
+ sendToolResult() {},
803
+ sendError() {},
804
+ sendDone() {}
805
+ };
806
+
807
+ const chatService = createChatService({
808
+ aiClient,
809
+ transcriptService,
810
+ serviceToolCatalog,
811
+ assistantSettingsService: createAssistantSettingsServiceStub()
812
+ });
813
+
814
+ await chatService.streamChat(
815
+ {
816
+ messageId: "msg_stream_sanitized",
817
+ input: "Tell me everything."
818
+ },
819
+ {
820
+ context: {
821
+ actor: {
822
+ id: 7
823
+ },
824
+ workspace: {
825
+ id: 1,
826
+ slug: "tonymobily3"
827
+ },
828
+ surface: "admin"
829
+ },
830
+ streamWriter
831
+ }
832
+ );
833
+
834
+ const streamedText = streamedDeltas.join("");
835
+ assert.equal(streamedText.includes("DSML"), false, streamedText);
836
+ assert.equal(streamedText.includes("Interim notes from successful operations."), true);
837
+ assert.equal(emittedAssistantMessages.length, 1);
838
+ assert.equal(emittedAssistantMessages[0].includes("Interim notes from successful operations."), true);
839
+ assert.equal(emittedAssistantMessages[0].includes("Final response from available data."), true);
840
+ assert.equal(emittedAssistantMessages[0].includes("DSML"), false);
841
+ });