@jskit-ai/assistant-core 0.2.7 → 0.2.9

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
@@ -9,6 +9,20 @@ import { createAssistantActions } from "../../assistant-runtime/src/server/actio
9
9
  import { accountSettingsResponseFormatter } from "../../users-core/src/server/common/formatters/accountSettingsResponseFormatter.js";
10
10
  import { DEFAULT_USER_SETTINGS } from "../../users-core/src/shared/settings.js";
11
11
 
12
+ // Each action's definition with a run(service, input, context) that executes
13
+ // it against the service a test supplies.
14
+ function specificationsOf(createActions) {
15
+ let service = null;
16
+ const forwarding = new Proxy({}, { get: (_target, method) => (...args) => service[method](...args) });
17
+ return createActions(new Proxy({}, { get: () => forwarding })).map(({ execute, ...definition }) => ({
18
+ ...definition,
19
+ run(nextService, input, context) {
20
+ service = nextService;
21
+ return execute(input, context);
22
+ }
23
+ }));
24
+ }
25
+
12
26
  const specifications = [];
13
27
  for (const owner of ["users-core", "workspaces-core", "console-core"]) {
14
28
  const directory = new URL(`../../${owner}/src/server/`, import.meta.url);
@@ -16,7 +30,7 @@ for (const owner of ["users-core", "workspaces-core", "console-core"]) {
16
30
  if (!path.endsWith("Actions.js")) continue;
17
31
  const module = await import(new URL(path, directory));
18
32
  for (const [name, value] of Object.entries(module)) {
19
- if (name.endsWith("ActionSpecifications")) specifications.push(...value);
33
+ if (/^create\w+Actions$/u.test(name)) specifications.push(...specificationsOf(value));
20
34
  }
21
35
  }
22
36
  }
