@jskit-ai/assistant-core 0.2.24 → 0.2.26
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.
- package/CHANGELOG.md +6 -1
- package/examples/conversation/package.json +6 -6
- package/package.json +8 -8
- package/src/client/components/AssistantSettingsFormCard.vue +14 -9
- package/src/client/conversation/AssistantAttachmentQueue.vue +2 -4
- package/src/client/conversation/AssistantConversationElement.vue +2 -2
- package/src/client/conversation/AssistantMessageAttachments.vue +2 -2
- package/src/client/conversation/AssistantModelControl.vue +5 -4
- package/src/client/conversation/AssistantPromptInput.vue +24 -18
- package/src/client/conversation/AssistantTranscript.css +7 -15
- package/src/client/conversation/LongTextInlineParts.vue +2 -3
- package/src/client/conversation/LongTextPreviewBlocks.vue +7 -6
- package/src/server/conversation/codexProviderOwner.js +6 -1
- package/src/server/conversation/conversationEvents.js +30 -2
- package/src/server/conversation/conversationHandles.js +1 -1
- package/src/server/lib/providers/openAiCompatibleClient.js +12 -8
- package/test/assistantScroll.browser.test.js +53 -103
- package/test/claudeConversationSession.test.js +16 -1
- package/test/componentContracts.test.js +44 -0
- package/test/conversationAvatarThumbnail.test.js +3 -4
- package/test/conversationSession.test.js +63 -0
- package/test/fixtures/nestedListQuestionMessage.md +19 -19
- package/test/longTextBlocks.test.js +14 -14
- package/test/messageDelivery.browser.test.js +12 -14
- package/test/openAiLoading.test.js +25 -0
- package/test/opencodeConversationSession.test.js +20 -4
- package/test/transcriptTheme.browser.test.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
- The
|
|
5
|
+
- The OpenAI SDK loads with an OpenAI-compatible client's first completion instead of with the server, so a boot of an app with the assistant loads 154 fewer modules (0.13 s on their own).
|
|
6
|
+
- A subscriber hears a streamed delta (an in-progress message) on an authorization under a second old instead of authorizing again for each one: every delta used to wait for one `authorize` call per subscriber before the stream continued (one SELECT each in `@jskit-ai/assistant-runtime`), so a 200-delta reply to one open page cost about 200 reads and now costs as many as a 10-delta one. Every other event (admission, phase, saved message, settlement) still authorizes again, and a revoked subscriber stops hearing the turn within a second.
|
|
7
|
+
- The conversation's remaining boxes are Material 3 tonal containers instead of outlines, visual only: queued and sent attachments and the questions are `surface-container-high`, code blocks and inline code `surface-container-highest`, tables and details `surface-container-low` (a table keeps `outline-variant` row dividers), question numbers `secondary-container` badges, and the model menu a `surface-container` surface at elevation level 2 with `outline-variant` dividers. The composer stays Material 3's outlined text field, the only outline left.
|
|
8
|
+
- `AssistantSettingsFormCard` takes a `formId`: its form gets that id and the card draws no Save button of its own, so the page's app bar can carry Save as a submit button for the form (`type="submit" form="<formId>"`), as a Material 3 settings pane does. Without one, the card keeps its Save; a person who may not save still sees the read-only note.
|
|
9
|
+
- Material 3 containers in the conversation, visual only: the transcript area is no longer outlined (its bubbles are the only containers, and the welcome bubble is a tonal fill without an outline), the composer is Material 3's outlined text field (4dp corners, the `outline` role, a 2dp primary outline on focus, 16dp side padding) with Send and the other actions 12dp from it (8dp when compact), and `AssistantSettingsFormCard` adds no heading or divider inside the settings pane that titles it: its subtitle is the card's first line and its title names the section for assistive technology.
|
|
10
|
+
- The avatar's Show and Minimise buttons set no fixed size: they are 40dp icon buttons with material-web's 48dp touch target, so `jskit check m3` passes on `AssistantConversationElement`. The collapsed Show button is 40dp wide like Minimise, no longer 44dp.
|
|
6
11
|
- The goal menu closes when Cancel succeeds, as it does after Start; Pause and Resume keep it open.
|
|
7
12
|
- `AssistantConversationElement` has an `avatar-thumbnail` slot, with the `{ size, height }` scope of the other avatar slots: while the avatar is hidden, the **Show avatar** button draws its content instead of the anonymous account icon, such as a still miniature of the avatar, clipped to the button's round 32-pixel disc. The button keeps its label, title and 44-pixel target, and shows the icon as before without the slot.
|
|
8
13
|
- A delivered Codex turn is held as not yet started only until Codex reports it: a `turn/started` that arrives before the `turn/start` answer counts, and a thread read listing exactly that turn as completed or failed settles a turn whose notifications were missed, so its answer is no longer kept waiting for a notification that never comes.
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
13
|
-
"@jskit-ai/kernel": "0.2.
|
|
14
|
-
"@jskit-ai/material-web": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.2.26",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.2.12",
|
|
13
|
+
"@jskit-ai/kernel": "0.2.12",
|
|
14
|
+
"@jskit-ai/material-web": "0.1.3",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.2.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.2.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.2.13",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.2.13"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "
|
|
6
|
+
"test": "jskit test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
52
52
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
53
53
|
"@ai-sdk/xai": "^4.0.58",
|
|
54
|
-
"@jskit-ai/agent": "0.1.
|
|
55
|
-
"@jskit-ai/resource-core": "0.1.
|
|
56
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
54
|
+
"@jskit-ai/agent": "0.1.2",
|
|
55
|
+
"@jskit-ai/resource-core": "0.1.203",
|
|
56
|
+
"@jskit-ai/resource-crud-core": "0.1.203",
|
|
57
57
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
58
58
|
"ai": "^7.0.152",
|
|
59
59
|
"json-rest-schema": "^1.0.17",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"ws": "^8.20.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
65
|
-
"@jskit-ai/kernel": "0.2.
|
|
64
|
+
"@jskit-ai/http-runtime": "0.2.12",
|
|
65
|
+
"@jskit-ai/kernel": "0.2.12",
|
|
66
66
|
"vue": "^3.5.13",
|
|
67
67
|
"vuetify": "^4.3.0"
|
|
68
68
|
},
|
|
@@ -220,6 +220,6 @@
|
|
|
220
220
|
}
|
|
221
221
|
},
|
|
222
222
|
"devDependencies": {
|
|
223
|
-
"@jskit-ai/material-web": "0.1.
|
|
223
|
+
"@jskit-ai/material-web": "0.1.3"
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- The settings pane around it carries the page title, so the card adds no heading of its own: the title names the
|
|
3
|
+
section for assistive technology and the subtitle says what the settings apply to. -->
|
|
4
|
+
<section :class="props.rootClass" :aria-label="props.title">
|
|
3
5
|
<v-card>
|
|
4
|
-
<v-card-
|
|
5
|
-
<
|
|
6
|
-
<v-card-subtitle>{{ props.subtitle }}</v-card-subtitle>
|
|
7
|
-
</v-card-item>
|
|
8
|
-
<v-divider />
|
|
9
|
-
<v-card-text class="pt-4">
|
|
6
|
+
<v-card-text>
|
|
7
|
+
<p class="text-body-medium on-surface-variant mb-4">{{ props.subtitle }}</p>
|
|
10
8
|
<template v-if="props.showFormSkeleton">
|
|
11
9
|
<v-skeleton-loader type="text@2, list-item-two-line@4, button" />
|
|
12
10
|
</template>
|
|
@@ -20,10 +18,12 @@
|
|
|
20
18
|
</p>
|
|
21
19
|
|
|
22
20
|
<template v-else>
|
|
23
|
-
|
|
21
|
+
<!-- With a formId, the page's app bar carries Save (a submit button for this form), as a settings pane does;
|
|
22
|
+
the card then shows only the read-only note. -->
|
|
23
|
+
<v-form :id="props.formId || undefined" @submit.prevent="props.addEdit.submit" novalidate>
|
|
24
24
|
<v-progress-linear v-if="props.addEdit.isRefetching" indeterminate class="mb-4" />
|
|
25
25
|
<slot />
|
|
26
|
-
<div class="d-flex align-center justify-end ga-3 mt-2">
|
|
26
|
+
<div v-if="!props.formId || !props.addEdit.canSave" class="d-flex align-center justify-end ga-3 mt-2">
|
|
27
27
|
<v-btn
|
|
28
28
|
v-if="props.addEdit.canSave"
|
|
29
29
|
type="submit"
|
|
@@ -74,6 +74,11 @@ const props = defineProps({
|
|
|
74
74
|
showFormSkeleton: {
|
|
75
75
|
type: Boolean,
|
|
76
76
|
default: false
|
|
77
|
+
},
|
|
78
|
+
// The form's id, when the page's app bar carries its Save button (type="submit" form="<formId>").
|
|
79
|
+
formId: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: ""
|
|
77
82
|
}
|
|
78
83
|
});
|
|
79
84
|
</script>
|
|
@@ -307,10 +307,9 @@ onBeforeUnmount(() => {
|
|
|
307
307
|
</script>
|
|
308
308
|
|
|
309
309
|
<style scoped>
|
|
310
|
+
/* A Material 3 tonal container above the composer: a surface-container-high fill, no outline. */
|
|
310
311
|
.assistant-attachment-queue {
|
|
311
|
-
background:
|
|
312
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
313
|
-
border-bottom: 0;
|
|
312
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
314
313
|
border-radius: 14px 14px 0 0;
|
|
315
314
|
color: rgb(var(--v-theme-on-surface));
|
|
316
315
|
display: grid;
|
|
@@ -332,7 +331,6 @@ onBeforeUnmount(() => {
|
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
.assistant-attachment-queue--standalone {
|
|
335
|
-
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
336
334
|
border-radius: 10px;
|
|
337
335
|
}
|
|
338
336
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<slot name="avatar-control" :size="avatarSize" :height="avatarHeight" />
|
|
9
9
|
<v-btn
|
|
10
10
|
icon="$accountCircle" size="small"
|
|
11
|
-
variant="text"
|
|
11
|
+
variant="text"
|
|
12
12
|
class="assistant-conversation__avatar-size"
|
|
13
13
|
:aria-label="t('jskit.assistant.chat.showAvatar')" :title="t('jskit.assistant.chat.showAvatar')"
|
|
14
14
|
:aria-expanded="avatarSize !== 'hidden'" @click="toggleAvatar"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<slot name="avatar-tools" :size="avatarSize" :height="avatarHeight" />
|
|
27
27
|
<v-btn
|
|
28
28
|
icon="$minus" size="small" variant="text" color="primary"
|
|
29
|
-
|
|
29
|
+
class="assistant-conversation__avatar-size"
|
|
30
30
|
:aria-label="t('jskit.assistant.chat.minimiseAvatar')" :title="t('jskit.assistant.chat.minimiseAvatar')" :aria-expanded="true"
|
|
31
31
|
@click="toggleAvatar"
|
|
32
32
|
>
|
|
@@ -70,10 +70,10 @@ function attachmentKey(attachment, index) {
|
|
|
70
70
|
width: min(19rem, 100%);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* Each attachment is a Material 3 tonal container: a surface-container-high fill, no outline. */
|
|
73
74
|
.assistant-conversation-attachments__item {
|
|
74
75
|
align-items: center;
|
|
75
|
-
background:
|
|
76
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
76
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
77
77
|
border-radius: 10px;
|
|
78
78
|
color: rgb(var(--v-theme-on-surface));
|
|
79
79
|
min-height: 2.8rem;
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
/>
|
|
21
21
|
</template>
|
|
22
22
|
|
|
23
|
+
<!-- A Material 3 menu surface: surface-container at elevation level 2, no outline. -->
|
|
23
24
|
<v-sheet
|
|
24
25
|
:aria-label="t('jskit.assistant.model.selector')"
|
|
25
|
-
border
|
|
26
26
|
class="assistant-model-control"
|
|
27
|
-
|
|
27
|
+
color="surface-container"
|
|
28
|
+
:elevation="2"
|
|
28
29
|
rounded="xl"
|
|
29
30
|
>
|
|
30
31
|
<header class="assistant-model-control__header">
|
|
@@ -235,7 +236,7 @@ function selectVariant(value) { if (!props.changesDisabled && !props.saving) emi
|
|
|
235
236
|
|
|
236
237
|
.assistant-model-control__header {
|
|
237
238
|
align-items: center;
|
|
238
|
-
border-bottom: 1px solid
|
|
239
|
+
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
|
|
239
240
|
display: flex;
|
|
240
241
|
gap: 0.65rem;
|
|
241
242
|
padding: 0.1rem 0.1rem 0.7rem;
|
|
@@ -311,7 +312,7 @@ function selectVariant(value) { if (!props.changesDisabled && !props.saving) emi
|
|
|
311
312
|
|
|
312
313
|
.assistant-model-control__actions {
|
|
313
314
|
align-items: center;
|
|
314
|
-
border-top: 1px solid
|
|
315
|
+
border-top: 1px solid rgb(var(--v-theme-outline-variant));
|
|
315
316
|
display: grid;
|
|
316
317
|
gap: 0.4rem;
|
|
317
318
|
grid-template-columns: auto 1fr auto;
|
|
@@ -261,12 +261,13 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
261
261
|
width: 100%;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/* Material 3's outlined text field: 4dp corners, a 1dp outline that becomes a 2dp primary one on focus (drawn inset, so
|
|
265
|
+
nothing moves), 16dp side padding, and the label in the outline. */
|
|
264
266
|
.assistant-prompt-input__field {
|
|
265
267
|
background: rgb(var(--v-theme-surface));
|
|
266
|
-
border: 1px solid
|
|
267
|
-
border-radius:
|
|
268
|
+
border: 1px solid rgb(var(--v-theme-outline));
|
|
269
|
+
border-radius: 4px;
|
|
268
270
|
box-sizing: border-box;
|
|
269
|
-
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.08);
|
|
270
271
|
display: grid;
|
|
271
272
|
max-width: 100%;
|
|
272
273
|
min-width: 0;
|
|
@@ -274,29 +275,33 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
274
275
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
275
276
|
}
|
|
276
277
|
|
|
278
|
+
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):hover {
|
|
279
|
+
border-color: rgb(var(--v-theme-on-surface));
|
|
280
|
+
}
|
|
281
|
+
|
|
277
282
|
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within {
|
|
278
283
|
border-color: rgb(var(--v-theme-primary));
|
|
279
|
-
box-shadow:
|
|
280
|
-
0 0 0 2px rgba(var(--v-theme-primary), 0.28),
|
|
281
|
-
inset 0 0 0 1px rgba(var(--v-theme-primary), 0.2);
|
|
284
|
+
box-shadow: inset 0 0 0 1px rgb(var(--v-theme-primary));
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
.assistant-prompt-input__field--disabled {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
border-color: rgba(var(--v-theme-on-surface), 0.12);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within .assistant-prompt-input__label {
|
|
292
|
+
color: rgb(var(--v-theme-primary));
|
|
288
293
|
}
|
|
289
294
|
|
|
290
295
|
.assistant-prompt-input__label {
|
|
291
296
|
align-self: start;
|
|
292
297
|
background: rgb(var(--v-theme-surface));
|
|
293
|
-
color:
|
|
294
|
-
font-size: 0.
|
|
295
|
-
line-height:
|
|
296
|
-
margin: -0.5rem 0 0
|
|
297
|
-
max-width: calc(100% -
|
|
298
|
+
color: rgb(var(--v-theme-on-surface-variant));
|
|
299
|
+
font-size: 0.75rem;
|
|
300
|
+
line-height: 1rem;
|
|
301
|
+
margin: -0.5rem 0 0 12px;
|
|
302
|
+
max-width: calc(100% - 24px);
|
|
298
303
|
overflow: hidden;
|
|
299
|
-
padding-inline:
|
|
304
|
+
padding-inline: 4px;
|
|
300
305
|
text-overflow: ellipsis;
|
|
301
306
|
white-space: nowrap;
|
|
302
307
|
width: fit-content;
|
|
@@ -349,19 +354,20 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
349
354
|
opacity: 1;
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
/* Send and the other actions keep 12dp from the outline (8dp in the compact composer). */
|
|
352
358
|
.assistant-prompt-input__footer {
|
|
353
359
|
min-width: 0;
|
|
354
|
-
padding: 0
|
|
360
|
+
padding: 0 12px 12px;
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
.assistant-prompt-input--compact .assistant-prompt-input__input {
|
|
358
364
|
max-height: min(8rem, 20dvh);
|
|
359
365
|
min-height: 2.5rem;
|
|
360
|
-
padding: 0.4rem
|
|
366
|
+
padding: 0.4rem 12px 0.15rem;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
.assistant-prompt-input--compact .assistant-prompt-input__footer {
|
|
364
|
-
padding: 0
|
|
370
|
+
padding: 0 8px 8px;
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
.assistant-prompt-input__details {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
.assistant-transcript__plain-user { white-space: pre-wrap; margin: 0; }
|
|
2
|
+
/* The transcript is the page's own area, not a container: its bubbles are the only containers in it. */
|
|
2
3
|
.assistant-transcript {
|
|
3
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.24);
|
|
4
|
-
border-radius: 8px;
|
|
5
4
|
display: grid;
|
|
6
5
|
gap: 0.35rem;
|
|
7
6
|
grid-template-rows: minmax(0, 1fr);
|
|
@@ -87,7 +86,6 @@
|
|
|
87
86
|
|
|
88
87
|
.assistant-transcript__welcome {
|
|
89
88
|
background: rgba(var(--v-theme-primary), 0.08);
|
|
90
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.18);
|
|
91
89
|
border-radius: 16px;
|
|
92
90
|
color: rgb(var(--v-theme-on-surface));
|
|
93
91
|
display: grid;
|
|
@@ -239,10 +237,6 @@
|
|
|
239
237
|
padding: 0;
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
.assistant-transcript--task {
|
|
243
|
-
border-color: rgba(126, 87, 194, 0.32);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
240
|
.assistant-transcript--task .assistant-transcript__avatar--assistant {
|
|
247
241
|
background: #7e57c2;
|
|
248
242
|
}
|
|
@@ -416,10 +410,9 @@
|
|
|
416
410
|
.assistant-transcript__message--user :deep(.studio-long-text-review__table th > code),
|
|
417
411
|
.assistant-transcript__message--user :deep(.studio-long-text-review__table td > code),
|
|
418
412
|
.assistant-transcript__message--user :deep(.studio-long-text-review__details-summary code) {
|
|
419
|
-
background:
|
|
420
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.14);
|
|
413
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
421
414
|
border-radius: 3px;
|
|
422
|
-
color:
|
|
415
|
+
color: rgb(var(--v-theme-on-surface));
|
|
423
416
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
424
417
|
font-size: 0.9em;
|
|
425
418
|
padding: 0.02rem 0.2rem;
|
|
@@ -436,10 +429,10 @@
|
|
|
436
429
|
padding: 0;
|
|
437
430
|
}
|
|
438
431
|
|
|
432
|
+
/* Questions are tonal containers (surface-container-high), their numbers secondary-container badges: no outlines. */
|
|
439
433
|
.assistant-transcript__question {
|
|
440
434
|
align-items: start;
|
|
441
|
-
background:
|
|
442
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
435
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
443
436
|
border-radius: 8px;
|
|
444
437
|
box-sizing: border-box;
|
|
445
438
|
display: grid;
|
|
@@ -452,10 +445,9 @@
|
|
|
452
445
|
|
|
453
446
|
.assistant-transcript__question-number {
|
|
454
447
|
align-items: center;
|
|
455
|
-
background:
|
|
456
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
|
448
|
+
background: rgb(var(--v-theme-secondary-container));
|
|
457
449
|
border-radius: 999px;
|
|
458
|
-
color: rgb(var(--v-theme-
|
|
450
|
+
color: rgb(var(--v-theme-on-secondary-container));
|
|
459
451
|
display: inline-flex;
|
|
460
452
|
font-size: 0.72rem;
|
|
461
453
|
font-weight: 760;
|
|
@@ -44,10 +44,9 @@ function handleLinkClick(event, part = {}) {
|
|
|
44
44
|
|
|
45
45
|
<style scoped>
|
|
46
46
|
code {
|
|
47
|
-
background:
|
|
48
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.14);
|
|
47
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
49
48
|
border-radius: 3px;
|
|
50
|
-
color:
|
|
49
|
+
color: rgb(var(--v-theme-on-surface));
|
|
51
50
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
52
51
|
font-size: 0.9em;
|
|
53
52
|
overflow-wrap: anywhere;
|
|
@@ -240,9 +240,10 @@ h5.studio-long-text-review__heading {
|
|
|
240
240
|
font-size: 0.8rem;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/* Code, tables and details are Material 3 tonal containers (surface-container tones), not outlined boxes; a
|
|
244
|
+
table keeps outline-variant dividers between its rows. */
|
|
243
245
|
.studio-long-text-review__code {
|
|
244
|
-
background:
|
|
245
|
-
border: 1px solid rgba(var(--v-border-color), 0.26);
|
|
246
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
246
247
|
border-radius: 8px;
|
|
247
248
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
248
249
|
font-size: 0.82rem;
|
|
@@ -256,7 +257,7 @@ h5.studio-long-text-review__heading {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
.studio-long-text-review__table-wrap {
|
|
259
|
-
|
|
260
|
+
background: rgb(var(--v-theme-surface-container-low));
|
|
260
261
|
border-radius: 8px;
|
|
261
262
|
inline-size: fit-content;
|
|
262
263
|
margin-block: 0.35rem;
|
|
@@ -282,7 +283,7 @@ h5.studio-long-text-review__heading {
|
|
|
282
283
|
|
|
283
284
|
.studio-long-text-review__table th,
|
|
284
285
|
.studio-long-text-review__table td {
|
|
285
|
-
border-bottom: 1px solid
|
|
286
|
+
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
|
|
286
287
|
max-inline-size: 28rem;
|
|
287
288
|
padding: 0.55rem 0.75rem;
|
|
288
289
|
text-align: left;
|
|
@@ -290,7 +291,7 @@ h5.studio-long-text-review__heading {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
.studio-long-text-review__table th {
|
|
293
|
-
background:
|
|
294
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
294
295
|
color: rgb(var(--v-theme-on-surface));
|
|
295
296
|
font-weight: 760;
|
|
296
297
|
position: sticky;
|
|
@@ -320,7 +321,7 @@ h5.studio-long-text-review__heading {
|
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
.studio-long-text-review__details {
|
|
323
|
-
|
|
324
|
+
background: rgb(var(--v-theme-surface-container-low));
|
|
324
325
|
border-radius: 8px;
|
|
325
326
|
display: block;
|
|
326
327
|
min-width: 0;
|
|
@@ -550,7 +550,12 @@ export function createCodexAppServerProviderOwner({
|
|
|
550
550
|
assertOpen();
|
|
551
551
|
if (!record.observationStop || record.observationStop.error !== error) {
|
|
552
552
|
reportRecoveryEvent(record, "lost", error);
|
|
553
|
-
|
|
553
|
+
// A shared stop of this native runtime gives each of its peers the failure that started it, and records their
|
|
554
|
+
// barriers and completion itself. A peer that then loses its own connection to the stopped process joins that
|
|
555
|
+
// stop instead of stopping the runtime again, which would reopen its stopped barrier while the first finishes.
|
|
556
|
+
const runtimeKey = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
|
|
557
|
+
const shared = runtimePeers(runtimeKey).find(peer => peer !== record && peer.observationStop?.error === error);
|
|
558
|
+
const stop = { error, promise: shared?.observationStop.promise || invalidation.suspendUnobservedProvider(record, error) };
|
|
554
559
|
record.observationStop = stop;
|
|
555
560
|
const tracked = stop.promise.catch(failure => {
|
|
556
561
|
reportRecoveryEvent(record, "stopFailed", failure);
|
|
@@ -4,6 +4,9 @@ import { submittedMessage } from "./conversationReads.js";
|
|
|
4
4
|
|
|
5
5
|
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
6
6
|
|
|
7
|
+
// How long a subscriber's authorization covers the streamed deltas that follow it.
|
|
8
|
+
const STREAMED_DELTA_AUTHORIZATION_MS = 1000;
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Runs one conversation entry's operations in order: each waits for the one before it.
|
|
9
12
|
* @param {ConversationEntry} entry
|
|
@@ -36,16 +39,40 @@ function createConversationEvents({ authorize, isClosed, storage, streams, compl
|
|
|
36
39
|
* @param {Set<string>} [grant] The operations this call already authorized.
|
|
37
40
|
*/
|
|
38
41
|
async function access(context, id, operation, entry, grant) {
|
|
39
|
-
|
|
42
|
+
assertOpen(entry);
|
|
40
43
|
if (!grant?.has(operation)) {
|
|
41
44
|
if (!await authorize({ context, conversationId: id, operation })) {
|
|
42
45
|
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
43
46
|
}
|
|
44
47
|
grant?.add(operation);
|
|
45
48
|
}
|
|
49
|
+
assertOpen(entry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {ConversationEntry} [entry] */
|
|
53
|
+
function assertOpen(entry) {
|
|
46
54
|
if (isClosed() || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_handle_closed", 409, { messageCode: "assistant.conversation.handleClosed" });
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A subscriber hears an event once it is authorized again. A streamed delta (an in-progress
|
|
59
|
+
* message) uses the subscriber's last authorization while it is under a second old: a reply
|
|
60
|
+
* streams hundreds of deltas, each authorization is a read the stream waits for, and a
|
|
61
|
+
* revoked subscriber still stops hearing the turn within a second. Every other event (an
|
|
62
|
+
* admission, a phase, a saved message, a settlement) authorizes again.
|
|
63
|
+
* @param {ConversationEntry} entry
|
|
64
|
+
* @param {{ context: object, authorizedAt?: number }} subscription
|
|
65
|
+
* @param {boolean} streamed
|
|
66
|
+
*/
|
|
67
|
+
async function admitSubscriber(entry, subscription, streamed) {
|
|
68
|
+
if (streamed && Date.now() - subscription.authorizedAt < STREAMED_DELTA_AUTHORIZATION_MS) {
|
|
69
|
+
assertOpen(entry);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await access(subscription.context, entry.id, "subscribe", entry);
|
|
73
|
+
subscription.authorizedAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/**
|
|
50
77
|
* @param {ConversationEntry} entry
|
|
51
78
|
* @param {object} event
|
|
@@ -80,8 +107,9 @@ function createConversationEvents({ authorize, isClosed, storage, streams, compl
|
|
|
80
107
|
.some(message => message.messageId === payload.messageId && message.completedEnvelope === true && message.origin === "application");
|
|
81
108
|
if (marked) payload.completedEnvelope = true;
|
|
82
109
|
}
|
|
110
|
+
const streamed = payload.type === "message" && payload.status === "inProgress";
|
|
83
111
|
await Promise.all([...entry.listeners].map(async subscription => {
|
|
84
|
-
try { await
|
|
112
|
+
try { await admitSubscriber(entry, subscription, streamed); }
|
|
85
113
|
catch { entry.listeners.delete(subscription); return; }
|
|
86
114
|
if (isCurrent && !isCurrent()) return;
|
|
87
115
|
// Presentation failures cannot interrupt persistence or provider work.
|
|
@@ -212,7 +212,7 @@ function createConversationHandles({ entries, storage, defaultHost, events, read
|
|
|
212
212
|
await events.access(context, id, "subscribe", entry);
|
|
213
213
|
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_event_delivery_retained", 409, { messageCode: "assistant.conversation.eventDeliveryRetained" });
|
|
214
214
|
if (typeof listener !== "function") throw new TypeError("A conversation listener is required.");
|
|
215
|
-
const subscription = { context, listener };
|
|
215
|
+
const subscription = { context, listener, authorizedAt: Date.now() };
|
|
216
216
|
entry.listeners.add(subscription);
|
|
217
217
|
return () => entry.listeners.delete(subscription);
|
|
218
218
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
2
|
import { chatModel } from "@jskit-ai/agent/server";
|
|
4
3
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
@@ -37,11 +36,16 @@ function createOpenAiCompatibleClient({
|
|
|
37
36
|
const normalizedBaseUrl = normalizeOptionalHttpUrl(baseUrl, {
|
|
38
37
|
context: `assistant ${normalizedProvider} baseUrl`
|
|
39
38
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
// The OpenAI SDK (150 modules) loads with the first completion, not with every server that could ask for one.
|
|
40
|
+
let clientPromise = null;
|
|
41
|
+
const chatCompletions = async () => {
|
|
42
|
+
clientPromise ??= import("openai").then(({ default: OpenAI }) => new OpenAI({
|
|
43
|
+
apiKey: normalizedApiKey,
|
|
44
|
+
...(normalizedBaseUrl ? { baseURL: normalizedBaseUrl } : {}),
|
|
45
|
+
timeout: normalizeTimeoutMs(timeoutMs)
|
|
46
|
+
}));
|
|
47
|
+
return (await clientPromise).chat.completions;
|
|
48
|
+
};
|
|
45
49
|
|
|
46
50
|
const baseURL = normalizedBaseUrl || undefined;
|
|
47
51
|
|
|
@@ -54,11 +58,11 @@ function createOpenAiCompatibleClient({
|
|
|
54
58
|
stream: false
|
|
55
59
|
};
|
|
56
60
|
|
|
57
|
-
return
|
|
61
|
+
return (await chatCompletions()).create(requestPayload, signal ? { signal } : undefined);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
async function createChatCompletionStream({ messages = [], tools = [], signal, temperature = 0.2 } = {}) {
|
|
61
|
-
return
|
|
65
|
+
return (await chatCompletions()).create(
|
|
62
66
|
{
|
|
63
67
|
model: normalizedModel,
|
|
64
68
|
messages,
|