@jskit-ai/assistant-core 0.2.25 → 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 +2 -0
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- 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/claudeConversationSession.test.js +15 -2
- package/test/conversationSession.test.js +63 -0
- package/test/fixtures/nestedListQuestionMessage.md +19 -19
- package/test/longTextBlocks.test.js +14 -14
- package/test/openAiLoading.test.js +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
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.
|
|
5
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.
|
|
6
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.
|
|
7
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.
|
|
@@ -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.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.2.26",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.2.12",
|
|
13
|
+
"@jskit-ai/kernel": "0.2.12",
|
|
14
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,6 +1,6 @@
|
|
|
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
6
|
"test": "jskit test"
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
53
53
|
"@ai-sdk/xai": "^4.0.58",
|
|
54
54
|
"@jskit-ai/agent": "0.1.2",
|
|
55
|
-
"@jskit-ai/resource-core": "0.1.
|
|
56
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
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
|
},
|
|
@@ -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,
|
|
@@ -760,9 +760,22 @@ test("Claude cancellation waits for an application effect after closing the nati
|
|
|
760
760
|
|
|
761
761
|
test("the common request deadline stops an unfinished native turn and retains its admitted history", async t => {
|
|
762
762
|
// The deadline also covers starting the native process and admitting the turn, which took up to 8.4 s (median 1.3 to
|
|
763
|
-
// 2.5 s) with 4 to 8 copies of this test running on an 8-thread host
|
|
764
|
-
|
|
763
|
+
// 2.5 s) with 4 to 8 copies of this test running on an 8-thread host. Rather than a deadline long enough to outlast
|
|
764
|
+
// that, the test holds the request's deadline timer and runs it once the turn is admitted, so it always stops an
|
|
765
|
+
// admitted turn and never waits for the clock.
|
|
766
|
+
const timeoutMs = 10_000;
|
|
767
|
+
const setTimer = globalThis.setTimeout;
|
|
768
|
+
let deadline = null;
|
|
769
|
+
t.mock.method(globalThis, "setTimeout", (callback, delay, ...args) => {
|
|
770
|
+
if (deadline || delay !== timeoutMs) return setTimer(callback, delay, ...args);
|
|
771
|
+
deadline = () => callback(...args);
|
|
772
|
+
return setTimer(() => {}, 0);
|
|
773
|
+
});
|
|
774
|
+
const f = await fixture(t, { limits: { timeoutMs } });
|
|
765
775
|
await f.conversation.send({ messageId: "waiting", text: "wait" });
|
|
776
|
+
assert.equal(typeof deadline, "function", "The request armed its deadline with the configured time limit");
|
|
777
|
+
assert.equal((await f.trace()).filter(row => row.frame?.type === "user").length, 1, "The turn was admitted");
|
|
778
|
+
deadline();
|
|
766
779
|
const snapshot = await f.conversation.wait();
|
|
767
780
|
assert.equal(snapshot.conversationLog[0].metadata.runtime.status, "failed");
|
|
768
781
|
assert.match(snapshot.error, /time limit/);
|
|
@@ -75,6 +75,69 @@ test("API runtime persists one admission for concurrent identical sends and stre
|
|
|
75
75
|
assert.equal(f.requests.length, 1);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// A reply streamed as `deltas` chunks to one subscriber, each written once the one before it
|
|
79
|
+
// was heard, with Date frozen from the send on (a streamed delta reuses a subscriber's
|
|
80
|
+
// authorization while it is under a second old). `during` runs before the reply finishes.
|
|
81
|
+
async function streamedReply(t, deltas, { subscriberAllowed = () => true, during } = {}) {
|
|
82
|
+
const source = new TransformStream();
|
|
83
|
+
const writer = source.writable.getWriter();
|
|
84
|
+
const write = text => writer.write(new TextEncoder().encode(text));
|
|
85
|
+
let subscribeChecks = 0;
|
|
86
|
+
const f = await fixture(t, {
|
|
87
|
+
authorize: ({ operation }) => operation !== "subscribe" || (subscribeChecks += 1, subscriberAllowed()),
|
|
88
|
+
fetch: () => new Response(source.readable, { headers: { "content-type": "text/event-stream" } })
|
|
89
|
+
});
|
|
90
|
+
const heard = [];
|
|
91
|
+
let deltaHeard = Promise.withResolvers();
|
|
92
|
+
await f.conversation.subscribe(event => {
|
|
93
|
+
heard.push(event);
|
|
94
|
+
if (event.type === "message" && event.status === "inProgress") deltaHeard.resolve();
|
|
95
|
+
});
|
|
96
|
+
t.mock.timers.enable({ apis: ["Date"], now: Date.now() });
|
|
97
|
+
await f.conversation.send(input);
|
|
98
|
+
for (let index = 0; index < deltas; index += 1) {
|
|
99
|
+
deltaHeard = Promise.withResolvers();
|
|
100
|
+
await write(event(`w${index} `));
|
|
101
|
+
await deltaHeard.promise;
|
|
102
|
+
}
|
|
103
|
+
await during?.({ heard, delta: text => write(event(text)) });
|
|
104
|
+
await write(event("", true));
|
|
105
|
+
await writer.close();
|
|
106
|
+
const result = await f.conversation.wait();
|
|
107
|
+
t.mock.timers.reset();
|
|
108
|
+
return { result, heard, subscribeChecks };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const words = count => Array.from({ length: count }, (_, index) => `w${index}`).join(" ");
|
|
112
|
+
|
|
113
|
+
test("a streamed reply authorizes its subscriber as often for 200 deltas as for 10", async t => {
|
|
114
|
+
const short = await streamedReply(t, 10);
|
|
115
|
+
const long = await streamedReply(t, 200);
|
|
116
|
+
assert.equal(short.result.conversationLog[0].assistant.text.trim(), words(10));
|
|
117
|
+
assert.equal(long.result.conversationLog[0].assistant.text.trim(), words(200));
|
|
118
|
+
assert.equal(long.heard.filter(event => event.type === "message" && event.status === "inProgress").length, 200,
|
|
119
|
+
"The subscriber hears every delta");
|
|
120
|
+
assert.equal(long.heard.at(-1).type, "settled");
|
|
121
|
+
assert.equal(long.subscribeChecks, short.subscribeChecks, "Streamed deltas reuse the subscriber's authorization for a second");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a subscriber whose access is revoked stops hearing a streamed reply within a second", async t => {
|
|
125
|
+
let allowed = true;
|
|
126
|
+
let heardAtRevocation = 0;
|
|
127
|
+
const { result, heard } = await streamedReply(t, 3, {
|
|
128
|
+
subscriberAllowed: () => allowed,
|
|
129
|
+
during: async ({ heard: heardSoFar, delta }) => {
|
|
130
|
+
allowed = false;
|
|
131
|
+
heardAtRevocation = heardSoFar.length;
|
|
132
|
+
t.mock.timers.tick(1000);
|
|
133
|
+
await delta("after revocation");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
assert.equal(result.conversationLog[0].assistant.text, "w0 w1 w2 after revocation", "The turn itself continues");
|
|
137
|
+
assert.ok(heardAtRevocation > 3);
|
|
138
|
+
assert.deepEqual(heard.slice(heardAtRevocation), [], "Nothing reaches the subscriber once its authorization is a second old");
|
|
139
|
+
});
|
|
140
|
+
|
|
78
141
|
test("saved history and current instructions use the existing authorized model connection", async t => {
|
|
79
142
|
const f = await fixture(t);
|
|
80
143
|
await f.conversation.send(input);
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
Here's how I'd run a final
|
|
1
|
+
Here's how I'd run a final check of the garden planner in three steps: I do a careful pass on my own first, then a short session with you for the parts that need you, then I tidy up.
|
|
2
2
|
|
|
3
|
-
**Step 1: I
|
|
4
|
-
- **
|
|
5
|
-
- **Gather the "
|
|
6
|
-
- **I can
|
|
7
|
-
- **Needs you:** real accounts,
|
|
8
|
-
- **Can't be
|
|
9
|
-
- **Output:** one gap list, saved to
|
|
3
|
+
**Step 1: I check on my own (no need for you)**
|
|
4
|
+
- **Try every screen.** I open every screen and compare it against the design notes: every button, the + menu, every right-click menu (beds, plants, seasons, saved plans), every keyboard shortcut on the help sheet, and every settings row. Anything with no note is a gap.
|
|
5
|
+
- **Gather the "open question" lists.** Every design note has one, and I'll merge them all into a single gap list. Each item gets labelled as one of three kinds:
|
|
6
|
+
- **I can check it now:** for example, renaming and deleting beds, or a plan made from a template.
|
|
7
|
+
- **Needs you:** real accounts, printing, a second account, payment, your tablet, links that open in a browser.
|
|
8
|
+
- **Can't be checked at all.**
|
|
9
|
+
- **Output:** one gap list, saved to the shared folder, with every item marked done, checkable, needs you, or impossible.
|
|
10
10
|
|
|
11
11
|
**Step 2: a session with you, for the "needs you" items and things only you know about**
|
|
12
|
-
- **You
|
|
13
|
-
- **I
|
|
14
|
-
- **Things outside the
|
|
12
|
+
- **You use the planner on your own computer**, as you normally would: your knowledge of features, the test account, the tablet app, links opening in your browser.
|
|
13
|
+
- **I take notes as you go** and capture screenshots when you tell me something interesting is happening. Just tell me what you did in plain words ("now I'm printing a plan", "this is the share link in my browser").
|
|
14
|
+
- **Things outside the planner itself:** put screenshots in the shared folder, for example from the tablet app, the help pages, or emails it sends. I'll file them.
|
|
15
15
|
- **What I need from you:**
|
|
16
16
|
- which features you know or suspect exist that I haven't covered (I'll ask about specific suspects from my list)
|
|
17
|
-
- the test
|
|
17
|
+
- the test account, so I can see the real signed-in states
|
|
18
18
|
- your call on anything that sends real data or costs money
|
|
19
19
|
|
|
20
20
|
**Step 3: I tidy up**
|
|
21
|
-
- **For
|
|
22
|
-
- Newly
|
|
23
|
-
- Each one gets a
|
|
21
|
+
- **For the design notes:**
|
|
22
|
+
- Newly found screens go into the notes as additions to the screen map.
|
|
23
|
+
- Each one gets a short note, written when you or the team ask for it. The release plan allows that.
|
|
24
24
|
- **For you:** a final coverage summary.
|
|
25
25
|
|
|
26
|
-
**One thing to keep in mind:**
|
|
26
|
+
**One thing to keep in mind:** some screens still show sample data. During this phase, treat anything odd there as a question for me rather than a bug.
|
|
27
27
|
|
|
28
|
-
[1] Shall I start Step 1 (the solo
|
|
28
|
+
[1] Shall I start Step 1 (the solo check and gap list) now?
|
|
29
29
|
Possible answers:
|
|
30
30
|
- Yes, start now
|
|
31
31
|
- Wait until I'm ready to do Step 2 as well
|
|
32
32
|
|
|
33
33
|
[2] For Step 2, how would you like to work?
|
|
34
34
|
Possible answers:
|
|
35
|
-
- I
|
|
36
|
-
- I'll put screenshots in the
|
|
35
|
+
- I use the planner while you take notes
|
|
36
|
+
- I'll put screenshots in the shared folder
|
|
37
37
|
- A mix of both
|
|
@@ -22,34 +22,34 @@ function outline(blocks = []) {
|
|
|
22
22
|
test("a question message keeps its intro paragraphs and nested lists apart from the question cards", () => {
|
|
23
23
|
const prompt = parseNumberedQuestionPrompt(message);
|
|
24
24
|
assert.deepEqual(prompt.questions.map((question) => [question.label, question.choices.length]), [
|
|
25
|
-
["Shall I start Step 1 (the solo
|
|
25
|
+
["Shall I start Step 1 (the solo check and gap list) now?", 2],
|
|
26
26
|
["For Step 2, how would you like to work?", 3]
|
|
27
27
|
]);
|
|
28
28
|
assert.equal(prompt.outro, "");
|
|
29
29
|
|
|
30
30
|
assert.deepEqual(outline(parseLongTextReviewBlocks(prompt.intro)), [
|
|
31
|
-
"paragraph: Here's how I'd run a final
|
|
32
|
-
"paragraph: **Step 1: I
|
|
31
|
+
"paragraph: Here's how I'd run a final check of the ",
|
|
32
|
+
"paragraph: **Step 1: I check on my own (no need for",
|
|
33
33
|
{
|
|
34
34
|
ul: [
|
|
35
|
-
"**
|
|
35
|
+
"**Try every screen.** I open every scree",
|
|
36
36
|
{
|
|
37
|
-
"**Gather the \"
|
|
38
|
-
{ ul: ["**I can
|
|
37
|
+
"**Gather the \"open question\" lists.** Ev": [
|
|
38
|
+
{ ul: ["**I can check it now:** for example, ren", "**Needs you:** real accounts, printing, ", "**Can't be checked at all.**"] }
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
|
-
"**Output:** one gap list, saved to
|
|
41
|
+
"**Output:** one gap list, saved to the s"
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"paragraph: **Step 2: a session with you, for the \"n",
|
|
45
45
|
{
|
|
46
46
|
ul: [
|
|
47
|
-
"**You
|
|
48
|
-
"**I
|
|
49
|
-
"**Things outside the
|
|
47
|
+
"**You use the planner on your own comput",
|
|
48
|
+
"**I take notes as you go** and capture s",
|
|
49
|
+
"**Things outside the planner itself:** p",
|
|
50
50
|
{
|
|
51
51
|
"**What I need from you:**": [
|
|
52
|
-
{ ul: ["which features you know or suspect exist", "the test
|
|
52
|
+
{ ul: ["which features you know or suspect exist", "the test account, so I can see the real ", "your call on anything that sends real da"] }
|
|
53
53
|
]
|
|
54
54
|
}
|
|
55
55
|
]
|
|
@@ -58,14 +58,14 @@ test("a question message keeps its intro paragraphs and nested lists apart from
|
|
|
58
58
|
{
|
|
59
59
|
ul: [
|
|
60
60
|
{
|
|
61
|
-
"**For
|
|
62
|
-
{ ul: ["Newly
|
|
61
|
+
"**For the design notes:**": [
|
|
62
|
+
{ ul: ["Newly found screens go into the notes as", "Each one gets a short note, written when"] }
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"**For you:** a final coverage summary."
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
|
-
"paragraph: **One thing to keep in mind:**
|
|
68
|
+
"paragraph: **One thing to keep in mind:** some scre"
|
|
69
69
|
]);
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { registerHooks } from "node:module";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
// Every module this process loads from here on; assistant-core is imported after the hook.
|
|
6
|
+
const loadedUrls = [];
|
|
7
|
+
registerHooks({
|
|
8
|
+
load(url, context, nextLoad) {
|
|
9
|
+
loadedUrls.push(url);
|
|
10
|
+
return nextLoad(url, context);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const openAiSdkModules = () => loadedUrls.filter((url) => /\/node_modules\/openai\//u.test(url));
|
|
14
|
+
|
|
15
|
+
test("the OpenAI SDK loads with an OpenAI-compatible client's first completion, not with the server", async () => {
|
|
16
|
+
await import("../src/server/index.js");
|
|
17
|
+
const { createOpenAiCompatibleClient } = await import("../src/server/lib/providers/openAiCompatibleClient.js");
|
|
18
|
+
const client = createOpenAiCompatibleClient({ apiKey: "test-only-key", baseUrl: "http://127.0.0.1:9/v1", model: "gpt-test" });
|
|
19
|
+
assert.equal(client.enabled, true);
|
|
20
|
+
assert.deepEqual(openAiSdkModules(), []);
|
|
21
|
+
|
|
22
|
+
const aborted = AbortSignal.abort();
|
|
23
|
+
await assert.rejects(client.createChatCompletion({ messages: [{ role: "user", content: "Hello" }], signal: aborted }));
|
|
24
|
+
assert.ok(openAiSdkModules().length > 0);
|
|
25
|
+
});
|