@@ -30,8 +44,9 @@ const roleCatalog = { collaborationEnabled: true, defaultInviteRole: "member", r
30
44
  const memberResult = { workspace, members: [], roleCatalog };
31
45
  const invite = { id: "31", email: "invite@example.test", roleSid: "member", status: "pending", expiresAt: "2026-10-01" };
32
46
  const invitesResult = { workspace, invites: [invite], roleCatalog };
47
+ // An auth service without the optional settings-profile operation: email is managed by auth.
33
48
  const settings = accountSettingsResponseFormatter({
34
- profile: { id: "7", displayName: "Alice", email: "alice@example.test" }, settings: DEFAULT_USER_SETTINGS
49
+ authService: {}, profile: { id: "7", displayName: "Alice", email: "alice@example.test" }, settings: DEFAULT_USER_SETTINGS
35
50
  });
36
51
  const workspaceSettings = {
37
52
  workspace: { id: "11", slug: "acme", ownerUserId: "7" }, roleCatalog,
@@ -49,24 +64,24 @@ const fixtures = {
49
64
  "settings.security.password_method.toggle": [{ enabled: true }, returnJsonApiData({ securityStatus: settings.security, settings, session: "SECRET" })],
50
65
  "settings.security.oauth.unlink": [{ provider: "google" }, returnJsonApiData({ securityStatus: settings.security, providerMessageId: "SECRET" })],
51
66
  "settings.security.sessions.logout_others": [{}, null],
52
- "workspace.workspaces.create": [{ name: "Acme" }, { ...workspace, isPersonal: false, createdAt: "2026-09-13", updatedAt: "2026-09-13", deletedAt: null }],
53
- "workspace.workspaces.list": [{}, [{ ...workspace, roleSid: "owner", isAccessible: true, membershipStatus: "active", createdAt: "2026-09-13" }]],
54
- "workspace.workspaces.read": [{ workspaceSlug: "evil" }, { ...workspace, isPersonal: false, createdAt: "2026-09-13" }],
55
- "workspace.workspaces.update": [{ workspaceSlug: "evil", name: "Acme" }, workspace],
56
- "workspace.settings.read": [{ workspaceSlug: "evil" }, workspaceSettings],
57
- "workspace.settings.update": [{ workspaceSlug: "evil", invitesEnabled: false }, workspaceSettings],
58
- "workspace.roles.list": [{ workspaceSlug: "evil" }, roleCatalog],
59
- "workspace.members.list": [{ workspaceSlug: "evil" }, memberResult],
60
- "workspace.member.role.update": [{ workspaceSlug: "evil", memberUserId: "8", roleSid: "member" }, memberResult],
61
- "workspace.member.remove": [{ workspaceSlug: "evil", memberUserId: "8" }, memberResult],
62
- "workspace.invites.list": [{ workspaceSlug: "evil" }, invitesResult],
63
- "workspace.invite.create": [{ workspaceSlug: "evil", email: invite.email, roleSid: "member" }, {
67
+ "workspace.workspaces.create": [{ name: "Acme" }, returnJsonApiData({ ...workspace, isPersonal: false, createdAt: "2026-09-13", updatedAt: "2026-09-13", deletedAt: null })],
68
+ "workspace.workspaces.list": [{}, returnJsonApiData({ items: [{ ...workspace, roleSid: "owner", isAccessible: true, membershipStatus: "active", createdAt: "2026-09-13" }], nextCursor: null })],
69
+ "workspace.workspaces.read": [{ workspaceSlug: "evil" }, returnJsonApiData({ ...workspace, isPersonal: false, createdAt: "2026-09-13" })],
70
+ "workspace.workspaces.update": [{ workspaceSlug: "evil", name: "Acme" }, returnJsonApiData(workspace)],
71
+ "workspace.settings.read": [{ workspaceSlug: "evil" }, returnJsonApiData(workspaceSettings)],
72
+ "workspace.settings.update": [{ workspaceSlug: "evil", invitesEnabled: false }, returnJsonApiData(workspaceSettings)],
73
+ "workspace.roles.list": [{ workspaceSlug: "evil" }, returnJsonApiData(roleCatalog)],
74
+ "workspace.members.list": [{ workspaceSlug: "evil" }, returnJsonApiData(memberResult)],
75
+ "workspace.member.role.update": [{ workspaceSlug: "evil", memberUserId: "8", roleSid: "member" }, returnJsonApiData(memberResult)],
76
+ "workspace.member.remove": [{ workspaceSlug: "evil", memberUserId: "8" }, returnJsonApiData(memberResult)],
77
+ "workspace.invites.list": [{ workspaceSlug: "evil" }, returnJsonApiData(invitesResult)],
78
+ "workspace.invite.create": [{ workspaceSlug: "evil", email: invite.email, roleSid: "member" }, returnJsonApiData({
64
79
  ...invitesResult, createdInviteId: "31", inviteTokenPreview: "SECRET", inviteUrl: "https://example.test/invite?token=SECRET",
65
80
  inviteDelivery: { status: "sent", message: "SECRET", providerMessageId: "SECRET" }
66
- }],
67
- "workspace.invite.revoke": [{ workspaceSlug: "evil", inviteId: "31" }, { ...invitesResult, revokedInviteId: "31" }],
68
- "workspace.invitations.pending.list": [{}, [{ id: "31", workspaceId: "11", workspaceSlug: "acme", workspaceName: "Acme",
69
- workspaceAvatarUrl: "", roleSid: "member", status: "pending", expiresAt: "2026-10-01", token: "SECRET" }]]
81
+ })],
82
+ "workspace.invite.revoke": [{ workspaceSlug: "evil", inviteId: "31" }, returnJsonApiData({ ...invitesResult, revokedInviteId: "31" })],
83
+ "workspace.invitations.pending.list": [{}, returnJsonApiData({ pendingInvites: [{ id: "31", workspaceId: "11", workspaceSlug: "acme", workspaceName: "Acme",
84
+ workspaceAvatarUrl: "", roleSid: "member", status: "pending", expiresAt: "2026-10-01", token: "SECRET" }] })]
70
85
  };
71
86
 
72
87
  function catalogFor(spec, result, observe = () => {}) {
@@ -12,7 +12,7 @@ function host(t) {
12
12
  t.after(() => execution.close());
13
13
  return execution;
14
14
  }
15
- const spec = (code) => ({ command: process.execPath, args: ["-e", code], cwd: process.cwd(), stream: true });
15
+ const spec = (code) => ({ command: process.execPath, args: ["-e", code], cwd: process.cwd(), env: process.env, stream: true });
16
16
 
17
17
  test("the supplied execution host carries native protocol bytes and proves owned cleanup", options, async (t) => {
18
18
  const execution = host(t);
@@ -59,14 +59,14 @@ test("native command requests use the same execution owner and retain bounded ca
59
59
  t.after(() => rm(directory, { recursive: true, force: true }));
60
60
  const captured = await execution.run({ command: process.execPath,
61
61
  args: ["-e", 'process.stdout.write("x".repeat(70000)); process.stderr.write("finished");'],
62
- cwd: directory, mode: "capture" });
62
+ cwd: directory, baseEnv: process.env, mode: "capture" });
63
63
  assert.equal(captured.ok, true);
64
64
  assert.equal(Buffer.byteLength(captured.output), 64 * 1024);
65
65
  assert.ok(captured.output.endsWith("finished"));
66
66
  const logPath = join(directory, "detached.log");
67
67
  const child = await execution.run({ command: process.execPath,
68
68
  args: ["-e", 'process.stderr.write("ready"); setInterval(() => {}, 1000);'],
69
- cwd: directory, mode: "detached", logPath });
69
+ cwd: directory, baseEnv: process.env, mode: "detached", logPath });
70
70
  assert.equal(child.ok, true);
71
71
  assert.ok(Number.isSafeInteger(child.pid) && child.pid > 0);
72
72
  const deadline = Date.now() + 3000;
@@ -83,7 +83,7 @@ test("native capture timeout drains its execution before returning", options, as
83
83
  const execution = host(t);
84
84
  const result = await execution.run({ command: process.execPath,
85
85
  args: ["-e", 'process.stdout.write(String(process.pid)); setInterval(() => {}, 1000);'],
86
- cwd: process.cwd(), mode: "capture", timeout: 500 });
86
+ cwd: process.cwd(), baseEnv: process.env, mode: "capture", timeout: 500 });
87
87
  assert.equal(result.ok, false);
88
88
  assert.equal(result.timedOut, true);
89
89
  assert.throws(() => process.kill(Number(result.output), 0), { code: "ESRCH" });
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { test } from "node:test";
5
- import { createConversationProcessIdentity } from "../src/server/conversation/localExecution.js";
5
+ import { createConversationProcessIdentity } from "../src/server/conversation/processIdentity.js";
6
6
 
7
7
  const TOKEN_ENV = "JSKIT_EXECUTION_RUNTIME_TOKEN";
8
8
  const HASH_ENV = "JSKIT_EXECUTION_COMMAND_HASH";
@@ -1,7 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
- import { createOpenCodeSharedRuntime, openCodeServerForDirectory } from "../src/server/conversation/openCodeRuntime.js";
4
+ import { createOpenCodeSharedRuntime } from "../src/server/conversation/openCodeRuntime.js";
5
+ import { openCodeServerForDirectory } from "../src/server/conversation/openCodeNativeSession.js";
5
6
 
6
7
  // One shared server whose native directories record what OpenCode is asked to do.
7
8
  function sharedServer() {
@@ -31,7 +32,7 @@ function sharedServer() {
31
32
  const deepseek = (fingerprint, apiKey) => ({ modelProviderId: "deepseek", canonicalUrl: "", endpointCode: "native", fingerprint, apiKey });
32
33
 
33
34
  async function startedRuntime(saved) {
34
- const runtime = createOpenCodeSharedRuntime();
35
+ const runtime = createOpenCodeSharedRuntime({});
35
36
  const native = sharedServer();
36
37
  const shared = await runtime.ensure(saved[0], async () => ({ server: native.server, connections: [...saved] }), async () => saved);
37
38
  return { runtime, native, shared };
@@ -4,11 +4,12 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
6
  import { setTimeout as delay } from "node:timers/promises";
7
- import { createOpenCodeSharedRuntime, ensureOpenCodeSession, openCodeServerForDirectory, readOpenCodeEnvironments } from "../src/server/conversation/openCodeRuntime.js";
7
+ import { createOpenCodeSharedRuntime, readOpenCodeEnvironments } from "../src/server/conversation/openCodeRuntime.js";
8
+ import { ensureOpenCodeSession, openCodeServerForDirectory } from "../src/server/conversation/openCodeNativeSession.js";
8
9
  import { createOpenCodeServerClient } from "../src/server/conversation/openCodeClient.js";
9
10
 
10
11
  function fixture(options = {}) {
11
- const runtime = createOpenCodeSharedRuntime();
12
+ const runtime = createOpenCodeSharedRuntime({});
12
13
  const starts = [], stops = [];
13
14
  const connection = { modelProviderId: "provider", canonicalUrl: "https://provider.invalid", fingerprint: "account", endpointCode: "api" };
14
15
  const start = async selected => {
@@ -37,7 +38,7 @@ function fixture(options = {}) {
37
38
  }
38
39
 
39
40
  test("history projection supplies ordered reasoning facts without native part envelopes", async () => {
40
- const runtime = createOpenCodeSharedRuntime();
41
+ const runtime = createOpenCodeSharedRuntime({});
41
42
  const message = { id: "reply", type: "assistant", time: { created: 1_000 }, content: [
42
43
  { type: "tool", id: "tool", text: "Not reasoning" },
43
44
  { type: "reasoning", id: "first", text: " First thought\n", time: { end: 0 }, nativeOnly: true },
@@ -250,7 +251,7 @@ test("deleting a scoped OpenCode conversation returns its scope's exit proof and
250
251
  });
251
252
 
252
253
  test("an active OpenCode turn with missing native identity still fails instead of claiming Stop", async () => {
253
- const runtime = createOpenCodeSharedRuntime();
254
+ const runtime = createOpenCodeSharedRuntime({});
254
255
  const client = createOpenCodeServerClient({ baseUrl: "http://127.0.0.1:1",
255
256
  fetchImpl: async () => assert.fail("Invalid identity must fail before transport.") });
256
257
  const turn = { active: true, state: "inProgress" };
@@ -447,7 +448,7 @@ test("OpenCode bindings share atomic publication without overwriting another con
447
448
  const root = await mkdtemp(path.join(os.tmpdir(), "jskit-opencode-bindings-"));
448
449
  t.after(() => rm(root, { force: true, recursive: true }));
449
450
  const registry = path.join(root, "private", "session-environments.json");
450
- const runtime = createOpenCodeSharedRuntime();
451
+ const runtime = createOpenCodeSharedRuntime({});
451
452
  const main = { upstreamSessionId: "main", workdir: "/work/main", env: { OWNER: "main" } };
452
453
  const colleague = { upstreamSessionId: "colleague", workdir: "/work/colleague", systemPrompt: "Current instructions" };
453
454
  await Promise.all([
@@ -467,7 +468,7 @@ test("OpenCode bindings share atomic publication without overwriting another con
467
468
 
468
469
 
469
470
  test("native output identity is opt-in for every existing OpenCode projection writer", async () => {
470
- const runtime = createOpenCodeSharedRuntime();
471
+ const runtime = createOpenCodeSharedRuntime({});
471
472
  for (const configured of [false, true]) {
472
473
  const writes = [];
473
474
  const streams = [];
@@ -545,7 +546,7 @@ async function finalReaderMonitorFixture(t, { publication, checkpoint, recovery
545
546
  }
546
547
 
547
548
  test("native final projection opt-in returns only its original published receipt without changing default fields", async () => {
548
- const runtime = createOpenCodeSharedRuntime();
549
+ const runtime = createOpenCodeSharedRuntime({});
549
550
  const originalTurn = { turnId: "authored", assistant: { messageId: "real-final:assistant",
550
551
  outputId: "real-final:assistant", role: "assistant", text: "Actual final." } };
551
552
  const rows = [{ id: "accepted", type: "user", time: { created: 1 } },
@@ -118,7 +118,7 @@ test("one native plugin routes current instructions, command policy and tools by
118
118
  parameters: { type: "object", properties: { query: { type: "string" } } } } }];
119
119
  process.env.JSKIT_OPENCODE_ENV_REGISTRY = registry;
120
120
  process.env.JSKIT_OPENCODE_TOOL_SCHEMAS = JSON.stringify(schemas);
121
- const owner = createOpenCodeSharedRuntime();
121
+ const owner = createOpenCodeSharedRuntime({});
122
122
  const calls = [];
123
123
  const bindings = [];
124
124
  for (const id of ["first", "second"]) {
@@ -525,7 +525,7 @@ function applicationActions(execute) {
525
525
  const actions = createActionCatalogue();
526
526
  actions.register({ contributorId: "test.tools", domain: "numbers", actions: [{
527
527
  id: "numbers.read", version: 1, kind: "query", channels: ["automation"], surfaces: ["app"],
528
- permission: { require: "all", permissions: ["numbers.read"] }, idempotency: "none",
528
+ permission: { require: "all", permissions: ["numbers.read"] },
529
529
  input: { schema: createSchema({}), mode: "replace" },
530
530
  output: { schema: createSchema({ value: { type: "number", required: true } }), mode: "replace" }, execute
531
531
  }] });
@@ -894,7 +894,7 @@ for (const change of ["model", "effort"]) {
894
894
  if (change === "model") model = "other-model";
895
895
  const selection = { operationId: `fresh-${change}`, expectedSegmentId: before.segmentId, engine: "opencode",
896
896
  configuration: { ...configuration, model, effort: change === "effort" ? "high" : "low" }, retireNative: true };
897
- await assert.rejects(f.conversation.select({ ...selection, retireNative: "yes" }), { code: "conversation_invalid_replacement" });
897
+ await assert.rejects(f.conversation.select({ ...selection, retireNative: "yes" }), { code: "conversation_replacement_request_invalid" });
898
898
  assert.deepEqual(await f.binding(), original);
899
899
  const selected = await f.conversation.select(selection);
900
900
  assert.notEqual(selected.segmentId, before.segmentId);
@@ -1098,7 +1098,8 @@ for (const savedStatus of ["cancelled", "failed", "unknown-tool", "interrupted"]
1098
1098
  // The original standalone fixture above remains byte-for-byte unchanged.
1099
1099
  async function boundToolFixture(t, { runInContext, monitorRunInContext, monitorContext } = {}) {
1100
1100
  const { createOpenCodeConversationServer, openCodeApplicationToolSchemas } = await import("../src/server/conversation/openCodeProcess.js");
1101
- const { createOpenCodeSharedRuntime, ensureOpenCodeSession } = await import("../src/server/conversation/openCodeRuntime.js");
1101
+ const { createOpenCodeSharedRuntime } = await import("../src/server/conversation/openCodeRuntime.js");
1102
+ const { ensureOpenCodeSession } = await import("../src/server/conversation/openCodeNativeSession.js");
1102
1103
  const { observeOpenCodeEvents } = await import("../src/server/conversation/openCodeTurn.js");
1103
1104
  let owner;
1104
1105
  const handles = [];
@@ -40,7 +40,6 @@ function action({
40
40
  permission,
41
41
  input,
42
42
  output,
43
- idempotency: "none",
44
43
  extensions,
45
44
  execute
46
45
  };
@@ -284,8 +283,7 @@ test("large authorized catalogs use compact paged discovery, exact contracts, an
284
283
  input: workspaceInput,
285
284
  extensions: {
286
285
  assistant: {
287
- alwaysAvailable: true,
288
- preflight: ["CURRENT-TIME", "current-time"]
286
+ alwaysAvailable: true
289
287
  }
290
288
  },
291
289
  execute: async (input, context) => {
@@ -327,7 +325,6 @@ test("large authorized catalogs use compact paged discovery, exact contracts, an
327
325
  "demo_clock_read"
328
326
  ]);
329
327
  const clockTool = toolSet.tools.find((entry) => entry.actionId === "demo.clock.read");
330
- assert.deepEqual(clockTool.preflight, ["current-time"]);
331
328
  assert.equal(clockTool.alwaysAvailable, true);
332
329
  assert.equal(clockTool.parameters.required?.includes("workspaceSlug") || false, false);
333
330
  assert.deepEqual(await catalog.executeToolCall({
@@ -489,8 +486,7 @@ test("discovery mode bounds authorized always-available tools", () => {
489
486
  id: `demo.clock-${String(index + 1).padStart(2, "0")}.read`,
490
487
  extensions: {
491
488
  assistant: {
492
- alwaysAvailable: true,
493
- preflight: ["current-time"]
489
+ alwaysAvailable: true
494
490
  }
495
491
  }
496
492
  }));
@@ -499,8 +495,7 @@ test("discovery mode bounds authorized always-available tools", () => {
499
495
  permission: { require: "all", permissions: ["clock.read"] },
500
496
  extensions: {
501
497
  assistant: {
502
- alwaysAvailable: true,
503
- preflight: ["current-time"]
498
+ alwaysAvailable: true
504
499
  }
505
500
  }
506
501
  }));
@@ -1,190 +0,0 @@
1
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
-
3
- function extractTextDelta(deltaContent) {
4
- if (typeof deltaContent === "string") {
5
- return deltaContent;
6
- }
7
-
8
- if (!Array.isArray(deltaContent)) {
9
- return "";
10
- }
11
-
12
- return deltaContent
13
- .map((entry) => {
14
- if (typeof entry === "string") {
15
- return entry;
16
- }
17
- if (!entry || typeof entry !== "object") {
18
- return "";
19
- }
20
- return String(entry.text || "");
21
- })
22
- .join("");
23
- }
24
-
25
- function sanitizeAssistantMessageText(value, { preserveWhitespace = false } = {}) {
26
- let source = String(value || "");
27
- if (!source) {
28
- return "";
29
- }
30
-
31
- const blockPatterns = [
32
- /<[^>\n]*function_calls[^>\n]*>[\s\S]*?(?:<\/[^>\n]*function_calls>|$)/gi,
33
- /<[^>\n]*tool_calls?[^>\n]*>[\s\S]*?(?:<\/[^>\n]*tool_calls?[^>\n]*>|$)/gi,
34
- /<[^>\n]*invoke\b[^>\n]*>[\s\S]*?(?:<\/[^>\n]*invoke>|$)/gi,
35
- /<(?:analysis|reasoning|think)>[\s\S]*?(?:<\/(?:analysis|reasoning|think)>|$)/gi
36
- ];
37
- for (const pattern of blockPatterns) {
38
- source = source.replace(pattern, " ");
39
- }
40
-
41
- const inlineTagPatterns = [
42
- /<[^>\n]*invoke\b[^>]*\/>/gi,
43
- /<\/?[^>\n]*invoke[^>\n]*>/gi,
44
- /<\/?[^>\n]*function_calls[^>\n]*>/gi,
45
- /<\/?[^>\n]*tool_calls?[^>\n]*>/gi,
46
- /<\/?[^>\n]*DSML[^>\n]*>/gi
47
- ];
48
- for (const pattern of inlineTagPatterns) {
49
- source = source.replace(pattern, " ");
50
- }
51
-
52
- if (preserveWhitespace) return source;
53
-
54
- return source
55
- .split(/\r?\n/)
56
- .map((line) => line.trim())
57
- .filter(Boolean)
58
- .join("\n");
59
- }
60
-
61
- function parseDsmlToolCallsFromText(value = "") {
62
- const source = String(value || "");
63
- if (!source) {
64
- return [];
65
- }
66
-
67
- const functionCallsMatch = source.match(
68
- /<[^>\n]*function_calls[^>\n]*>([\s\S]*?)<\/[^>\n]*function_calls>/i
69
- );
70
- if (!functionCallsMatch) {
71
- return [];
72
- }
73
-
74
- const blockText = String(functionCallsMatch[1] || "");
75
- if (!blockText) {
76
- return [];
77
- }
78
-
79
- const calls = [];
80
- const invokePattern = /<[^>\n]*invoke\b([^>]*)>([\s\S]*?)<\/[^>\n]*invoke>/gi;
81
- let match = invokePattern.exec(blockText);
82
- while (match) {
83
- const attributes = String(match[1] || "");
84
- const body = normalizeText(String(match[2] || ""));
85
- const quotedNameMatch =
86
- attributes.match(/\bname\s*=\s*"([^"]+)"/i) || attributes.match(/\bname\s*=\s*'([^']+)'/i);
87
- const bareNameMatch = attributes.match(/\bname\s*=\s*([^\s"'/>]+)/i);
88
- const name = normalizeText(quotedNameMatch?.[1] || bareNameMatch?.[1]);
89
- if (name) {
90
- calls.push({
91
- id: `dsml_tool_call_${calls.length + 1}`,
92
- name,
93
- arguments: body && /^[\[{]/.test(body) ? body : "{}"
94
- });
95
- }
96
-
97
- match = invokePattern.exec(blockText);
98
- }
99
-
100
- return calls;
101
- }
102
-
103
- async function consumeCompletionStream(stream, onText = () => {}, {
104
- signal, limits, preserveWhitespace = false, requireCallIds = false, callIdPrefix = ""
105
- } = {}) {
106
- let assistantText = "";
107
- let finishReason;
108
- const toolCallsByIndex = new Map();
109
-
110
- function checkToolLimits(call, count) {
111
- if (!limits) return;
112
- if (limits.maxToolCalls === 0) throw new Error("This conversation has no configured application tools.");
113
- if (count > limits.maxToolCalls || call.name.length > 256 || call.id.length > 256 ||
114
- Buffer.byteLength(call.arguments) > limits.maxToolArgumentBytes) {
115
- throw new Error("The model exceeded the configured application tool-call limits.");
116
- }
117
- }
118
-
119
- for await (const chunk of stream) {
120
- signal?.throwIfAborted();
121
- const choice = chunk?.choices?.[0] || {};
122
- const delta = choice?.delta || {};
123
- finishReason = choice.finish_reason || finishReason;
124
-
125
- const textDelta = extractTextDelta(delta.content);
126
- if (textDelta) {
127
- assistantText += textDelta;
128
- if (limits && assistantText.length > limits.maxOutputCharacters) {
129
- throw new Error("The assistant exceeded the configured response limit.");
130
- }
131
- // An opening provider tag can be split between chunks. Keep it private
132
- // until its boundary is known; complete internal blocks are stripped below.
133
- const tagStart = assistantText.lastIndexOf("<");
134
- const safeText = tagStart > assistantText.lastIndexOf(">") ? assistantText.slice(0, tagStart) : assistantText;
135
- await onText(sanitizeAssistantMessageText(safeText, { preserveWhitespace }));
136
- }
137
-
138
- const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
139
- for (const partialToolCall of toolCalls) {
140
- const index = Number(partialToolCall?.index || 0);
141
- const existing =
142
- toolCallsByIndex.get(index) ||
143
- {
144
- id: normalizeText(partialToolCall?.id) || (requireCallIds ? "" : `tool_call_${index + 1}`),
145
- name: "",
146
- arguments: ""
147
- };
148
-
149
- if (partialToolCall?.id) {
150
- existing.id = normalizeText(partialToolCall.id) || existing.id;
151
- }
152
- if (partialToolCall?.function?.name) {
153
- existing.name += String(partialToolCall.function.name || "");
154
- }
155
- if (partialToolCall?.function?.arguments) {
156
- existing.arguments += String(partialToolCall.function.arguments || "");
157
- }
158
-
159
- toolCallsByIndex.set(index, existing);
160
- checkToolLimits(existing, toolCallsByIndex.size);
161
- }
162
- }
163
-
164
- let toolCalls = [...toolCallsByIndex.values()].map((toolCall, index) => ({
165
- id: normalizeText(toolCall.id) || (requireCallIds ? "" : `tool_call_${index + 1}`),
166
- name: normalizeText(toolCall.name),
167
- arguments: String(toolCall.arguments || "")
168
- }));
169
-
170
- let toolCallSource = toolCalls.length ? "structured" : "";
171
- if (toolCalls.length < 1) {
172
- const parsedDsmlCalls = parseDsmlToolCallsFromText(assistantText);
173
- if (parsedDsmlCalls.length > 0) {
174
- toolCallSource = "text";
175
- toolCalls = parsedDsmlCalls.map(call => ({ ...call, id: `${callIdPrefix}${call.id}` }));
176
- for (const call of toolCalls) checkToolLimits(call, toolCalls.length);
177
- const visible = sanitizeAssistantMessageText(assistantText, { preserveWhitespace });
178
- assistantText = preserveWhitespace ? visible : normalizeText(visible);
179
- }
180
- }
181
-
182
- return {
183
- assistantText,
184
- toolCalls,
185
- toolCallSource,
186
- finishReason
187
- };
188
- }
189
-
190
- export { consumeCompletionStream, sanitizeAssistantMessageText };