@jskit-ai/assistant-core 0.1.200 → 0.1.202

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 (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -1,26 +1,29 @@
1
- import { reactive } from "vue";
1
+ import { reactive, toRaw, toValue, watch } from "vue";
2
2
 
3
3
  const messageText = value => String(value || "").trim();
4
4
 
5
5
  function turnMatchesOptimisticMessage(turn = {}, optimistic = {}) {
6
- const canonicalMessageId = messageText(turn?.user?.messageId);
6
+ const authored = optimistic.origin === "application" ? turn.system : turn.user;
7
+ const canonicalMessageId = messageText(authored?.messageId);
7
8
  const optimisticMessageId = messageText(optimistic.id);
8
9
  if (canonicalMessageId && optimisticMessageId) {
9
10
  return canonicalMessageId === optimisticMessageId;
10
11
  }
11
- if (messageText(turn?.user?.text) !== optimistic.text) {
12
+ if (messageText(authored?.text) !== optimistic.text) {
12
13
  return false;
13
14
  }
14
- const userAtMs = Date.parse(String(turn?.user?.at || ""));
15
+ const userAtMs = Date.parse(String(authored?.at || ""));
15
16
  return Number.isFinite(userAtMs) && userAtMs >= optimistic.createdAtMs - 5000;
16
17
  }
17
18
 
18
- function unmatchedOptimisticMessages(turns = [], optimisticMessages = []) {
19
+ function unmatchedOptimisticMessages(turns = [], optimisticMessages = [], { receiptsOnly = false } = {}) {
19
20
  const conversationTurns = Array.isArray(turns) ? turns : [];
20
21
  const matchedTurnIndexes = new Set();
21
22
  return (Array.isArray(optimisticMessages) ? optimisticMessages : []).filter((message) => {
22
23
  const turnIndex = conversationTurns.findIndex((turn, index) => (
23
- !matchedTurnIndexes.has(index) && turnMatchesOptimisticMessage(turn, message)
24
+ !matchedTurnIndexes.has(index) && turnMatchesOptimisticMessage(turn, message) &&
25
+ (!receiptsOnly || message.status === "accepted" ||
26
+ (message.origin === "application" ? turn.system : turn.user)?.receipt !== false)
24
27
  ));
25
28
  if (turnIndex < 0) {
26
29
  return true;
@@ -31,9 +34,10 @@ function unmatchedOptimisticMessages(turns = [], optimisticMessages = []) {
31
34
  }
32
35
 
33
36
  // One delivery state per conversation. The caller owns transport and receipts;
34
- // the shared assistant element renders these pending and failed turns.
37
+ // the shared assistant element renders pending, failed and uncertain turns.
35
38
  function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
36
39
  const state = reactive({ messages: [], sending: false });
40
+ const acceptanceCallbacks = new WeakMap();
37
41
  let deliveryTail = null;
38
42
  let pendingSends = new Map();
39
43
 
@@ -46,35 +50,64 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
46
50
  }
47
51
 
48
52
  function reconcile(turns) {
49
- state.messages = unmatchedOptimisticMessages(turns, state.messages);
53
+ const unmatched = unmatchedOptimisticMessages(turns, state.messages, { receiptsOnly: true });
54
+ const retained = new Set(unmatched.map(message => message.id));
55
+ for (const message of state.messages) {
56
+ if (!retained.has(message.id)) accept(message.id);
57
+ }
58
+ state.messages = unmatched;
50
59
  }
51
60
 
52
61
  function turns(savedTurns = []) {
53
62
  return [
54
- ...savedTurns,
55
- ...unmatchedOptimisticMessages(savedTurns, state.messages).map((message) => ({
56
- optimistic: { error: message.error, id: message.id, status: message.status },
57
- turnId: message.id,
58
- user: {
59
- attachments: message.attachments,
60
- at: message.createdAt,
61
- messageId: message.id,
62
- role: "user",
63
- text: message.text
64
- }
65
- }))
63
+ ...savedTurns.map(turn => {
64
+ const message = state.messages.find(message => message.status !== "accepted" &&
65
+ turnMatchesOptimisticMessage(turn, message) &&
66
+ (message.origin === "application" ? turn.system : turn.user)?.receipt === false);
67
+ if (!message) return turn;
68
+ const pending = optimisticTurn(message);
69
+ return { ...turn, optimistic: pending.optimistic, ...(pending.system ? { system: pending.system } : {}) };
70
+ }),
71
+ ...unmatchedOptimisticMessages(savedTurns, state.messages).map(optimisticTurn)
66
72
  ];
67
73
  }
68
74
 
75
+ function optimisticTurn(message) {
76
+ const role = message.origin === "application" ? "system" : "user";
77
+ const authored = {
78
+ attachments: message.attachments,
79
+ at: message.createdAt,
80
+ messageId: message.id,
81
+ role,
82
+ text: message.text
83
+ };
84
+ const status = message.status === "uncertain" ? {
85
+ role: "system", text: "Message delivery is not confirmed.",
86
+ delivery: { messageId: message.id, error: message.error, checking: message.checking === true }
87
+ } : null;
88
+ return {
89
+ optimistic: { error: message.error, id: message.id, status: message.status },
90
+ turnId: message.id,
91
+ [role]: authored,
92
+ ...(status ? { system: role === "system" ? { ...authored, ...status, text: `${authored.text}\n\n${status.text}` } : status } : {})
93
+ };
94
+ }
95
+
69
96
  async function send(payload, {
70
97
  messageId = crypto.randomUUID(),
71
98
  deliver = defaultDeliver,
72
99
  isCurrent = () => true,
100
+ receiptTurns = null,
101
+ onAccepted,
102
+ uncertainOnError = false,
73
103
  queue = false
74
104
  } = {}) {
75
- if ((state.sending && !queue) || pendingSends.has(messageId) || !messageText(payload?.message)) return false;
105
+ if ((state.sending && !queue) || pendingSends.has(messageId) || find(messageId)?.status === "uncertain" ||
106
+ (!messageText(payload?.message) && !payload?.displayAttachments?.length)) return false;
76
107
  if (typeof deliver !== "function") throw new TypeError("Message delivery requires a deliver function.");
77
108
  const snapshot = JSON.parse(JSON.stringify(payload));
109
+ const previous = find(messageId);
110
+ const acknowledge = previous && acceptanceCallbacks.get(toRaw(previous)) || onAccepted;
78
111
  const ownerPendingSends = pendingSends;
79
112
  const current = () => pendingSends === ownerPendingSends && isCurrent();
80
113
  const predecessor = deliveryTail;
@@ -90,27 +123,44 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
90
123
  id: messageId,
91
124
  payload: snapshot,
92
125
  status: "pending",
93
- text: String(snapshot.displayMessage || snapshot.message).trim()
126
+ text: String(snapshot.displayMessage || snapshot.message || "").trim()
94
127
  };
128
+ if (typeof acknowledge === "function") acceptanceCallbacks.set(message, acknowledge);
95
129
  state.messages = [...state.messages.filter((entry) => entry.id !== messageId), message];
96
130
  state.sending = true;
131
+ let stopReceipt;
97
132
  try {
98
133
  if (predecessor) await predecessor;
99
134
  if (!current()) return false;
100
- const response = await deliver({ ...snapshot, messageId });
135
+ if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain")) {
136
+ fail(messageId, "Check the previous message's delivery before sending again.");
137
+ return false;
138
+ }
139
+ let receipt;
140
+ if (receiptTurns !== null) {
141
+ receipt = Promise.withResolvers();
142
+ settled.receipt = receipt;
143
+ stopReceipt = watch([current, () => toValue(receiptTurns)], ([isCurrent, turns]) => {
144
+ if (!isCurrent) receipt.resolve(false);
145
+ else if (turns?.some(turn => turn.user?.messageId === messageId && turn.user.receipt !== false)) receipt.resolve({ ok: true });
146
+ }, { immediate: true });
147
+ }
148
+ const submitted = deliver({ ...snapshot, messageId });
149
+ const response = await (receipt ? Promise.race([submitted, receipt.promise]) : submitted);
101
150
  if (!current()) return false;
102
151
  if (response === false || response?.ok === false) {
103
- fail(messageId, response?.error || "Message could not be sent.");
152
+ fail(messageId, response?.error || "Message could not be sent.", response?.status === "uncertain" ? "uncertain" : "failed");
104
153
  } else {
105
- const acceptedMessage = find(messageId);
106
- if (acceptedMessage) acceptedMessage.status = "accepted";
154
+ accept(messageId);
107
155
  }
108
156
  return response;
109
157
  } catch (error) {
110
158
  if (!current()) return false;
111
- fail(messageId, error);
159
+ if (find(messageId)?.status === "accepted") return { ok: true };
160
+ fail(messageId, error, uncertainOnError || error?.status === "uncertain" ? "uncertain" : "failed");
112
161
  throw error;
113
162
  } finally {
163
+ stopReceipt?.();
114
164
  ownerPendingSends.delete(messageId);
115
165
  if (pendingSends === ownerPendingSends) {
116
166
  state.sending = ownerPendingSends.size > 0;
@@ -120,14 +170,43 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
120
170
  }
121
171
  }
122
172
 
123
- function fail(messageId, error) {
173
+ function fail(messageId, error, status = "failed") {
124
174
  const message = find(messageId);
125
- if (message) {
175
+ if (message && message.status !== "accepted") {
126
176
  message.error = String(error?.message || error || "Message could not be sent.").trim();
127
- message.status = "failed";
177
+ message.status = status;
178
+ }
179
+ }
180
+
181
+ function accept(messageId) {
182
+ const message = find(messageId);
183
+ if (message) { message.status = "accepted"; message.error = ""; }
184
+ pendingSends.get(messageId)?.receipt?.resolve({ ok: true });
185
+ if (message) {
186
+ const entry = toRaw(message);
187
+ const acknowledge = acceptanceCallbacks.get(entry);
188
+ acceptanceCallbacks.delete(entry);
189
+ acknowledge?.();
128
190
  }
129
191
  }
130
192
 
193
+ function restoreUncertain(request, savedTurns = []) {
194
+ const messageId = messageText(request?.messageId);
195
+ if (!messageId || savedTurns.some(turn => (turn.user || turn.system)?.messageId === messageId &&
196
+ (turn.user || turn.system).receipt !== false) ||
197
+ find(messageId)?.status === "accepted") return false;
198
+ if (!find(messageId)) {
199
+ const payload = JSON.parse(JSON.stringify({ message: request.text || "", displayAttachments: request.attachments || [] }));
200
+ const createdAt = request.at || new Date().toISOString();
201
+ state.messages = [...state.messages, { id: messageId, payload, text: payload.message, origin: request.origin,
202
+ attachments: payload.displayAttachments, createdAt, createdAtMs: Date.parse(createdAt),
203
+ status: "uncertain", error: "" }];
204
+ }
205
+ fail(messageId, request.error || "Delivery could not be confirmed.", "uncertain");
206
+ pendingSends.get(messageId)?.receipt?.resolve({ ok: false, status: "uncertain", error: request.error });
207
+ return true;
208
+ }
209
+
131
210
  function cancel(messageId) {
132
211
  if (find(messageId)?.status !== "failed") return false;
133
212
  remove(messageId);
@@ -148,13 +227,17 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
148
227
 
149
228
  function reset() {
150
229
  deliveryTail = null;
151
- for (const pending of pendingSends.values()) pending.resolve();
230
+ for (const pending of pendingSends.values()) {
231
+ pending.receipt?.resolve(false);
232
+ pending.resolve();
233
+ }
152
234
  pendingSends = new Map();
153
235
  state.messages = [];
154
236
  state.sending = false;
155
237
  }
156
238
 
157
- return { cancel, edit, find, reconcile, remove, resend, reset, send, state, turns };
239
+ return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, send, state, turns };
158
240
  }
159
241
 
160
242
  export { createAssistantMessageDelivery, unmatchedOptimisticMessages };
243
+ export { retainAssistantConversation } from "./retainedConversation.js";
@@ -0,0 +1,32 @@
1
+ import { effectScope, shallowReactive } from "vue";
2
+
3
+ const applicationConversations = new WeakMap();
4
+
5
+ /** Views and voice retain the same readers and delivery state for an exact target. */
6
+ function retainAssistantConversation(app, key, createRuntime, reader = { active: true }) {
7
+ let conversations = applicationConversations.get(app);
8
+ if (!conversations) { conversations = new Map(); applicationConversations.set(app, conversations); }
9
+ let entry = conversations.get(key);
10
+ if (!entry) {
11
+ const scope = effectScope(true);
12
+ const readers = shallowReactive(new Map());
13
+ try {
14
+ const runtime = app.runWithContext(() => scope.run(() => createRuntime(readers)));
15
+ entry = { scope, readers, runtime };
16
+ runtime.retain = () => retainAssistantConversation(app, key, createRuntime);
17
+ conversations.set(key, entry);
18
+ } catch (error) { scope.stop(); throw error; }
19
+ }
20
+ const token = Symbol("conversation reader");
21
+ entry.readers.set(token, reader);
22
+ return {
23
+ runtime: entry.runtime,
24
+ release() {
25
+ if (!entry.readers.delete(token) || entry.readers.size) return;
26
+ entry.scope.stop();
27
+ conversations.delete(key);
28
+ }
29
+ };
30
+ }
31
+
32
+ export { retainAssistantConversation };
@@ -0,0 +1,76 @@
1
+ import { computed, ref, toValue, watch } from "vue";
2
+ import { numberedQuestionSubmissionText, parseNumberedQuestionPrompt } from "../../shared/conversation/numberedQuestionSugar.js";
3
+ import { parseAnswerChoicePrompt } from "../../shared/conversation/answerChoiceSugar.js";
4
+
5
+ /** Ordinary chat answer fields; the caller owns authorized message selection and delivery. */
6
+ function useAssistantQuestions({ message = "", extraChoice = null } = {}) {
7
+ const questionText = computed(() => String(toValue(message) || ""));
8
+ const answers = ref({});
9
+ const choice = ref("");
10
+ const dismissedText = ref("");
11
+ const submittedText = ref("");
12
+ const numberedInput = computed(() => parseNumberedQuestionPrompt(questionText.value));
13
+ const questions = computed(() => (
14
+ [dismissedText.value, submittedText.value].includes(questionText.value)
15
+ ? []
16
+ : numberedInput.value.questions || []
17
+ ));
18
+ const selectItems = computed(() => Object.fromEntries(
19
+ questions.value.map((question) => {
20
+ const choices = Array.isArray(question.choices) ? question.choices : [];
21
+ const extra = toValue(extraChoice);
22
+ const includesExtra = extra && choices.some((item) => (
23
+ String(item?.value || "").trim().toLowerCase() === String(extra.value || "").trim().toLowerCase()
24
+ ));
25
+ return [question.name, !extra || includesExtra ? choices : [...choices, extra]];
26
+ })
27
+ ));
28
+ const choices = computed(() => (
29
+ questions.value.length || submittedText.value === questionText.value
30
+ ? []
31
+ : parseAnswerChoicePrompt(questionText.value).choices || []
32
+ ));
33
+ const active = computed(() => Boolean(questions.value.length || choices.value.length));
34
+
35
+ function capture(draft = "") {
36
+ const additionalContext = String(draft || "").trim();
37
+ return {
38
+ message: questions.value.length
39
+ ? [numberedQuestionSubmissionText(questions.value, answers.value), additionalContext].filter(Boolean).join("\n\n")
40
+ : choice.value || additionalContext,
41
+ questionText: active.value ? questionText.value : ""
42
+ };
43
+ }
44
+
45
+ function markSubmitted(text = "") {
46
+ submittedText.value = text;
47
+ answers.value = {};
48
+ choice.value = "";
49
+ }
50
+
51
+ function dismiss() {
52
+ if (!questions.value.length) return false;
53
+ dismissedText.value = questionText.value;
54
+ answers.value = {};
55
+ return true;
56
+ }
57
+
58
+ function reset() {
59
+ answers.value = {};
60
+ dismissedText.value = "";
61
+ submittedText.value = "";
62
+ choice.value = "";
63
+ }
64
+
65
+ watch(questionText, (text) => {
66
+ answers.value = {};
67
+ if (text !== dismissedText.value) dismissedText.value = "";
68
+ if (text !== submittedText.value) submittedText.value = "";
69
+ choice.value = "";
70
+ });
71
+
72
+ return { questions, selectItems, choices, answers, choice, active, capture, markSubmitted, dismiss, reset,
73
+ setAnswers(value) { answers.value = value; }, setChoice(value) { choice.value = value; } };
74
+ }
75
+
76
+ export { useAssistantQuestions };
@@ -77,12 +77,82 @@ function resolveRequiredBasePath(resolveBasePath) {
77
77
  }
78
78
 
79
79
  function createAssistantApi({ request, requestStream, resolveBasePath, resolveSurfaceId = null } = {}) {
80
- if (typeof request !== "function" || typeof requestStream !== "function") {
81
- throw new Error("createAssistantApi requires request() and requestStream().");
80
+ if (typeof request !== "function") {
81
+ throw new Error("createAssistantApi requires request().");
82
+ }
83
+
84
+ function requestConversation(conversationId, suffix, options) {
85
+ const basePath = resolveRequiredBasePath(resolveBasePath);
86
+ const encodedId = encodeURIComponent(String(conversationId || "").trim());
87
+ const headers = resolveAssistantRequestHeaders(resolveSurfaceId);
88
+ return request(`${basePath}/conversations/${encodedId}${suffix}`, {
89
+ ...options,
90
+ ...(headers ? { headers } : {})
91
+ });
82
92
  }
83
93
 
84
94
  return Object.freeze({
95
+ readConversation(conversationId, { signal, beforeTurnId, limit } = {}) {
96
+ const params = new URLSearchParams();
97
+ appendQueryParam(params, "beforeTurnId", beforeTurnId);
98
+ appendQueryParam(params, "limit", limit);
99
+ return requestConversation(conversationId, params.size ? `?${params}` : "", { method: "GET", signal });
100
+ },
101
+
102
+ sendConversationMessage(conversationId, payload, { signal } = {}) {
103
+ return requestConversation(conversationId, "/messages", {
104
+ method: "POST", signal,
105
+ body: {
106
+ messageId: payload.messageId,
107
+ text: payload.text,
108
+ ...(Object.hasOwn(payload, "data") ? { data: payload.data } : {}),
109
+ ...(Object.hasOwn(payload, "attachmentIds") ? { attachmentIds: payload.attachmentIds } : {}),
110
+ ...(Object.hasOwn(payload, "steer") ? { steer: payload.steer } : {})
111
+ }
112
+ });
113
+ },
114
+
115
+ cancelConversation(conversationId, { signal } = {}) {
116
+ return requestConversation(conversationId, "/cancel", { method: "POST", signal });
117
+ },
118
+
119
+ inspectConversationDelivery(conversationId, messageId, { signal } = {}) {
120
+ const encodedMessageId = encodeURIComponent(String(messageId || "").trim());
121
+ return requestConversation(conversationId, `/deliveries/${encodedMessageId}/inspect`, { method: "POST", signal });
122
+ },
123
+
124
+ readConversationGoal(conversationId, { signal } = {}) {
125
+ return requestConversation(conversationId, "/goal", { method: "GET", signal });
126
+ },
127
+
128
+ updateConversationGoal(conversationId, payload, { signal } = {}) {
129
+ const body = {};
130
+ for (const key of ["action", "expectedSegmentId", "expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
131
+ if (Object.hasOwn(payload, key)) body[key] = payload[key];
132
+ }
133
+ return requestConversation(conversationId, "/goal", { method: "POST", signal, body });
134
+ },
135
+
136
+ configureConversation(conversationId, configuration, { signal } = {}) {
137
+ return requestConversation(conversationId, "/configuration", {
138
+ method: "PATCH", signal, body: { configuration }
139
+ });
140
+ },
141
+
142
+ selectConversation(conversationId, selection, { signal } = {}) {
143
+ return requestConversation(conversationId, "/selection", {
144
+ method: "POST", signal, body: { selection }
145
+ });
146
+ },
147
+
148
+ replaceConversation(conversationId, replacement, { signal } = {}) {
149
+ return requestConversation(conversationId, "/replacement", {
150
+ method: "POST", signal, body: { replacement }
151
+ });
152
+ },
153
+
85
154
  async streamChat(payload, { signal, onEvent, onMalformedLine, rejectOnErrorEvent = true } = {}) {
155
+ if (typeof requestStream !== "function") throw new TypeError("streamChat requires requestStream().");
86
156
  const basePath = resolveRequiredBasePath(resolveBasePath);
87
157
  let streamEventError = null;
88
158
  const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
@@ -0,0 +1,222 @@
1
+ import { isPlainObject, normalizeText } from "./normalize.js";
2
+ import { createReentrantConversationStorage } from "./storage.js";
3
+ import { normalizeCodexAppServerRunState, codexAppServerRunStateIsActive,
4
+ codexAppServerRunStateIsTerminal } from "./codexTurn.js";
5
+
6
+ const normalizeRunState = state => normalizeCodexAppServerRunState(normalizeText(state));
7
+ const runStateIsActive = state => codexAppServerRunStateIsActive(normalizeText(state));
8
+ const runStateIsTerminal = state => codexAppServerRunStateIsTerminal(normalizeText(state));
9
+
10
+ export function conversationAgentRunRecord(record, normalizedRunId, {
11
+ normalizeState = normalizeRunState,
12
+ isActive = runStateIsActive
13
+ } = {}) {
14
+ return isPlainObject(record)
15
+ ? {
16
+ ...record,
17
+ active: isActive(record.state),
18
+ events: Array.isArray(record.events) ? record.events.filter(isPlainObject) : [],
19
+ id: normalizedRunId,
20
+ state: normalizeState(record.state)
21
+ }
22
+ : null;
23
+ }
24
+
25
+ export function conversationAgentRunEvent(previous, normalizedRunId, {
26
+ event = {},
27
+ patch = {}
28
+ } = {}, {
29
+ now = () => new Date(),
30
+ normalizeState = normalizeRunState,
31
+ isTerminal = runStateIsTerminal
32
+ } = {}) {
33
+ const eventAt = normalizeText(event.at || patch.updatedAt) || now().toISOString();
34
+ const state = normalizeState(patch.state || event.state || previous.state);
35
+ const terminalState = isTerminal(state);
36
+ const eventRecord = {
37
+ ...event,
38
+ at: eventAt,
39
+ kind: normalizeText(event.kind || state || "updated"),
40
+ message: normalizeText(event.message || patch.message),
41
+ state
42
+ };
43
+ const record = {
44
+ ...previous,
45
+ ...patch,
46
+ active: !terminalState,
47
+ events: [
48
+ ...(Array.isArray(previous.events) ? previous.events : []),
49
+ eventRecord
50
+ ],
51
+ finishedAt: terminalState
52
+ ? normalizeText(patch.finishedAt || previous.finishedAt) || eventAt
53
+ : "",
54
+ id: normalizedRunId,
55
+ startedAt: normalizeText(previous.startedAt || patch.startedAt) || eventAt,
56
+ state,
57
+ updatedAt: eventAt
58
+ };
59
+ if (!terminalState && !Object.hasOwn(patch, "error")) {
60
+ record.error = "";
61
+ }
62
+ return record;
63
+ }
64
+
65
+ /** The original native run occupies its bound conversation segment. Reading a
66
+ * binding without a run never constructs historical ownership or writes state.
67
+ */
68
+ export function createCodexConversationRunStore({ storage, scope, segmentId, isCurrent, clock = () => new Date() } = {}) {
69
+ if (typeof isCurrent !== "function") throw new TypeError("A native run store requires its provider ownership fence.");
70
+ storage = createReentrantConversationStorage(storage);
71
+ const runId = "codex_app_server";
72
+
73
+ function assertScope(sessionId, requestedRunId = runId) {
74
+ if (sessionId !== scope) throw new Error("The native run belongs to another conversation.");
75
+ if (normalizeText(requestedRunId) !== runId) throw new TypeError("This binding stores only its Codex app-server run.");
76
+ if (!isCurrent()) throw new Error("A retired provider cannot change a native binding.");
77
+ }
78
+
79
+ async function currentMetadata(transaction) {
80
+ const metadata = await transaction.readMetadata();
81
+ if (!isCurrent() || metadata.runtime?.segmentId !== segmentId || metadata.runtime?.engine !== "codex") {
82
+ throw new Error("A retired native conversation cannot change the current binding.");
83
+ }
84
+ return metadata;
85
+ }
86
+
87
+ async function readAgentRun(sessionId, requestedRunId) {
88
+ assertScope(sessionId, requestedRunId);
89
+ return storage.read(scope, async transaction => {
90
+ const metadata = await currentMetadata(transaction);
91
+ return conversationAgentRunRecord(metadata.runtime.binding?.codexAppServerRun, runId);
92
+ });
93
+ }
94
+
95
+ function mutateSession(sessionId, operation) {
96
+ assertScope(sessionId);
97
+ return storage.write(scope, async transaction => {
98
+ await currentMetadata(transaction);
99
+ return operation(transaction);
100
+ });
101
+ }
102
+
103
+ return Object.freeze({
104
+ readAgentRun,
105
+ mutateSession,
106
+ async getSession(sessionId) {
107
+ const run = await readAgentRun(sessionId, runId);
108
+ return { sessionId, agentRuns: run ? [run] : [] };
109
+ },
110
+ async writeAgentRunEvent(sessionId, requestedRunId, input = {}) {
111
+ assertScope(sessionId, requestedRunId);
112
+ return mutateSession(sessionId, async transaction => {
113
+ const previous = await readAgentRun(sessionId, runId) || { events: [], id: runId };
114
+ const record = conversationAgentRunEvent(previous, runId, input, { now: clock });
115
+ const metadata = await currentMetadata(transaction);
116
+ metadata.runtime.binding.codexAppServerRun = record;
117
+ await transaction.writeMetadata(metadata);
118
+ return record;
119
+ });
120
+ }
121
+ });
122
+ }
123
+
124
+ /** The native owner uses the existing transcript and transient streams through
125
+ * the same fenced transaction as its original run. Its private delivery read is
126
+ * a projection of the one authored request, never another stored journal.
127
+ */
128
+ export function createCodexConversationStore({ storage, scope, segmentId, isCurrent, transcript, streams } = {}) {
129
+ const runStore = createCodexConversationRunStore({ storage, scope, segmentId, isCurrent });
130
+ const assertCurrent = sessionId => {
131
+ if (sessionId !== scope || !isCurrent()) throw new Error("A retired provider cannot change this conversation.");
132
+ };
133
+ const reader = name => (sessionId, ...args) => {
134
+ assertCurrent(sessionId);
135
+ return transcript[name](sessionId, ...args);
136
+ };
137
+ async function nativeOrigin(transaction, identity) {
138
+ if (!identity?.threadId || !identity.turnId) return undefined;
139
+ const { runtime } = await transaction.readMetadata();
140
+ const run = runtime.binding?.codexAppServerRun;
141
+ if (runtime.segmentId !== segmentId || runtime.binding.threadId !== identity.threadId ||
142
+ run?.providerThreadId !== identity.threadId || run.providerTurnId !== identity.turnId || !run.outerTurnId) return undefined;
143
+ for (const turnId of (await transaction.listTurnIds()).reverse()) {
144
+ const turn = await transaction.readTurn(turnId);
145
+ if (turn?.metadata?.runtime?.segmentId !== segmentId) continue;
146
+ if (turn.user?.messageId === run.outerTurnId) return "user";
147
+ if (turn.system?.messageId === run.outerTurnId && turn.system.origin === "application") return "application";
148
+ }
149
+ const request = runtime.request;
150
+ if (request?.messageId === run.outerTurnId && ["user", "application"].includes(request.origin)) return request.origin;
151
+ return undefined;
152
+ }
153
+ const writer = name => (sessionId, input) => runStore.mutateSession(sessionId, async transaction => {
154
+ const written = await transcript[name](sessionId, input);
155
+ if (!written || written.metadata?.runtime?.segmentId) return written;
156
+ const origin = await nativeOrigin(transaction, input.nativeIdentity);
157
+ await transaction.updateTurnMetadata(written.turnId, { runtime: {
158
+ status: "complete", engine: "codex", segmentId, ...(origin ? { origin } : {}),
159
+ ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
160
+ } });
161
+ return transaction.readTurn(written.turnId);
162
+ });
163
+ return Object.freeze({
164
+ ...runStore,
165
+ readConversationLog: reader("readConversationLog"),
166
+ conversationMessageIdExists: reader("conversationMessageIdExists"),
167
+ writeConversationAssistantMessage: writer("writeConversationAssistantMessage"),
168
+ writeConversationCommentaryMessage: writer("writeConversationCommentaryMessage"),
169
+ writeConversationThinkingMessage: writer("writeConversationThinkingMessage"),
170
+ upsertConversationAssistantMessage: writer("upsertConversationAssistantMessage"),
171
+ async writeConversationUserMessage(sessionId, input = {}) {
172
+ return runStore.mutateSession(sessionId, async transaction => {
173
+ if (input.messageId && await transaction.hasMessage(input.messageId)) return null;
174
+ const metadata = await transaction.readMetadata();
175
+ const request = input.authoredRequest?.messageId === input.messageId ? input.authoredRequest
176
+ : metadata.runtime.request?.messageId === input.messageId ? metadata.runtime.request : null;
177
+ if (!request) {
178
+ const written = await transcript.writeConversationUserMessage(sessionId, input);
179
+ if (!written) return null;
180
+ await transaction.updateTurnMetadata(written.turnId, { runtime: {
181
+ status: "running", engine: "codex", segmentId, origin: "user",
182
+ ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
183
+ } });
184
+ return transaction.readTurn(written.turnId);
185
+ }
186
+ const turnId = await transaction.nextTurnId();
187
+ await transaction.appendMessage(turnId, {
188
+ role: request.origin === "application" ? "system" : "user",
189
+ messageId: request.messageId, text: request.text, origin: request.origin || "user",
190
+ attachments: request.attachments || input.attachments || [], at: request.at || new Date().toISOString(),
191
+ ...(request.data !== undefined ? { data: request.data } : {}), ...(request.goal ? { goal: request.goal } : {}),
192
+ turnMetadata: { ...input.turnMetadata, runtime: { ...input.turnMetadata?.runtime,
193
+ status: "running", engine: "codex", segmentId, origin: request.origin || "user",
194
+ ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {}),
195
+ ...(request.goal ? { goalMessageId: request.messageId } : {}) } }
196
+ });
197
+ return transaction.readTurn(turnId);
198
+ });
199
+ },
200
+ readMetadataValue(sessionId, key) {
201
+ assertCurrent(sessionId);
202
+ if (key !== "assistant_delivery") throw new Error("The native conversation requested unknown delivery metadata.");
203
+ return storage.read(sessionId, async transaction => {
204
+ const { runtime } = await transaction.readMetadata();
205
+ if (!isCurrent() || runtime.segmentId !== segmentId || runtime.engine !== "codex") {
206
+ throw new Error("A retired native conversation cannot read current delivery metadata.");
207
+ }
208
+ const request = runtime.request;
209
+ return JSON.stringify({ engines: { codex: { pending: request && { ...request,
210
+ displayMessage: request.displayMessage ?? request.text,
211
+ displayAttachments: request.displayAttachments ?? request.attachments } } } });
212
+ });
213
+ },
214
+ updateConversationStream(sessionId, input) {
215
+ return runStore.mutateSession(sessionId, async transaction => streams.update(sessionId, {
216
+ ...input, origin: await nativeOrigin(transaction, input.nativeIdentity)
217
+ }));
218
+ },
219
+ completeConversationStreamMessage(sessionId, messageId) { assertCurrent(sessionId); return streams.complete(sessionId, messageId); },
220
+ clearConversationStream(sessionId) { assertCurrent(sessionId); return streams.clear(sessionId); }
221
+ });
222
+ }