@jskit-ai/assistant-core 0.2.7 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -64,32 +64,42 @@ export function createAiConnectionClient(connection, { fetch, timeoutMs = 120_00
64
64
  abortSignal: signal, timeout: timeoutMs, maxRetries: 0, temperature, maxOutputTokens, reasoning: effort
65
65
  };
66
66
  }
67
- return Object.freeze({
68
- enabled: true, supportsAttachments: true, provider: connection.providerId, defaultModel: connection.model,
69
- async createChatCompletion(input) {
70
- const result = await generateText(options(input));
71
- return { choices: [{ message: { role: "assistant", content: result.text,
72
- tool_calls: result.toolCalls.map(call => ({ id: call.toolCallId, type: "function",
73
- function: { name: call.toolName, arguments: JSON.stringify(call.input) } })) }, finish_reason: result.finishReason }] };
74
- },
75
- async *createChatCompletionStream(input) {
76
- const result = streamText({ ...options(input), onError() {} });
77
- let toolIndex = 0;
78
- for await (const part of result.fullStream) {
79
- if (part.type === "error") throw part.error;
80
- if (part.type === "abort") throw input?.signal?.reason || new Error("Assistant request was aborted.");
81
- if (part.type === "text-delta") yield { choices: [{ delta: { content: part.text } }] };
82
- if (part.type === "finish") yield { choices: [{ delta: {}, finish_reason: part.finishReason }], usage: part.totalUsage };
83
- if (part.type === "tool-call") {
84
- // Only the application catalogue can reject an unavailable action
85
- // and persist its no-effect receipt. Other invalid calls stay errors.
86
- const unavailable = reportUnavailableToolCalls === true && NoSuchToolError.isInstance(part.error) &&
87
- !part.providerExecuted && part.input !== null && typeof part.input === "object" && !Array.isArray(part.input);
88
- if (part.invalid && !unavailable) throw part.error;
89
- yield { choices: [{ delta: { tool_calls: [{ index: toolIndex++, id: part.toolCallId,
90
- function: { name: part.toolName, arguments: JSON.stringify(part.input) } }] } }] };
91
- }
67
+
68
+ async function createChatCompletion(input) {
69
+ const result = await generateText(options(input));
70
+ return { choices: [{ message: { role: "assistant", content: result.text,
71
+ tool_calls: result.toolCalls.map(call => ({ id: call.toolCallId, type: "function",
72
+ function: { name: call.toolName, arguments: JSON.stringify(call.input) } })) }, finish_reason: result.finishReason }] };
73
+ }
74
+
75
+ async function* createChatCompletionStream(input) {
76
+ const result = streamText({ ...options(input), onError() {} });
77
+ let toolIndex = 0;
78
+ for await (const part of result.fullStream) {
79
+ if (part.type === "error") throw part.error;
80
+ if (part.type === "abort") throw input?.signal?.reason || new Error("Assistant request was aborted.");
81
+ if (part.type === "text-delta") yield { choices: [{ delta: { content: part.text } }] };
82
+ if (part.type === "finish") yield { choices: [{ delta: {}, finish_reason: part.finishReason }], usage: part.totalUsage };
83
+ if (part.type === "tool-call") {
84
+ // Only the application catalogue can reject an unavailable action
85
+ // and persist its no-effect receipt. Other invalid calls stay errors.
86
+ const unavailable = reportUnavailableToolCalls === true && NoSuchToolError.isInstance(part.error) &&
87
+ !part.providerExecuted && part.input !== null && typeof part.input === "object" && !Array.isArray(part.input);
88
+ if (part.invalid && !unavailable) throw part.error;
89
+ yield { choices: [{ delta: { tool_calls: [{ index: toolIndex++, id: part.toolCallId,
90
+ function: { name: part.toolName, arguments: JSON.stringify(part.input) } }] } }] };
92
91
  }
93
92
  }
93
+ }
94
+
95
+ return Object.freeze({
96
+ enabled: true,
97
+ supportsAttachments: true,
98
+ provider: connection.providerId,
99
+ defaultModel: connection.model,
100
+ // The AI SDK model itself, for the agent engine's loop.
101
+ model,
102
+ createChatCompletion,
103
+ createChatCompletionStream
94
104
  });
95
105
  }
@@ -7,6 +7,7 @@ function setNdjsonHeaders(reply) {
7
7
  reply.header("Cache-Control", "no-cache");
8
8
  reply.header("X-Accel-Buffering", "no");
9
9
 
10
+ // readable-check: allow method-probe - platform object: a Fastify reply's raw Node response, when it has one
10
11
  if (reply?.raw && typeof reply.raw.setHeader === "function") {
11
12
  reply.raw.setHeader("Content-Type", NDJSON_CONTENT_TYPE);
12
13
  reply.raw.setHeader("Cache-Control", "no-cache");
@@ -297,6 +297,10 @@ async function fetchAnthropicMessage({
297
297
  }
298
298
  }
299
299
 
300
+ /**
301
+ * An assistant client for Anthropic's Messages API, or a disabled client without a key.
302
+ * @param {{ enabled?: boolean, apiKey?: string, baseUrl?: string, model?: string, timeoutMs?: number }} [options]
303
+ */
300
304
  function createAnthropicClient({
301
305
  enabled = true,
302
306
  apiKey = "",
@@ -339,17 +343,22 @@ function createAnthropicClient({
339
343
  };
340
344
  }
341
345
 
346
+ const streaming = createAiConnectionClient({
347
+ providerId: "anthropic", sdkPackage: "@ai-sdk/anthropic", apiKey: normalizedApiKey,
348
+ model: normalizedModel, baseURL: `${normalizedBaseUrl}/v1`
349
+ }, { timeoutMs, maxOutputTokens: DEFAULT_ANTHROPIC_MAX_OUTPUT_LIMIT });
350
+
351
+ function createChatCompletionStream(input) {
352
+ return streaming.createChatCompletionStream({ temperature: 0.2, ...input });
353
+ }
354
+
342
355
  return Object.freeze({
343
356
  enabled: true,
344
357
  provider: "anthropic",
345
358
  defaultModel: normalizedModel,
359
+ model: streaming.model,
346
360
  createChatCompletion: createCompletion,
347
- createChatCompletionStream(input) {
348
- return createAiConnectionClient({
349
- providerId: "anthropic", sdkPackage: "@ai-sdk/anthropic", apiKey: normalizedApiKey,
350
- model: normalizedModel, baseURL: `${normalizedBaseUrl}/v1`
351
- }, { timeoutMs, maxOutputTokens: DEFAULT_ANTHROPIC_MAX_OUTPUT_LIMIT }).createChatCompletionStream({ temperature: 0.2, ...input });
352
- }
361
+ createChatCompletionStream
353
362
  });
354
363
  }
355
364
 
@@ -61,16 +61,20 @@ function createDisabledClient({ provider = DEFAULT_AI_PROVIDER, model = "" } = {
61
61
  throw new AppError(503, "Assistant provider is not configured.", { code: "assistant.provider.notConfigured" });
62
62
  };
63
63
 
64
+ async function createChatCompletion() {
65
+ disabledError();
66
+ }
67
+
68
+ async function createChatCompletionStream() {
69
+ disabledError();
70
+ }
71
+
64
72
  return Object.freeze({
65
73
  enabled: false,
66
74
  provider,
67
75
  defaultModel: model,
68
- async createChatCompletion() {
69
- disabledError();
70
- },
71
- async createChatCompletionStream() {
72
- disabledError();
73
- }
76
+ createChatCompletion,
77
+ createChatCompletionStream
74
78
  });
75
79
  }
76
80
 
@@ -1,4 +1,6 @@
1
1
  import OpenAI from "openai";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { chatModel } from "@jskit-ai/agent/server";
2
4
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
5
  import {
4
6
  createDisabledClient,
@@ -7,6 +9,11 @@ import {
7
9
  normalizeTimeoutMs
8
10
  } from "./common.js";
9
11
 
12
+ /**
13
+ * An assistant client for an OpenAI-compatible chat completions API, or a disabled client without a key.
14
+ * @param {{ enabled?: boolean, apiKey?: string, baseUrl?: string, model?: string, defaultModel?: string, provider?: string,
15
+ * timeoutMs?: number }} [options]
16
+ */
10
17
  function createOpenAiCompatibleClient({
11
18
  enabled = true,
12
19
  apiKey = "",
@@ -36,33 +43,43 @@ function createOpenAiCompatibleClient({
36
43
  timeout: normalizeTimeoutMs(timeoutMs)
37
44
  });
38
45
 
39
- return Object.freeze({
40
- enabled: true,
41
- provider: normalizedProvider,
42
- defaultModel: normalizedModel,
43
- async createChatCompletion({ messages = [], tools = [], temperature = 0, signal } = {}) {
44
- const requestPayload = {
46
+ const baseURL = normalizedBaseUrl || undefined;
47
+
48
+ async function createChatCompletion({ messages = [], tools = [], temperature = 0, signal } = {}) {
49
+ const requestPayload = {
50
+ model: normalizedModel,
51
+ messages,
52
+ ...(Array.isArray(tools) && tools.length > 0 ? { tools } : {}),
53
+ temperature,
54
+ stream: false
55
+ };
56
+
57
+ return client.chat.completions.create(requestPayload, signal ? { signal } : undefined);
58
+ }
59
+
60
+ async function createChatCompletionStream({ messages = [], tools = [], signal, temperature = 0.2 } = {}) {
61
+ return client.chat.completions.create(
62
+ {
45
63
  model: normalizedModel,
46
64
  messages,
47
- ...(Array.isArray(tools) && tools.length > 0 ? { tools } : {}),
65
+ tools,
48
66
  temperature,
49
- stream: false
50
- };
67
+ stream: true
68
+ },
69
+ signal ? { signal } : undefined
70
+ );
71
+ }
51
72
 
52
- return client.chat.completions.create(requestPayload, signal ? { signal } : undefined);
53
- },
54
- async createChatCompletionStream({ messages = [], tools = [], signal, temperature = 0.2 } = {}) {
55
- return client.chat.completions.create(
56
- {
57
- model: normalizedModel,
58
- messages,
59
- tools,
60
- temperature,
61
- stream: true
62
- },
63
- signal ? { signal } : undefined
64
- );
65
- }
73
+ return Object.freeze({
74
+ enabled: true,
75
+ provider: normalizedProvider,
76
+ defaultModel: normalizedModel,
77
+ // The same endpoint as an AI SDK model, for the agent engine's loop.
78
+ model: normalizedProvider === "deepseek"
79
+ ? chatModel({ apiKey: normalizedApiKey, model: normalizedModel, baseURL })
80
+ : createOpenAI({ apiKey: normalizedApiKey, baseURL }).chat(normalizedModel),
81
+ createChatCompletion,
82
+ createChatCompletionStream
66
83
  });
67
84
  }
68
85
 
@@ -1,16 +1,13 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
1
2
  import { requireAuth } from "@jskit-ai/kernel/server/runtime";
2
3
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
4
  import { normalizePositiveInteger, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
- import {
5
- resolveSchemaTransportSchemaDefinition,
6
- validateSchemaPayload
7
- } from "@jskit-ai/kernel/shared/validators";
5
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
8
6
  import { resolveWorkspaceSlug } from "./resolveWorkspaceSlug.js";
7
+ import { AUTOMATION_CHANNEL, normalizePermissionSpec, resolveActionToolEntries } from "./serviceToolEntries.js";
9
8
 
10
- const AUTOMATION_CHANNEL = "automation";
11
9
  const DEFAULT_MAX_DIRECT_TOOLS = 32;
12
10
  const MAX_ALWAYS_AVAILABLE_TOOLS = 8;
13
- const MAX_PREFLIGHT_INTENTS_PER_TOOL = 8;
14
11
  const DEFAULT_DISCOVERY_PAGE_SIZE = 10;
15
12
  const MAX_DISCOVERY_PAGE_SIZE = 20;
16
13
  const DEFAULT_MAX_TOOL_ARGUMENT_BYTES = 32 * 1024;
@@ -143,122 +140,6 @@ const DISCOVERY_TOOL_DESCRIPTORS = Object.freeze([
143
140
  })
144
141
  ]);
145
142
 
146
- function normalizePreflightIntents(value) {
147
- const source = Array.isArray(value) ? value : [value];
148
- const normalized = source
149
- .map((entry) => normalizeText(entry).toLowerCase())
150
- .filter(Boolean);
151
- return Object.freeze([...new Set(normalized)].slice(0, MAX_PREFLIGHT_INTENTS_PER_TOOL));
152
- }
153
-
154
- function normalizeAssistantExtension(value) {
155
- const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
156
- if (source.transformResult != null && typeof source.transformResult !== "function") {
157
- throw new TypeError("extensions.assistant.transformResult must be a function when provided.");
158
- }
159
-
160
- return Object.freeze({
161
- description: normalizeText(source.description),
162
- exclude: normalizeText(source.exclude),
163
- alwaysAvailable: source.alwaysAvailable === true,
164
- preflight: normalizePreflightIntents(source.preflight),
165
- output: Object.hasOwn(source, "output") ? source.output : null,
166
- transformResult: typeof source.transformResult === "function" ? source.transformResult : null
167
- });
168
- }
169
-
170
- function normalizeAssistantActionExtension(action = {}) {
171
- const source = action && typeof action === "object" && !Array.isArray(action) ? action : {};
172
- const actionId = normalizeText(source.id);
173
-
174
- if (Object.prototype.hasOwnProperty.call(source, "assistantTool")) {
175
- throw new Error(
176
- `Action definition \"${actionId || "<unknown>"}\" assistantTool is not supported. Use extensions.assistant instead.`
177
- );
178
- }
179
-
180
- const extensions = source.extensions && typeof source.extensions === "object" && !Array.isArray(source.extensions)
181
- ? source.extensions
182
- : {};
183
- return normalizeAssistantExtension(extensions.assistant);
184
- }
185
-
186
- function normalizeBarredEntry(value) {
187
- return normalizeText(value).toLowerCase();
188
- }
189
-
190
- function normalizeBarredActionSet(value) {
191
- const source = Array.isArray(value) ? value : [value];
192
- const exact = new Set();
193
- const prefixes = [];
194
-
195
- for (const entry of source) {
196
- const normalized = normalizeBarredEntry(entry);
197
- if (!normalized) {
198
- continue;
199
- }
200
-
201
- if (normalized.endsWith(".*")) {
202
- const prefix = normalized.slice(0, -1);
203
- if (prefix) {
204
- prefixes.push(prefix);
205
- }
206
- continue;
207
- }
208
-
209
- exact.add(normalized);
210
- }
211
-
212
- return Object.freeze({
213
- exact,
214
- prefixes: Object.freeze(prefixes)
215
- });
216
- }
217
-
218
- function isActionBarred(barredRules, actionId) {
219
- const normalizedActionId = normalizeText(actionId).toLowerCase();
220
- if (!normalizedActionId) {
221
- return true;
222
- }
223
-
224
- if (barredRules.exact.has(normalizedActionId)) {
225
- return true;
226
- }
227
-
228
- return barredRules.prefixes.some((prefix) => normalizedActionId.startsWith(prefix));
229
- }
230
-
231
- function sanitizeToolName(value) {
232
- const normalized = String(value || "")
233
- .trim()
234
- .toLowerCase()
235
- .replace(/[^a-z0-9_]+/g, "_")
236
- .replace(/_+/g, "_")
237
- .replace(/^_+|_+$/g, "");
238
-
239
- if (!normalized) {
240
- return "tool";
241
- }
242
-
243
- return normalized;
244
- }
245
-
246
- function resolveUniqueToolName(baseName, used) {
247
- const normalizedBase = sanitizeToolName(baseName) || "tool";
248
- let candidate = normalizedBase.slice(0, 64);
249
- let suffix = 1;
250
-
251
- while (used.has(candidate)) {
252
- const suffixText = `_${suffix}`;
253
- const baseBudget = Math.max(1, 64 - suffixText.length);
254
- candidate = `${normalizedBase.slice(0, baseBudget)}${suffixText}`;
255
- suffix += 1;
256
- }
257
-
258
- used.add(candidate);
259
- return candidate;
260
- }
261
-
262
143
  // Empty arguments mean "no input"; anything else must be one JSON object, so a
263
144
  // truncated or malformed call fails instead of running the action with {}.
264
145
  function parseToolPayload(argumentsText) {
@@ -293,11 +174,7 @@ function serializedByteLength(value) {
293
174
  }
294
175
 
295
176
  function createToolError(status, code, message) {
296
- const error = new Error(message);
297
- error.status = status;
298
- error.statusCode = status;
299
- error.code = code;
300
- return error;
177
+ return new AppError(status, message, { code });
301
178
  }
302
179
 
303
180
  function ensureSerializedSize(value, maxBytes, { code = "assistant_tool_result_too_large", label = "Tool result" } = {}) {
@@ -411,21 +288,6 @@ function canInvokeMethod(permission, context) {
411
288
  }
412
289
  }
413
290
 
414
- function normalizePermissionSpec(permission) {
415
- const source = permission && typeof permission === "object" && !Array.isArray(permission)
416
- ? permission
417
- : {};
418
- const requireMode = normalizeText(source.require || "none").toLowerCase();
419
- const permissions = Array.isArray(source.permissions) ? source.permissions : [];
420
-
421
- return Object.freeze({
422
- require: requireMode || "none",
423
- permissions,
424
- message: normalizeText(source.message),
425
- code: normalizeText(source.code)
426
- });
427
- }
428
-
429
291
  function stripWorkspaceSlugFromSchema(schema, context = {}) {
430
292
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
431
293
  return schema;
@@ -457,31 +319,13 @@ function stripWorkspaceSlugFromSchema(schema, context = {}) {
457
319
  };
458
320
  }
459
321
 
460
- function hasAutomationChannel(action = {}) {
461
- const channels = Array.isArray(action.channels) ? action.channels : [];
462
- return channels.some((channel) => normalizeText(channel).toLowerCase() === AUTOMATION_CHANNEL);
463
- }
464
-
465
- function normalizeSurfaceList(value) {
466
- const source = Array.isArray(value) ? value : [value];
467
- const resolved = [];
468
-
469
- for (const entry of source) {
470
- const normalized = normalizeSurfaceId(entry);
471
- if (normalized) {
472
- resolved.push(normalized);
473
- }
474
- }
475
-
476
- return Object.freeze(resolved);
477
- }
478
-
479
322
  function canUseToolOnSurface(entry = {}, context = {}) {
480
323
  const toolSurfaces = Array.isArray(entry.surfaces) ? entry.surfaces : [];
481
324
  if (toolSurfaces.length < 1) {
482
325
  return true;
483
326
  }
484
327
 
328
+ // readable-check: allow no-optional-after-default - input validation: a caller may pass a null context
485
329
  const contextSurfaceId = normalizeSurfaceId(context?.surface);
486
330
  if (!contextSurfaceId) {
487
331
  return true;
@@ -490,125 +334,22 @@ function canUseToolOnSurface(entry = {}, context = {}) {
490
334
  return toolSurfaces.includes("*") || toolSurfaces.includes(contextSurfaceId);
491
335
  }
492
336
 
493
- function resolveActionBackedToolEntries(actions) {
494
- if (!actions || typeof actions.listDefinitions !== "function") {
495
- throw new TypeError("Assistant tool catalog requires runtime.actions.");
496
- }
497
- const entriesByActionId = new Map();
498
- for (const action of actions.listDefinitions()) {
499
- if (!action || typeof action !== "object") {
500
- continue;
501
- }
502
- if (!hasAutomationChannel(action)) {
503
- continue;
504
- }
505
-
506
- const actionId = normalizeText(action.id);
507
- if (!actionId) {
508
- continue;
509
- }
510
-
511
- let assistantExtension = null;
512
- try {
513
- assistantExtension = normalizeAssistantActionExtension(action);
514
- } catch {
515
- continue;
516
- }
517
-
518
- if (assistantExtension.exclude) {
519
- continue;
520
- }
521
-
522
- const outputDefinition = assistantExtension.output || action.output;
523
- const inputSchema = resolveSchemaTransportSchemaDefinition(action.input, {
524
- context: `Action definition "${actionId}" input`,
525
- defaultMode: "patch"
526
- }) || null;
527
- const outputSchema = resolveSchemaTransportSchemaDefinition(outputDefinition, {
528
- context: `Action definition "${actionId}" assistant output`,
529
- defaultMode: "replace"
530
- }) || null;
531
- if (!inputSchema || !outputSchema) {
532
- continue;
533
- }
534
-
535
- const actionVersion = Number(action.version) || 1;
536
- const actionKey = actionId.toLowerCase();
537
- const nextEntry = Object.freeze({
538
- actionId,
539
- actionVersion,
540
- kind: normalizeText(action.kind).toLowerCase() || "command",
541
- toolBaseName: actionId,
542
- description: assistantExtension.description || `Run ${actionId}.`,
543
- alwaysAvailable: assistantExtension.alwaysAvailable,
544
- preflight: assistantExtension.preflight,
545
- inputSchema,
546
- outputDefinition,
547
- outputSchema,
548
- transformResult: assistantExtension.transformResult,
549
- permission: normalizePermissionSpec(action.permission),
550
- surfaces: normalizeSurfaceList(action.surfaces)
551
- });
552
- const existing = entriesByActionId.get(actionKey);
553
- if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
554
- entriesByActionId.set(actionKey, nextEntry);
555
- }
556
- }
557
-
558
- return entriesByActionId;
559
- }
560
-
561
- function resolveActionToolEntries(
562
- actions,
563
- { barredActionIds = [], skipActionPrefixes = [] } = {}
564
- ) {
565
- const actionBackedEntries = resolveActionBackedToolEntries(actions);
566
- const barredRules = normalizeBarredActionSet(barredActionIds);
567
- const usedToolNames = new Set();
568
- const entries = [];
569
-
570
- for (const actionEntry of actionBackedEntries.values()) {
571
- const actionId = normalizeText(actionEntry?.actionId);
572
- if (!actionId) {
573
- continue;
574
- }
575
-
576
- const normalizedActionId = actionId.toLowerCase();
577
- const skipByPrefix = skipActionPrefixes.some((prefix) => normalizedActionId.startsWith(prefix));
578
- if (skipByPrefix) {
579
- continue;
580
- }
581
-
582
- if (isActionBarred(barredRules, actionId)) {
583
- continue;
584
- }
585
-
586
- const toolName = resolveUniqueToolName(actionEntry.toolBaseName, usedToolNames);
587
- entries.push(
588
- Object.freeze({
589
- descriptor: Object.freeze({
590
- name: toolName,
591
- actionId,
592
- actionVersion: Number(actionEntry.actionVersion) || null,
593
- description: normalizeText(actionEntry.description) || `Run ${actionId}.`,
594
- alwaysAvailable: actionEntry.alwaysAvailable === true,
595
- preflight: actionEntry.preflight,
596
- parameters: actionEntry.inputSchema,
597
- outputSchema: actionEntry.outputSchema
598
- }),
599
- kind: actionEntry.kind,
600
- acceptsWorkspaceSlug: Object.hasOwn(actionEntry.inputSchema.properties || {}, "workspaceSlug"),
601
- outputDefinition: actionEntry.outputDefinition,
602
- transformResult: actionEntry.transformResult,
603
- permission: actionEntry.permission,
604
- surfaces: actionEntry.surfaces
605
- })
606
- );
607
- }
608
-
609
- return Object.freeze(entries.sort((left, right) => left.descriptor.name.localeCompare(right.descriptor.name)));
610
- }
611
-
337
+ // A tool call whose caller does not watch its failures.
338
+ function reportNoToolFailure() {}
339
+
340
+ /**
341
+ * The assistant tools an application's actions offer one caller: direct tools, or discovery
342
+ * tools when there are too many, each checked against permission, surface and policy.
343
+ * @param {{ listDefinitions(): object[], execute(request: object): Promise<unknown> }} actions The application's action runtime.
344
+ * @param {object} [options]
345
+ * @param {(request: { actionId: string, kind: string, context: object }) => boolean} [options.isActionAvailable]
346
+ * @param {string[] | string} [options.barredActionIds]
347
+ * @param {string[] | string} [options.skipActionPrefixes]
348
+ * @param {number} [options.maxDirectTools]
349
+ * @param {number} [options.discoveryPageSize]
350
+ * @param {number} [options.maxToolArgumentBytes]
351
+ * @param {number} [options.maxToolResultBytes]
352
+ */
612
353
  function createServiceToolCatalog(
613
354
  actions,
614
355
  {
@@ -621,9 +362,11 @@ function createServiceToolCatalog(
621
362
  maxToolResultBytes: rawMaxToolResultBytes = DEFAULT_MAX_TOOL_RESULT_BYTES
622
363
  } = {}
623
364
  ) {
365
+ // readable-check: allow method-probe - input validation: the application passes its action runtime
624
366
  if (!actions || typeof actions.listDefinitions !== "function" || typeof actions.execute !== "function") {
625
367
  throw new TypeError("createServiceToolCatalog requires runtime.actions.");
626
368
  }
369
+ // readable-check: allow method-probe - input validation: an optional action policy must be a function
627
370
  if (isActionAvailable !== undefined && typeof isActionAvailable !== "function") {
628
371
  throw new TypeError("isActionAvailable must be a synchronous action policy.");
629
372
  }
@@ -875,7 +618,7 @@ function createServiceToolCatalog(
875
618
  };
876
619
  }
877
620
 
878
- async function executeToolCall({ toolName = "", argumentsText = "", context = {}, toolSet = null, onFailure } = {}) {
621
+ async function executeToolCall({ toolName = "", argumentsText = "", context = {}, toolSet = null, onFailure = reportNoToolFailure } = {}) {
879
622
  const normalizedToolName = normalizeText(toolName);
880
623
  const suppliedState = toolSet && typeof toolSet === "object" ? toolSetStates.get(toolSet) : null;
881
624
  const resolvedToolSet = suppliedState ? toolSet : resolveToolSet(context);
@@ -920,13 +663,16 @@ function createServiceToolCatalog(
920
663
  result
921
664
  };
922
665
  } catch (error) {
923
- onFailure?.(error);
666
+ onFailure(error);
924
667
  return resolveToolFailure(error);
925
668
  }
926
669
  }
927
670
 
928
671
  return Object.freeze({
929
- limits: Object.freeze({ maxToolArgumentBytes, maxToolResultBytes }),
672
+ limits: Object.freeze({
673
+ maxToolArgumentBytes,
674
+ maxToolResultBytes
675
+ }),
930
676
  resolveToolSet,
931
677
  toOpenAiToolSchema,
932
678
  executeToolCall