@jskit-ai/assistant-core 0.2.6 → 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 +18 -12
  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
@@ -6,8 +6,8 @@ import { realpath } from "node:fs/promises";
6
6
  import { isDeepStrictEqual } from "node:util";
7
7
  import { bindClaudeConversationAccount, claudeConnectionIdentity, claudeFlagSettings, claudeModelConfiguration, readClaudeCodeAuthStatus } from "../claudeProcess.js";
8
8
  import { nativeAiModel, nativeAiProvider } from "../../../shared/nativeProviders.js";
9
- import { requireClaudeSessionId, listClaudeConversationStorage } from "../claudeHistory.js";
10
- import { createClaudeConversationOwner, claudeNativeMessageId } from "../claudeTurn.js";
9
+ import { claudeNativeMessageId, requireClaudeSessionId, listClaudeConversationStorage } from "../claudeHistory.js";
10
+ import { createClaudeConversationOwner } from "../claudeTurn.js";
11
11
  import { CLAUDE_APPLICATION_TOOL_SERVER } from "../claudeTools.js";
12
12
  import { createLocalConversationExecution } from "../localExecution.js";
13
13
  import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
@@ -29,9 +29,34 @@ const goalCapabilities = Object.freeze({ goals: true, goalBudgets: false,
29
29
  cancel: Object.freeze({ delivery: "message", interruptsTurn: true })
30
30
  }) });
31
31
 
