@jskit-ai/assistant-core 0.1.164 → 0.1.166

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 (63) hide show
  1. package/examples/conversation/App.vue +104 -0
  2. package/examples/conversation/README.md +42 -0
  3. package/examples/conversation/dist/assets/index-CSTXm6q3.css +1 -0
  4. package/examples/conversation/dist/assets/index-DGgOLzoo.js +30 -0
  5. package/examples/conversation/dist/index.html +4 -0
  6. package/examples/conversation/index.html +2 -0
  7. package/examples/conversation/main.js +9 -0
  8. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js +7450 -0
  9. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js.map +1 -0
  10. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js +2311 -0
  11. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js.map +1 -0
  12. package/examples/conversation/node_modules/.vite/deps/_metadata.json +70 -0
  13. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js +710 -0
  14. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js.map +1 -0
  15. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js +1214 -0
  16. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js.map +1 -0
  17. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js +98 -0
  18. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js.map +1 -0
  19. package/examples/conversation/node_modules/.vite/deps/package.json +3 -0
  20. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js +1301 -0
  21. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js.map +1 -0
  22. package/examples/conversation/node_modules/.vite/deps/vue.js +2 -0
  23. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js +8538 -0
  24. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js.map +1 -0
  25. package/examples/conversation/node_modules/.vite/deps/vuetify.js +413 -0
  26. package/examples/conversation/node_modules/.vite/deps/vuetify.js.map +1 -0
  27. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js +25370 -0
  28. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js.map +1 -0
  29. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js +132 -0
  30. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js.map +1 -0
  31. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js +70 -0
  32. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js.map +1 -0
  33. package/examples/conversation/package.json +22 -0
  34. package/examples/conversation/server.js +123 -0
  35. package/examples/conversation/vite.config.js +8 -0
  36. package/package.json +59 -7
  37. package/src/client/conversation/AssistantComposerActions.vue +43 -0
  38. package/src/client/conversation/AssistantConversationElement.vue +78 -0
  39. package/src/client/conversation/AssistantProgress.vue +88 -0
  40. package/src/client/conversation/AssistantPromptInput.vue +467 -0
  41. package/src/client/conversation/AssistantTranscript.vue +1361 -0
  42. package/src/client/conversation/LongTextInlineParts.vue +64 -0
  43. package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
  44. package/src/client/conversation/index.js +9 -0
  45. package/src/client/conversation/submitText.js +29 -0
  46. package/src/client/conversation/useScrollToBottom.js +151 -0
  47. package/src/server/conversation/codexClient.js +328 -0
  48. package/src/server/conversation/codexEvents.js +623 -0
  49. package/src/server/conversation/codexTurn.js +280 -0
  50. package/src/server/conversation/index.js +2 -0
  51. package/src/server/conversation/memoryStorage.js +76 -0
  52. package/src/server/conversation/normalize.js +7 -0
  53. package/src/server/conversation/openCodeClient.js +512 -0
  54. package/src/server/conversation/transcript.js +92 -0
  55. package/src/shared/conversation/answerChoiceSugar.js +191 -0
  56. package/src/shared/conversation/index.js +6 -0
  57. package/src/shared/conversation/longTextBlocks.js +403 -0
  58. package/src/shared/conversation/numberedQuestionSugar.js +295 -0
  59. package/src/shared/conversation/scrollFollowState.js +32 -0
  60. package/src/shared/conversation/thinkingText.js +13 -0
  61. package/src/shared/conversation/turns.js +30 -0
  62. package/src/testing/conversationStorageContract.js +35 -0
  63. package/test/conversation.test.js +57 -0