32
- /** The ordinary native host uses the CLI's own login; no application launch callbacks. */
33
- export function createClaudeConversationDriver({ connections, host = {}, limits = {} } = {}) {
34
- const env = { ...(host.env || process.env) };
32
+ /** A runtime given no host runs Claude in the process's working directory, with local execution and the `claude` command. */
33
+ const LOCAL_HOST = Object.freeze({});
34
+
35
+ /**
36
+ * The ordinary native host uses the CLI's own login; no application launch callbacks.
37
+ * @param {{
38
+ * connections?: { resolve(request: { context: object, integrationId: string }): Promise<object> },
39
+ * host?: {
40
+ * env?: import("@jskit-ai/kernel/server/types").RuntimeEnv,
41
+ * workdir?: string,
42
+ * execution?: import("../localExecution.js").ConversationExecution,
43
+ * nativeTools?: boolean,
44
+ * allowAccountChange?: boolean,
45
+ * commands?: { claude?: string },
46
+ * limits?: object,
47
+ * commandWrapper?: object
48
+ * },
49
+ * limits?: Record<string, number>
50
+ * }} options
51
+ */
52
+ export function createClaudeConversationDriver({
53
+ connections,
54
+ host = LOCAL_HOST,
55
+ limits = {}
56
+ }) {
57
+ // A host that opens its own conversations may pass no environment; the
58
+ // driver's own conversations require it when they open.
59
+ const env = { ...host.env };
35
60
  const workdir = path.resolve(host.workdir || process.cwd());
36
61
  const configRoot = path.resolve(workdir, env.CLAUDE_CONFIG_DIR || path.join(env.HOME || homedir(), ".claude"));
37
62
  if (env.CLAUDE_CONFIG_DIR) env.CLAUDE_CONFIG_DIR = configRoot;
@@ -53,6 +78,7 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
53
78
 
54
79
  async function captureAccountStatus({ command, args, cwd, baseEnv, signal, timeout, maxBuffer }) {
55
80
  signal.throwIfAborted();
81
+ // readable-check: allow method-probe - optional operation: an application's execution host that captures commands itself supplies run
56
82
  if (typeof host.execution?.run === "function") {
57
83
  return host.execution.run({ command, args, cwd, baseEnv, signal, timeout, maxBuffer,
58
84
  mode: "capture", limits: host.limits });
@@ -154,6 +180,7 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
154
180
  }
155
181
  const current = entry.command;
156
182
  if (!current) return;
183
+ // readable-check: allow method-probe - optional operation: a command with application tools observes native tool use
157
184
  if (event.type === "provider-event" && typeof current.onNativeToolUse === "function" && event.event.type === "assistant") {
158
185
  const prefix = `mcp__${CLAUDE_APPLICATION_TOOL_SERVER}__`;
159
186
  for (const block of event.event.message?.content || []) {
@@ -237,259 +264,307 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
237
264
  return native.owner.closeSession(prepared.context, prepared.application);
238
265
  }
239
266
 
240
- return Object.freeze({
241
- admissionBeforeDispatch: false,
242
- toolDiscovery: true,
243
- attachmentTypes: ["text", "image"],
244
- capabilities: Object.freeze({ streaming: true, cancellation: true, instructions: true,
245
- configuration: true, history: true, steering: true, ...goalCapabilities,
246
- attachments: true, tools: true, nativeTools, structuredOutput: true }),
247
- validateConfiguration(configuration) {
248
- validateConversationConfiguration(configuration, { engine: "Claude", connections, efforts: ["low", "medium", "high", "xhigh", "max"],
249
- structuredOutput: true, maxOutputCharacters: maximumOutput });
250
- },
251
- async readState({ conversation, context, representation }) {
252
- let nativeResult = null;
253
- if (representation === "native" || context) {
254
- const { owner } = conversation.native;
255
- const entry = await owner.acquire(context);
256
- nativeResult = { ok: true, thread: { id: entry.id }, turn: owner.snapshot(entry), workdir: entry.context.workdir };
267
+ function validateConfiguration(configuration) {
268
+ validateConversationConfiguration(configuration, { engine: "Claude", connections, efforts: ["low", "medium", "high", "xhigh", "max"],
269
+ structuredOutput: true, maxOutputCharacters: maximumOutput });
270
+ }
271
+
272
+ async function readState({ conversation, context, representation }) {
273
+ let nativeResult = null;
274
+ if (representation === "native" || context) {
275
+ const { owner } = conversation.native;
276
+ const entry = await owner.acquire(context);
277
+ nativeResult = { ok: true, thread: { id: entry.id }, turn: owner.snapshot(entry), workdir: entry.context.workdir };
278
+ }
279
+ return conversation.read(context, representation, nativeResult);
280
+ }
281
+
282
+ function inspectTemporaryActivity({ native, context }) {
283
+ return native.owner.hasActiveTemporaryConversation(context);
284
+ }
285
+
286
+ function releaseRenewalPredecessorProcessExitProof({ native, context }) {
287
+ return native.owner.releaseSession(context, native.application);
288
+ }
289
+
290
+ function releaseRenewalSuccessorProcessExitProof({ native, context }) {
291
+ return native.owner.releaseSession(context, native.application);
292
+ }
293
+
294
+ async function generateRenewalHandover({ native, input, context }) {
295
+ const prepared = native.preparation.handover(input, context);
296
+ const entry = await native.owner.acquire(context);
297
+ const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
298
+ return prepared.completeResult(result);
299
+ }
300
+
301
+ async function seedRenewalHandover({ native, input, context }) {
302
+ const prepared = native.preparation.seed(input, context);
303
+ const entry = await native.owner.acquire(context);
304
+ const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
305
+ return prepared.completeResult(result);
306
+ }
307
+
308
+ async function ensureConversation({ native, context }) {
309
+ const entry = await native.owner.acquire(context);
310
+ return native.owner.ensureReady(entry);
311
+ }
312
+
313
+ async function updateConversationSettings({ native, context, input }) {
314
+ const entry = await native.owner.acquire(context, input.threadId);
315
+ if (entry.process) await native.owner.ensureProcess(entry);
316
+ return { ok: true };
317
+ }
318
+
319
+ async function interruptDetachedConversation({ native, input, context }) {
320
+ const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
321
+ { cleanupExecutionId: input.cleanupExecutionId });
322
+ return native.owner.stopConversation(entry);
323
+ }
324
+
325
+ async function deleteDetachedConversation({ native, input, context }) {
326
+ const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
327
+ { operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId });
328
+ return native.owner.deleteConversation(entry);
329
+ }
330
+
331
+ function listConversationStorage({ native, binding }) {
332
+ return listClaudeConversationStorage({ configRoot: native.configRoot, binding });
333
+ }
334
+
335
+ function retireConversationHistory({ native, binding, context }) {
336
+ return native.owner.retireConversationHistory(native.preparation.retirement(binding, context));
337
+ }
338
+
339
+ function closeProject({ native, input }) {
340
+ return native.owner.closeProject(input, native.application);
341
+ }
342
+
343
+ function invalidateRuntimes({ native, input }) {
344
+ return native.owner.invalidateRuntimes(input, native.application, native.account);
345
+ }
346
+
347
+ function reconcileSessions({ native, sessions, options }) {
348
+ return native.owner.reconcileSessions(sessions, options, native.application);
349
+ }
350
+
351
+ /**
352
+ * Creates a temporary native conversation through the application's owner.
353
+ * @param {{ native: { owner: object }, input: object, context: object }} request
354
+ */
355
+ async function createConversation({ native, input, context }) {
356
+ return native.owner.createConversation(input, { context, acquire: native.owner.acquire });
357
+ }
358
+
359
+ async function runDetachedConversation({ native, input, context }) {
360
+ const conversationId = input.conversationId || input.threadId || randomUUID();
361
+ const created = !input.conversationId && !input.threadId;
362
+ const { owner } = native;
363
+ const entry = await owner.acquire(context, conversationId, { create: created });
364
+ const executionProfile = native.executionProfile;
365
+ if (created && context.assistantScope) entry.profile = executionProfile;
366
+ // readable-check: allow optional-call - optional operation: a native context that observes execution profiles supplies onEvent
367
+ if (executionProfile) await context.onEvent?.({ type: "execution-profile", executionProfile });
368
+ const request = { ...input, conversationId };
369
+ await owner.startTurn(await owner.acquire(context, conversationId, { operation: "start", input: request }), request);
370
+ const result = await owner.wait(await owner.acquire(context, conversationId), request, { context, acquire: owner.acquire });
371
+ return executionProfile ? { ...result, executionProfile } : result;
372
+ }
373
+
374
+ async function createBinding() {
375
+ return { conversationId: randomUUID(), workdir: await realpath(workdir), configRoot, executionId: "", sent: false };
376
+ }
377
+
378
+ async function open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
379
+ const supplied = conversation?.native;
380
+ const scoped = supplied?.scoped;
381
+ let owner = supplied?.owner;
382
+ let entry;
383
+ if (!supplied) {
384
+ if (!host.env) throw new TypeError("A Claude conversation's host requires the application's environment, such as process.env.");
385
+ requireClaudeSessionId(binding?.conversationId);
386
+ const scopeWorkdir = await realpath(workdir);
387
+ // A binding with a scope workdir keeps native history in the credential
388
+ // home. Both paths must still come from the current authorized host.
389
+ const nativeWorkdir = Object.hasOwn(binding, "scopeWorkdir")
390
+ ? await realpath(env.HOME || homedir()) : scopeWorkdir;
391
+ if (binding.workdir !== nativeWorkdir || binding.configRoot !== configRoot ||
392
+ (Object.hasOwn(binding, "scopeWorkdir") && binding.scopeWorkdir !== scopeWorkdir)) {
393
+ throw new Error("This Claude conversation belongs to another working directory or credential home. Restore its original host configuration.");
257
394
  }
258
- return conversation.read(context, representation, nativeResult);
259
- },
260
- inspectTemporaryActivity({ native, context }) {
261
- return native.owner.hasActiveTemporaryConversation(context);
262
- },
263
- releaseRenewalPredecessorProcessExitProof({ native, context }) {
264
- return native.owner.releaseSession(context, native.application);
265
- },
266
- releaseRenewalSuccessorProcessExitProof({ native, context }) {
267
- return native.owner.releaseSession(context, native.application);
268
- },
269
- async generateRenewalHandover({ native, input, context }) {
270
- const prepared = native.preparation.handover(input, context);
271
- const entry = await native.owner.acquire(context);
272
- const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
273
- return prepared.completeResult(result);
274
- },
275
- async seedRenewalHandover({ native, input, context }) {
276
- const prepared = native.preparation.seed(input, context);
277
- const entry = await native.owner.acquire(context);
278
- const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
279
- return prepared.completeResult(result);
280
- },
281
- async ensureConversation({ native, context }) {
282
- const entry = await native.owner.acquire(context);
283
- return native.owner.ensureReady(entry);
284
- },
285
- async updateConversationSettings({ native, context, input }) {
286
- const entry = await native.owner.acquire(context, input.threadId);
287
- if (entry.process) await native.owner.ensureProcess(entry);
288
- return { ok: true };
289
- },
290
- disposeNative,
291
- async interruptDetachedConversation({ native, input, context }) {
292
- const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
293
- { cleanupExecutionId: input.cleanupExecutionId });
294
- return native.owner.stopConversation(entry);
295
- },
296
- async deleteDetachedConversation({ native, input, context }) {
297
- const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
298
- { operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId });
299
- return native.owner.deleteConversation(entry);
300
- },
301
- listConversationStorage({ native, binding }) {
302
- return listClaudeConversationStorage({ configRoot: native.configRoot, binding });
303
- },
304
- retireConversationHistory({ native, binding, context }) {
305
- return native.owner.retireConversationHistory(native.preparation.retirement(binding, context));
306
- },
307
- closeProject({ native, input }) {
308
- return native.owner.closeProject(input, native.application);
309
- },
310
- invalidateRuntimes({ native, input }) {
311
- return native.owner.invalidateRuntimes(input, native.application, native.account);
312
- },
313
- reconcileSessions({ native, sessions, options }) {
314
- return native.owner.reconcileSessions(sessions, options, native.application);
315
- },
316
- async createConversation({ native, input, context }) {
317
- return native.owner.createConversation(input, { context, acquire: native.owner.acquire });
318
- },
319
- async runDetachedConversation({ native, input, context }) {
320
- const conversationId = input.conversationId || input.threadId || randomUUID();
321
- const created = !input.conversationId && !input.threadId;
322
- const { owner } = native;
323
- const entry = await owner.acquire(context, conversationId, { create: created });
324
- const executionProfile = native.executionProfile;
325
- if (created && context.assistantScope) entry.profile = executionProfile;
326
- if (executionProfile) await context.onEvent?.({ type: "execution-profile", executionProfile });
327
- const request = { ...input, conversationId };
328
- await owner.startTurn(await owner.acquire(context, conversationId, { operation: "start", input: request }), request);
329
- const result = await owner.wait(await owner.acquire(context, conversationId), request, { context, acquire: owner.acquire });
330
- return executionProfile ? { ...result, executionProfile } : result;
331
- },
332
- async createBinding() {
333
- return { conversationId: randomUUID(), workdir: await realpath(workdir), configRoot, executionId: "", sent: false };
334
- },
335
- async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
336
- const supplied = conversation?.native;
337
- const scoped = supplied?.scoped;
338
- let owner = supplied?.owner;
339
- let entry;
340
- if (!supplied) {
341
- requireClaudeSessionId(binding?.conversationId);
342
- const scopeWorkdir = await realpath(workdir);
343
- // Original scoped conversations keep native history in the credential
344
- // home. Both paths must still come from the current authorized host.
345
- const nativeWorkdir = Object.hasOwn(binding, "scopeWorkdir")
346
- ? await realpath(env.HOME || homedir()) : scopeWorkdir;
347
- if (binding.workdir !== nativeWorkdir || binding.configRoot !== configRoot ||
348
- (Object.hasOwn(binding, "scopeWorkdir") && binding.scopeWorkdir !== scopeWorkdir)) {
349
- throw new Error("This Claude conversation belongs to another working directory or credential home. Restore its original host configuration.");
395
+ owner = standaloneOwner();
396
+ const context = { key: scopeWorkdir, workdir: binding.workdir, binding, reportFailure,
397
+ async updateBinding(patch) {
398
+ const next = { ...context.binding, ...patch };
399
+ await writeBinding(next);
400
+ context.binding = next;
350
401
  }
351
- owner = standaloneOwner();
352
- const context = { key: scopeWorkdir, workdir: binding.workdir, binding, reportFailure,
353
- async updateBinding(patch) {
354
- const next = { ...context.binding, ...patch };
355
- await writeBinding(next);
356
- context.binding = next;
357
- }
358
- };
359
- entry = await owner.open(context, { conversationId: binding.conversationId, mainId: binding.conversationId, create: true });
360
- // An unknown execution is never reclaimed by PID or silently abandoned.
361
- // Managed hosts can prove cleanup using their durable execution identity.
362
- if (binding.executionId) await entry.nativeTurn.stopProcess("The application reconnected to this conversation.");
363
- }
364
- const acquire = supplied ? owner.acquire : (() => entry);
365
- const control = { current: null, disposed: false, cleanupFailure: null };
366
- const commandOptions = { acquire, reportFailure, entry,
367
- maximumOutput, maximumToolCalls: limits.maxToolCalls ?? 32, timeoutMs: limits.admissionTimeoutMs };
368
- const scopedInput = scopedConversationInput(scoped?.conversationId);
369
- return Object.freeze({
370
- ...(scoped ? { capabilities: SCOPED_CONVERSATION_CAPABILITIES,
371
- async sendNative({ input, context }) {
372
- input = scopedInput(input);
373
- return owner.startTurn(await acquire(context, input.conversationId, { operation: "start", input }), input);
374
- },
375
- async readNative({ input, context }) {
376
- input = scopedInput(input);
377
- return owner.read(await acquire(context, input.conversationId), input);
378
- },
379
- async waitNative({ input, context }) {
380
- input = scopedInput(input);
381
- return owner.wait(await acquire(context, input.conversationId), input, { context, acquire });
382
- }
383
- } : {}),
384
- ...(supplied && !scoped ? {
385
- publishNative: event => conversation.publish({ ...event, goalChanged: event.reason === "claude-goal" })
386
- } : {}),
387
- async readGoal({ configuration, context }) {
388
- if (scoped) return unsupportedScopedConversation();
389
- if (supplied) {
390
- await acquire(context);
391
- const completeResult = supplied.completeResult?.(context);
392
- const nativeResult = await owner.readGoal(await acquire(context));
393
- await completeResult?.(nativeResult, {
394
- segmentId: nativeResult.threadId ? `claude:${nativeResult.threadId}` : null, capabilities: goalCapabilities
395
- });
396
- return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
397
- }
398
- await bindAccount(entry, configuration, context);
399
- return claudeGoalValue(await entry.goals.read(), entry.id);
402
+ };
403
+ entry = await owner.open(context, { conversationId: binding.conversationId, mainId: binding.conversationId, create: true });
404
+ // An unknown execution is never reclaimed by PID or silently abandoned.
405
+ // Managed hosts can prove cleanup using their durable execution identity.
406
+ if (binding.executionId) await entry.nativeTurn.stopProcess("The application reconnected to this conversation.");
407
+ }
408
+ const acquire = supplied ? owner.acquire : (() => entry);
409
+ const control = { current: null, disposed: false, cleanupFailure: null };
410
+ const commandOptions = { acquire, reportFailure, entry,
411
+ maximumOutput, maximumToolCalls: limits.maxToolCalls ?? 32, timeoutMs: limits.admissionTimeoutMs };
412
+ const scopedInput = scopedConversationInput(scoped?.conversationId);
413
+ return Object.freeze({
414
+ ...(scoped ? { capabilities: SCOPED_CONVERSATION_CAPABILITIES,
415
+ async sendNative({ input, context }) {
416
+ input = scopedInput(input);
417
+ return owner.startTurn(await acquire(context, input.conversationId, { operation: "start", input }), input);
400
418
  },
401
- async updateGoal({ configuration, context, input, representation, interrupt, send }) {
402
- if (scoped) return unsupportedScopedConversation();
403
- if (supplied) {
404
- await acquire(context);
405
- const completeResult = supplied.completeResult?.(context);
406
- let command = input;
407
- let facilities = { signal: context?.signal };
408
- if (representation !== "native") {
409
- const current = await owner.readGoal(await acquire(context));
410
- if (input.action !== "set" && claudeGoalValue(current.goal, current.threadId)?.id !== input.expectedGoalId) {
411
- throw new Error("The Claude goal changed. Refresh before trying again.");
412
- }
413
- command = { ...input, threadId: current.threadId,
414
- ...(input.action !== "set" ? { createdAt: current.goal.createdAt, objective: current.goal.objective } : {}) };
415
- facilities = { signal: context?.signal, interrupt, send: command => send(command, { threadId: current.threadId }) };
416
- }
417
- const nativeResult = await owner.updateGoal(await acquire(context), command, facilities);
418
- await completeResult?.(nativeResult);
419
- return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
420
- }
421
- await bindAccount(entry, configuration, context);
422
- const goal = await entry.goals.read();
423
- if (input.action !== "set" && claudeGoalValue(goal, entry.id)?.id !== input.expectedGoalId) {
424
- throw new Error("The Claude goal changed. Refresh before trying again.");
425
- }
426
- await entry.goals.update({ ...input, threadId: entry.id,
427
- ...(input.action !== "set" ? { createdAt: goal?.createdAt, objective: goal?.objective } : {}) }, { interrupt, send, signal: context?.signal });
428
- return claudeGoalValue(await entry.goals.read(), entry.id);
419
+ async readNative({ input, context }) {
420
+ input = scopedInput(input);
421
+ return owner.read(await acquire(context, input.conversationId), input);
429
422
  },
430
- async inspectAdmission({ context, representation, verifyAccount = false, ...input }) {
431
- if (scoped) return unsupportedScopedConversation();
432
- if (supplied) {
433
- await acquire(context);
434
- const result = await owner.inspectAdmission(await acquire(context), input);
435
- return representation === "native" ? result
436
- : { accepted: result.admission === "accepted", notAdmitted: result.admission === "not-admitted" };
437
- }
438
- if (verifyAccount === true) {
439
- const binding = input.receiptBinding;
440
- if (input.threadId !== entry.id || binding?.conversationId !== entry.id ||
441
- binding.workdir !== entry.context.binding.workdir || binding.configRoot !== configRoot ||
442
- Object.hasOwn(binding, "scopeWorkdir") !== Object.hasOwn(entry.context.binding, "scopeWorkdir") ||
443
- binding.scopeWorkdir !== entry.context.binding.scopeWorkdir) {
444
- throw new Error("The completed Claude response belongs to a different native conversation or host scope.");
423
+ async waitNative({ input, context }) {
424
+ input = scopedInput(input);
425
+ return owner.wait(await acquire(context, input.conversationId), input, { context, acquire });
426
+ }
427
+ } : {}),
428
+ ...(supplied && !scoped ? {
429
+ publishNative: event => conversation.publish({ ...event, goalChanged: event.reason === "claude-goal" })
430
+ } : {}),
431
+ async readGoal({ configuration, context }) {
432
+ if (scoped) return unsupportedScopedConversation();
433
+ if (supplied) {
434
+ await acquire(context);
435
+ // readable-check: allow optional-call - optional operation: an application's native conversation may record the goal results it completes
436
+ const completeResult = supplied.completeResult?.(context);
437
+ const nativeResult = await owner.readGoal(await acquire(context));
438
+ await completeResult?.(nativeResult, {
439
+ segmentId: nativeResult.threadId ? `claude:${nativeResult.threadId}` : null, capabilities: goalCapabilities
440
+ });
441
+ return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
442
+ }
443
+ await bindAccount(entry, configuration, context);
444
+ return claudeGoalValue(await entry.goals.read(), entry.id);
445
+ },
446
+ async updateGoal({ configuration, context, input, representation, interrupt, send }) {
447
+ if (scoped) return unsupportedScopedConversation();
448
+ if (supplied) {
449
+ await acquire(context);
450
+ // readable-check: allow optional-call - optional operation: an application's native conversation may record the goal results it completes
451
+ const completeResult = supplied.completeResult?.(context);
452
+ let command = input;
453
+ let facilities = { signal: context?.signal };
454
+ if (representation !== "native") {
455
+ const current = await owner.readGoal(await acquire(context));
456
+ if (input.action !== "set" && claudeGoalValue(current.goal, current.threadId)?.id !== input.expectedGoalId) {
457
+ throw new Error("The Claude goal changed. Refresh before trying again.");
445
458
  }
446
- await bindAccount(entry, input.configuration, context, AbortSignal.timeout(30_000), true, binding);
459
+ command = { ...input, threadId: current.threadId,
460
+ ...(input.action !== "set" ? { createdAt: current.goal.createdAt, objective: current.goal.objective } : {}) };
461
+ facilities = { signal: context?.signal, interrupt, send: command => send(command, { threadId: current.threadId }) };
447
462
  }
448
- const history = await owner.readHistory(entry);
449
- const nativeId = claudeNativeMessageId(input.messageId);
450
- const accepted = history.userIds.includes(nativeId);
451
- return { accepted, notAdmitted: !accepted && owner.nativeWorkSettled(entry),
452
- messages: history.messages.filter(message => message.userId === nativeId) };
453
- },
454
- steer: command => owner.steer(control, command, commandOptions),
455
- async run(command) {
456
- if (entry && (entry.disposed || entry.command)) throw new Error("This Claude conversation is closed or already working.");
457
- return owner.run(control, command, commandOptions);
458
- },
459
- async cancel({ input, context, representation } = {}) {
460
- if (scoped) {
461
- input = scopedInput(input);
462
- return owner.stopConversation(await acquire(context, input.conversationId, { cleanupExecutionId: input.cleanupExecutionId }));
463
+ const nativeResult = await owner.updateGoal(await acquire(context), command, facilities);
464
+ await completeResult?.(nativeResult);
465
+ return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
466
+ }
467
+ await bindAccount(entry, configuration, context);
468
+ const goal = await entry.goals.read();
469
+ if (input.action !== "set" && claudeGoalValue(goal, entry.id)?.id !== input.expectedGoalId) {
470
+ throw new Error("The Claude goal changed. Refresh before trying again.");
471
+ }
472
+ await entry.goals.update({ ...input, threadId: entry.id,
473
+ ...(input.action !== "set" ? { createdAt: goal?.createdAt, objective: goal?.objective } : {}) }, { interrupt, send, signal: context?.signal });
474
+ return claudeGoalValue(await entry.goals.read(), entry.id);
475
+ },
476
+ async inspectAdmission({ context, representation, verifyAccount = false, ...input }) {
477
+ if (scoped) return unsupportedScopedConversation();
478
+ if (supplied) {
479
+ await acquire(context);
480
+ const result = await owner.inspectAdmission(await acquire(context), input);
481
+ return representation === "native" ? result
482
+ : { accepted: result.admission === "accepted", notAdmitted: result.admission === "not-admitted" };
483
+ }
484
+ if (verifyAccount === true) {
485
+ const binding = input.receiptBinding;
486
+ if (input.threadId !== entry.id || binding?.conversationId !== entry.id ||
487
+ binding.workdir !== entry.context.binding.workdir || binding.configRoot !== configRoot ||
488
+ Object.hasOwn(binding, "scopeWorkdir") !== Object.hasOwn(entry.context.binding, "scopeWorkdir") ||
489
+ binding.scopeWorkdir !== entry.context.binding.scopeWorkdir) {
490
+ throw new Error("The completed Claude response belongs to a different native conversation or host scope.");
463
491
  }
464
- if (!supplied) for (const id of pendingAccountStops) await stopAccount(id);
465
- const value = await owner.cancel(control, { context, acquire, reportFailure, entry });
466
- return supplied && representation === "native" ? { value } : value;
467
- },
468
- async dispose(options) {
469
- try {
470
- let result;
471
- if (scoped && options) {
472
- const input = scopedInput(options.input);
473
- result = await owner.deleteConversation(await acquire(options.context, input.conversationId, {
474
- operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId }));
475
- } else if (supplied) {
476
- result = await disposeNative({ native: supplied, options: scoped ? scoped.context : options });
477
- if (result?.ok === false) throw Object.assign(new Error(result.error || (scoped ? "Scoped cleanup could not be confirmed." : "Claude cleanup could not be confirmed.")), result);
478
- } else {
479
- for (const id of pendingAccountStops) await stopAccount(id);
480
- await owner.close(entry);
481
- }
482
- control.disposed = true;
483
- return result;
484
- } catch (error) {
485
- if (supplied && !scoped) {
486
- error.cleanupFailed = true;
487
- control.current?.failure.reject(error);
488
- }
489
- throw error;
492
+ await bindAccount(entry, input.configuration, context, AbortSignal.timeout(30_000), true, binding);
493
+ }
494
+ const history = await owner.readHistory(entry);
495
+ const nativeId = claudeNativeMessageId(input.messageId);
496
+ const accepted = history.userIds.includes(nativeId);
497
+ return { accepted, notAdmitted: !accepted && owner.nativeWorkSettled(entry),
498
+ messages: history.messages.filter(message => message.userId === nativeId) };
499
+ },
500
+ steer: command => owner.steer(control, command, commandOptions),
501
+ async run(command) {
502
+ if (entry && (entry.disposed || entry.command)) throw new Error("This Claude conversation is closed or already working.");
503
+ return owner.run(control, command, commandOptions);
504
+ },
505
+ async cancel({ input, context, representation } = {}) {
506
+ if (scoped) {
507
+ input = scopedInput(input);
508
+ return owner.stopConversation(await acquire(context, input.conversationId, { cleanupExecutionId: input.cleanupExecutionId }));
509
+ }
510
+ if (!supplied) for (const id of pendingAccountStops) await stopAccount(id);
511
+ const value = await owner.cancel(control, { context, acquire, reportFailure, entry });
512
+ return supplied && representation === "native" ? { value } : value;
513
+ },
514
+ async dispose(options) {
515
+ try {
516
+ let result;
517
+ if (scoped && options) {
518
+ const input = scopedInput(options.input);
519
+ result = await owner.deleteConversation(await acquire(options.context, input.conversationId, {
520
+ operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId }));
521
+ } else if (supplied) {
522
+ result = await disposeNative({ native: supplied, options: scoped ? scoped.context : options });
523
+ if (result?.ok === false) throw Object.assign(new Error(result.error || (scoped ? "Scoped cleanup could not be confirmed." : "Claude cleanup could not be confirmed.")), result);
524
+ } else {
525
+ for (const id of pendingAccountStops) await stopAccount(id);
526
+ await owner.close(entry);
527
+ }
528
+ control.disposed = true;
529
+ return result;
530
+ } catch (error) {
531
+ if (supplied && !scoped) {
532
+ error.cleanupFailed = true;
533
+ control.current?.failure.reject(error);
490
534
  }
535
+ throw error;
491
536
  }
492
- });
493
- }
537
+ }
538
+ });
539
+ }
540
+
541
+ return Object.freeze({
542
+ admissionBeforeDispatch: false,
543
+ toolDiscovery: true,
544
+ attachmentTypes: ["text", "image"],
545
+ capabilities: Object.freeze({ streaming: true, cancellation: true, instructions: true,
546
+ configuration: true, history: true, steering: true, ...goalCapabilities,
547
+ attachments: true, tools: true, nativeTools, structuredOutput: true }),
548
+ validateConfiguration,
549
+ readState,
550
+ inspectTemporaryActivity,
551
+ releaseRenewalPredecessorProcessExitProof,
552
+ releaseRenewalSuccessorProcessExitProof,
553
+ generateRenewalHandover,
554
+ seedRenewalHandover,
555
+ ensureConversation,
556
+ updateConversationSettings,
557
+ disposeNative,
558
+ interruptDetachedConversation,
559
+ deleteDetachedConversation,
560
+ listConversationStorage,
561
+ retireConversationHistory,
562
+ closeProject,
563
+ invalidateRuntimes,
564
+ reconcileSessions,
565
+ createConversation,
566
+ runDetachedConversation,
567
+ createBinding,
568
+ open
494
569
  });
495
570
  }