@@ -0,0 +1,280 @@
1
+ import { normalizeText, isPlainObject } from "./normalize.js";
2
+ import { classifyCodexAppServerEvent, codexAppServerNotificationParams, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerNotificationError, codexAppServerStatusFromValue, codexAppServerProviderThreadAssistantSegments } from "./codexEvents.js";
3
+ const CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS = 180_000;
4
+ const CODEX_APP_SERVER_DETACHED_FAILURE_DETAIL_GRACE_MS = 500;
5
+
6
+ function codexAppServerTurnStatusIsActive(status = "") {
7
+ return normalizeText(status) === "inProgress";
8
+ }
9
+
10
+ function codexAppServerTurnStatusIsComplete(status = "") {
11
+ return ["completed", "interrupted", "failed"].includes(normalizeText(status));
12
+ }
13
+
14
+ function codexAppServerTurnStatusIsSuccessfulComplete(status = "") {
15
+ return normalizeText(status) === "completed";
16
+ }
17
+
18
+ function codexAppServerTurnStatusIsProviderFailure(status = "") {
19
+ return ["failed", "interrupted"].includes(normalizeText(status));
20
+ }
21
+
22
+ function codexAppServerTurnTokenUsage(notification = {}) {
23
+ if (normalizeText(notification.method) !== "thread/tokenUsage/updated") {
24
+ return null;
25
+ }
26
+ const params = codexAppServerNotificationParams(notification);
27
+ const tokenUsage = isPlainObject(params.tokenUsage) ? params.tokenUsage : {};
28
+ const turnUsage = isPlainObject(tokenUsage.last) ? tokenUsage.last : tokenUsage;
29
+ const snapshot = {};
30
+ for (const field of [
31
+ "cachedInputTokens",
32
+ "cacheWriteInputTokens",
33
+ "inputTokens",
34
+ "outputTokens",
35
+ "reasoningOutputTokens",
36
+ "totalTokens"
37
+ ]) {
38
+ const value = Number(turnUsage[field]);
39
+ if (Number.isSafeInteger(value) && value >= 0) {
40
+ snapshot[field] = value;
41
+ }
42
+ }
43
+ return Object.keys(snapshot).length ? Object.freeze(snapshot) : null;
44
+ }
45
+
46
+ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "", {
47
+ includeThreadHistory = true,
48
+ onEvent = null,
49
+ timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
50
+ } = {}) {
51
+ const normalizedThreadId = normalizeText(threadId);
52
+ let targetTurnId = "";
53
+ let finalText = "";
54
+ let usage = null;
55
+ let failureDetailTimeout = null;
56
+ let settled = false;
57
+ let timeout = null;
58
+ let connectionCheck = null;
59
+ let unsubscribe = null;
60
+ let pendingCompletionStatus = "";
61
+ let pendingFailure = null;
62
+ let resolveWaiter = null;
63
+ let rejectWaiter = null;
64
+
65
+ function cleanup() {
66
+ clearTimeout(timeout);
67
+ timeout = null;
68
+ clearInterval(connectionCheck);
69
+ connectionCheck = null;
70
+ clearTimeout(failureDetailTimeout);
71
+ failureDetailTimeout = null;
72
+ unsubscribe?.();
73
+ unsubscribe = null;
74
+ }
75
+
76
+ function finish(result = {}) {
77
+ if (settled) {
78
+ return;
79
+ }
80
+ settled = true;
81
+ cleanup();
82
+ resolveWaiter?.(result);
83
+ }
84
+
85
+ function fail(error) {
86
+ if (settled) {
87
+ return;
88
+ }
89
+ settled = true;
90
+ cleanup();
91
+ rejectWaiter?.(error);
92
+ }
93
+
94
+ function emitWatcherEvent(classification = {}) {
95
+ if (typeof onEvent !== "function" || !classification?.kind) {
96
+ return;
97
+ }
98
+ onEvent({
99
+ ...classification,
100
+ threadId: classification.threadId || normalizedThreadId,
101
+ turnId: classification.turnId || targetTurnId
102
+ });
103
+ }
104
+
105
+ async function resultFromThread() {
106
+ if (
107
+ !includeThreadHistory ||
108
+ !normalizedThreadId ||
109
+ !targetTurnId ||
110
+ typeof provider?.readThread !== "function"
111
+ ) {
112
+ return {
113
+ status: "",
114
+ statusType: "",
115
+ text: ""
116
+ };
117
+ }
118
+ const thread = await provider.readThread(normalizedThreadId);
119
+ const rawStatus = thread.raw?.status || thread.response?.thread?.status;
120
+ return {
121
+ status: codexAppServerStatusFromValue(rawStatus),
122
+ statusType: normalizeText(typeof rawStatus === "string" ? rawStatus : rawStatus?.type),
123
+ text: codexAppServerProviderThreadAssistantSegments(thread, targetTurnId)
124
+ .map((segment) => segment.text)
125
+ .join("\n\n")
126
+ };
127
+ }
128
+
129
+ function failAfterDetailGrace(error) {
130
+ if (settled || failureDetailTimeout) {
131
+ return;
132
+ }
133
+ failureDetailTimeout = setTimeout(() => {
134
+ failureDetailTimeout = null;
135
+ fail(error);
136
+ }, CODEX_APP_SERVER_DETACHED_FAILURE_DETAIL_GRACE_MS);
137
+ }
138
+
139
+ async function finishFromCompletion(status = "completed") {
140
+ try {
141
+ if (!targetTurnId) {
142
+ pendingCompletionStatus = normalizeText(status) || "completed";
143
+ return;
144
+ }
145
+ const authoritative = await resultFromThread().catch(() => ({
146
+ status: "",
147
+ statusType: "",
148
+ text: ""
149
+ }));
150
+ finalText = authoritative.text || finalText;
151
+ if (!finalText) {
152
+ pendingCompletionStatus = normalizeText(status) || "completed";
153
+ const systemError = authoritative.statusType === "systemError" || authoritative.status === "failed";
154
+ failAfterDetailGrace(new Error(systemError
155
+ ? "Codex app-server thread entered a system error before producing an assistant response."
156
+ : "Codex app-server completed without producing an assistant response."));
157
+ return;
158
+ }
159
+ finish({
160
+ status,
161
+ text: finalText,
162
+ threadId: normalizedThreadId,
163
+ turnId: targetTurnId,
164
+ usage
165
+ });
166
+ } catch (error) {
167
+ fail(error);
168
+ }
169
+ }
170
+
171
+ function notificationMatches(notification = {}) {
172
+ const notificationThreadId = codexAppServerNotificationThreadId(notification);
173
+ if (notificationThreadId && notificationThreadId !== normalizedThreadId) {
174
+ return false;
175
+ }
176
+ const notificationTurnId = codexAppServerNotificationTurnId(notification);
177
+ return !targetTurnId || !notificationTurnId || notificationTurnId === targetTurnId;
178
+ }
179
+
180
+ return {
181
+ async completeNow(status = "completed") {
182
+ await finishFromCompletion(status);
183
+ },
184
+ failNow(error) {
185
+ fail(error);
186
+ },
187
+ failAfterDetailGrace(error) {
188
+ failAfterDetailGrace(error);
189
+ },
190
+ setTurnId(turnId = "") {
191
+ targetTurnId = normalizeText(turnId);
192
+ if (pendingFailure) {
193
+ failAfterDetailGrace(pendingFailure);
194
+ return;
195
+ }
196
+ if (pendingCompletionStatus) {
197
+ const status = pendingCompletionStatus;
198
+ pendingCompletionStatus = "";
199
+ void finishFromCompletion(status);
200
+ }
201
+ },
202
+ wait() {
203
+ if (settled) {
204
+ return Promise.reject(new Error("Codex app-server detached turn watcher was already settled."));
205
+ }
206
+ return new Promise((resolve, reject) => {
207
+ resolveWaiter = resolve;
208
+ rejectWaiter = reject;
209
+ if (timeoutMs > 0) {
210
+ timeout = setTimeout(() => {
211
+ fail(new Error("Timed out waiting for Codex app-server response."));
212
+ }, timeoutMs);
213
+ } else {
214
+ // Interactive repair turns may take longer than bounded helper jobs.
215
+ // Retain their watcher until completion, interruption, or connection loss.
216
+ const generation = provider?.currentConnectionGeneration?.();
217
+ connectionCheck = setInterval(() => {
218
+ if (provider?.isAvailable?.() === false ||
219
+ provider?.currentConnectionGeneration?.() !== generation) {
220
+ fail(new Error("Connection to Codex was lost while the temporary AI was working."));
221
+ }
222
+ }, 1000);
223
+ connectionCheck.unref?.();
224
+ }
225
+ unsubscribe = typeof provider?.subscribe === "function"
226
+ ? provider.subscribe((notification = {}) => {
227
+ if (!notificationMatches(notification)) {
228
+ return;
229
+ }
230
+ usage = codexAppServerTurnTokenUsage(notification) || usage;
231
+ const classification = classifyCodexAppServerEvent(notification);
232
+ emitWatcherEvent(classification);
233
+ if (
234
+ classification.kind === "provider_error" &&
235
+ classification.text &&
236
+ codexAppServerNotificationParams(notification).willRetry !== true
237
+ ) {
238
+ const error = new Error(classification.text);
239
+ if (!targetTurnId) {
240
+ pendingFailure = error;
241
+ return;
242
+ }
243
+ fail(error);
244
+ return;
245
+ }
246
+ if (classification.kind === "final_assistant_result" && classification.text) {
247
+ finalText = classification.text;
248
+ if (pendingCompletionStatus && targetTurnId) {
249
+ const status = pendingCompletionStatus;
250
+ pendingCompletionStatus = "";
251
+ clearTimeout(failureDetailTimeout);
252
+ failureDetailTimeout = null;
253
+ void finishFromCompletion(status);
254
+ }
255
+ }
256
+ const method = normalizeText(notification.method);
257
+ if (method !== "turn/completed" && method !== "thread/status/changed") {
258
+ return;
259
+ }
260
+ const status = codexAppServerNotificationTurnStatus(notification) || "completed";
261
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
262
+ const error = new Error(codexAppServerNotificationError(notification) || `Codex app-server turn ${status}.`);
263
+ if (!targetTurnId) {
264
+ pendingFailure = error;
265
+ return;
266
+ }
267
+ failAfterDetailGrace(error);
268
+ return;
269
+ }
270
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
271
+ void finishFromCompletion(status);
272
+ }
273
+ })
274
+ : null;
275
+ });
276
+ }
277
+ };
278
+ }
279
+
280
+ export { createCodexAppServerDetachedTurnWatcher, codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure };
@@ -0,0 +1,2 @@
1
+ export { createConversationTranscript } from "./transcript.js";
2
+ export { createMemoryConversationStorage } from "./memoryStorage.js";
@@ -0,0 +1,76 @@
1
+ /** Reference adapter for transient conversations. Scope must be an app-issued string. */
2
+ export function createMemoryConversationStorage() {
3
+ const conversations = new Map();
4
+ const pending = new Map();
5
+
6
+ function key(scope) {
7
+ if (typeof scope !== "string" || !scope.trim()) throw new TypeError("Conversation scope must be a nonempty string.");
8
+ return scope;
9
+ }
10
+
11
+ function transaction(turns) {
12
+ return {
13
+ async listTurnIds() { return [...turns.keys()]; },
14
+ async nextTurnId() { return String(Math.max(0, ...[...turns.keys()].map(Number).filter(Number.isSafeInteger)) + 1).padStart(6, "0"); },
15
+ async hasMessage(id) {
16
+ return Boolean(id) && [...turns.values()].some((turn) => turn.messages.some((message) => message.messageId === id));
17
+ },
18
+ async readTurn(id) {
19
+ const turn = turns.get(id);
20
+ if (!turn) return null;
21
+ const find = (role) => turn.messages.find((message) => message.role === role) || null;
22
+ const thinking = turn.messages.filter((message) => message.role === "thinking");
23
+ const commentary = turn.messages.filter((message) => message.role === "commentary");
24
+ const activity = [...thinking, ...commentary].sort((a, b) => a.at.localeCompare(b.at));
25
+ const system = find("system"), user = find("user"), assistant = find("assistant");
26
+ return structuredClone({
27
+ turnId: id, user, assistant, thinking, commentary,
28
+ ...(system ? { system } : {}), ...(turn.metadata ? { metadata: turn.metadata } : {}),
29
+ messages: [system, user, ...activity, assistant].filter(Boolean)
30
+ });
31
+ },
32
+ async appendMessage(id, { turnMetadata, ...message }) {
33
+ const turn = turns.get(id) || { messages: [] };
34
+ if (turnMetadata) turn.metadata = structuredClone(turnMetadata);
35
+ turn.messages.push(structuredClone(message));
36
+ turns.set(id, turn);
37
+ },
38
+ async replaceAssistant(id, message) {
39
+ if (!id) throw new TypeError("Assistant replacement requires a turn id.");
40
+ const turn = turns.get(id) || { messages: [] };
41
+ const previous = turn.messages.find((entry) => entry.role === "assistant");
42
+ turn.messages = turn.messages.filter((entry) => entry.role !== "assistant");
43
+ turn.messages.push({ ...previous, ...structuredClone(message), at: previous?.at || message.at });
44
+ turns.set(id, turn);
45
+ }
46
+ };
47
+ }
48
+
49
+ async function write(scope, callback, remove = false) {
50
+ const id = key(scope);
51
+ const previous = pending.get(id) || Promise.resolve();
52
+ const operation = previous.then(async () => {
53
+ const draft = structuredClone(conversations.get(id) || new Map());
54
+ const result = await callback(transaction(draft));
55
+ if (remove) conversations.delete(id);
56
+ else conversations.set(id, draft);
57
+ return result;
58
+ });
59
+ const settled = operation.then(() => undefined, () => undefined);
60
+ pending.set(id, settled);
61
+ try { return await operation; }
62
+ finally { if (pending.get(id) === settled) pending.delete(id); }
63
+ }
64
+
65
+ return Object.freeze({
66
+ write,
67
+ async read(scope, callback) {
68
+ const id = key(scope);
69
+ await pending.get(id);
70
+ return callback(transaction(structuredClone(conversations.get(id) || new Map())));
71
+ },
72
+ async deleteConversation(scope) {
73
+ await write(scope, async () => {}, true);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,7 @@
1
+ export function normalizeText(value) {
2
+ return String(value ?? "").trim();
3
+ }
4
+
5
+ export function isPlainObject(value) {
6
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
7
+ }