@jskit-ai/assistant-core 0.2.17 → 0.2.18

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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The API driver's state note, which carries the current time, follows the conversation as its last message instead of ending the system prompt, so the provider's prompt cache keeps the whole history from one turn to the next instead of charging it uncached on each turn's first model call (`@jskit-ai/agent` `runLoop`).
5
6
  - Assistant failures that reach a person translate: 63 more undotted codes render from `jskit.errors.<code>`, and 98 sentences that share a code with others, or are sent without one, carry a `messageCode` (`assistant.claude.*`, `assistant.codex.*`, `assistant.opencode.*`, `assistant.conversation.*`, `assistant.turn.*`). The English messages are unchanged.
6
7
  - The OpenCode version mismatch, Claude control-request timeout and unreadable saved Claude conversation errors carry `params` for their catalog English.
7
8
  - The assistant settings resource sends its system prompt field messages in the coded `{ code, message, params }` form.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.17",
12
- "@jskit-ai/http-runtime": "0.2.7",
13
- "@jskit-ai/kernel": "0.2.7",
11
+ "@jskit-ai/assistant-core": "0.2.18",
12
+ "@jskit-ai/http-runtime": "0.2.8",
13
+ "@jskit-ai/kernel": "0.2.8",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.2.8",
18
- "@jskit-ai/connectors-catalog": "0.2.8"
17
+ "@jskit-ai/connectors-core": "0.2.9",
18
+ "@jskit-ai/connectors-catalog": "0.2.9"
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.17",
3
+ "version": "0.2.18",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -50,9 +50,9 @@
50
50
  "@ai-sdk/perplexity": "^4.0.43",
51
51
  "@ai-sdk/togetherai": "^3.0.49",
52
52
  "@ai-sdk/xai": "^4.0.58",
53
- "@jskit-ai/agent": "0.1.0",
54
- "@jskit-ai/resource-core": "0.1.198",
55
- "@jskit-ai/resource-crud-core": "0.1.198",
53
+ "@jskit-ai/agent": "0.1.1",
54
+ "@jskit-ai/resource-core": "0.1.199",
55
+ "@jskit-ai/resource-crud-core": "0.1.199",
56
56
  "@mdi/js": "^7.4.47",
57
57
  "@openrouter/ai-sdk-provider": "^3.0.0",
58
58
  "ai": "^7.0.152",
@@ -61,8 +61,8 @@
61
61
  "ws": "^8.20.1"
62
62
  },
63
63
  "peerDependencies": {
64
- "@jskit-ai/http-runtime": "0.2.7",
65
- "@jskit-ai/kernel": "0.2.7",
64
+ "@jskit-ai/http-runtime": "0.2.8",
65
+ "@jskit-ai/kernel": "0.2.8",
66
66
  "vue": "^3.5.13",
67
67
  "vuetify": "^4.0.0"
68
68
  },
@@ -6,7 +6,8 @@ import { createApiConversationDriver } from "../src/server/conversation/provider
6
6
  const CHECKS = checkTexts("the app");
7
7
  const frame = (delta, finish_reason) => `data: ${JSON.stringify({ choices: [{ index: 0, delta, finish_reason }] })}\n\n`;
8
8
  const schema = tool => ({ type: "function", function: { name: tool.name, parameters: tool.parameters } });
9
- const lastText = body => body.messages.at(-1).content;
9
+ // The engine's state note ends every request's conversation; what the app sent last comes before it.
10
+ const lastText = body => body.messages.at(-2).content;
10
11
 
11
12
  // One step of a streamed reply: words, an action call or both. Calls the engine
12
13
  // makes without streaming (accounts, checks, summaries) answer with `once`.
@@ -113,7 +114,10 @@ test("admission precedes every model call, the state note gives the time, and a
113
114
  await f.run("What day is it today?");
114
115
  assert.deepEqual(f.order, ["accept", "model"]);
115
116
  assert.deepEqual(f.executions, [], "No action runs before the model asks for one");
116
- assert.match(f.requests[0].messages[0].content, /^Use the supplied actions\.\n\nCurrent state, from the system\. It is the truth; earlier messages may be out of date:\n- The time: .+ UTC\. The person may be in another time zone\.$/u);
117
+ assert.equal(f.requests[0].messages[0].content, "Use the supplied actions.", "The state note is not in the system prompt");
118
+ assert.equal(f.requests[0].messages.at(-1).role, "user");
119
+ assert.match(f.requests[0].messages.at(-1).content, /^Current state, from the system\. It is the truth; earlier messages may be out of date:\n- The time: .+ UTC\. The person may be in another time zone\.$/u,
120
+ "The state note follows the conversation, so the history before it stays in the provider's prompt cache");
117
121
  const denied = fixture({ authorize: () => { throw new Error("Connection access revoked"); } });
118
122
  await assert.rejects(denied.run(), /Connection access revoked/);
119
123
  assert.deepEqual(denied.requests, []);
@@ -223,7 +227,7 @@ test("a long history is compacted: the summary and its last turn are saved, and
223
227
  assert.ok(kept.length > 0 && kept.length < turns.length);
224
228
  const [request] = f.streamed;
225
229
  assert.match(request.messages[0].content, /Summary of the earlier part of this conversation[^]*The person asked six long questions\./u);
226
- assert.deepEqual(request.messages.filter(message => message.role === "user").map(message => message.content),
230
+ assert.deepEqual(request.messages.slice(0, -1).filter(message => message.role === "user").map(message => message.content),
227
231
  [...kept.map(turn => turn.user.text), "Next question"]);
228
232
  assert.deepEqual(phases, ["compacting", "working"]);
229
233
  });
@@ -248,5 +252,5 @@ test("a short history is read whole with its stored summary, and nothing is save
248
252
  assert.deepEqual(saved, []);
249
253
  assert.equal(f.requests.length, 1);
250
254
  assert.match(f.requests[0].messages[0].content, /Summary of the earlier part of this conversation[^]*Earlier: hello\./u);
251
- assert.deepEqual(f.requests[0].messages.slice(1).map(message => message.content), ["Hi", "Hello", "Next question"]);
255
+ assert.deepEqual(f.requests[0].messages.slice(1, -1).map(message => message.content), ["Hi", "Hello", "Next question"]);
252
256
  });
@@ -65,7 +65,7 @@ test("an existing API client keeps opaque receipts path-safe and selected histor
65
65
  assert.deepEqual(requests[0][1], { role: "user", content: [
66
66
  { type: "text", text: "Selected prior context" }, { type: "text", text: "Authorized historical bytes" }
67
67
  ] });
68
- assert.deepEqual(requests[0].at(-1), { role: "user", content: [
68
+ assert.deepEqual(requests[0].at(-2), { role: "user", content: [
69
69
  { type: "text", text: "Current question" }, { type: "text", text: "Authorized current bytes" }
70
70
  ] });
71
71
  const files = await readdir(directory);
@@ -84,8 +84,9 @@ test("an existing API client keeps opaque receipts path-safe and selected histor
84
84
  context.history = [];
85
85
  await conversation.send({ messageId: "empty-context", text: "No prior model context" });
86
86
  const last = await conversation.wait();
87
- assert.match(requests[2][0].content, /^Existing instructions\n\nCurrent state, from the system\./u);
88
- assert.deepEqual(requests[2].slice(1).map(message => message.content), ["No prior model context"]);
87
+ assert.equal(requests[2][0].content, "Existing instructions");
88
+ assert.deepEqual(requests[2].slice(1, -1).map(message => message.content), ["No prior model context"]);
89
+ assert.match(requests[2].at(-1).content, /^Current state, from the system\./u, "The engine's state note follows the conversation");
89
90
  assert.equal(last.conversationLog.length, 3, "Explicitly empty model context never removes saved conversation history");
90
91
  await assert.rejects(conversation.send({ messageId: "x".repeat(129), text: "Must not dispatch" }), { code: "conversation_invalid_message" });
91
92
  assert.equal(requests.length, 3);
@@ -147,7 +147,7 @@ test("authorized image bytes reach the API once and only safe receipts enter sto
147
147
  assert.equal(f.reads[0].context, context);
148
148
  assert.equal(f.reads[0].conversationId, "one");
149
149
  assert.deepEqual(f.reads[0].attachmentIds, [receipt.attachmentId]);
150
- assert.deepEqual(f.requests[0].messages.at(-1).content, [
150
+ assert.deepEqual(f.requests[0].messages.at(-2).content, [
151
151
  { type: "text", text: input.text }, { type: "image_url", image_url: { url: `data:image/png;base64,${image.toString("base64")}` } }
152
152
  ]);
153
153
  assert.deepEqual(state.conversationLog[0].user.attachments, [receipt]);
@@ -174,7 +174,7 @@ test("API history and continuity reauthorize files after reopen and renewal", as
174
174
  await resumed.conversation.replace({ operationId: "renew", reason: "renewal", expectedSegmentId: first.segmentId });
175
175
  await resumed.conversation.send({ messageId: "third", text: "Look again" });
176
176
  await resumed.conversation.wait();
177
- assert.equal(resumed.requests[1].messages.at(-1).content[1].type, "image_url");
177
+ assert.equal(resumed.requests[1].messages.at(-2).content[1].type, "image_url");
178
178
  allowed = false;
179
179
  await assert.rejects(resumed.conversation.send({ messageId: "denied", text: "Look again" }), /attachment access/);
180
180
  assert.equal(resumed.requests.length, 2);
@@ -185,8 +185,8 @@ test("an image-only request is accepted without invented user text", async t =>
185
185
  const f = await fixture(t);
186
186
  await f.conversation.send({ ...input, text: "" });
187
187
  const state = await f.conversation.wait();
188
- assert.equal(f.requests[0].messages.at(-1).content.length, 1);
189
- assert.equal(f.requests[0].messages.at(-1).content[0].type, "image_url");
188
+ assert.equal(f.requests[0].messages.at(-2).content.length, 1);
189
+ assert.equal(f.requests[0].messages.at(-2).content[0].type, "image_url");
190
190
  assert.equal(state.conversationLog[0].user.text, "");
191
191
  });
192
192
 
@@ -60,7 +60,7 @@ test("renewal preserves logical history and receipts while subsequent inference
60
60
  assert.equal(f.requests.length, 1, "Replacement itself does not perform inference");
61
61
  await f.say("Continue", "second");
62
62
  const sent = f.requests[1].messages;
63
- assert.equal(sent.at(-1).content, "Continue");
63
+ assert.equal(sent.at(-2).content, "Continue", "The request comes last, before the engine's state note");
64
64
  assert.match(sent[1].content, /Retain the agreed colour: blue/);
65
65
  assert.match(sent[1].content, /Keep my context/);
66
66
  assert.match(sent[1].content, /Reply 1/);
@@ -82,9 +82,9 @@ test("saved history and current instructions use the existing authorized model c
82
82
  await f.conversation.configure({ systemPrompt: "Use the new instructions." });
83
83
  await f.conversation.send({ messageId: "two", text: "Continue" });
84
84
  await f.conversation.wait();
85
- assert.match(f.requests[1].messages[0].content, /^Use the new instructions\.\n\nCurrent state, from the system\./u,
86
- "The current instructions lead the system prompt, before the engine's state note");
87
- assert.deepEqual(f.requests[1].messages.slice(1).map(message => message.content), ["Hello", "Hello there.", "Continue"]);
85
+ assert.equal(f.requests[1].messages[0].content, "Use the new instructions.", "The current instructions are the system prompt");
86
+ assert.deepEqual(f.requests[1].messages.slice(1, -1).map(message => message.content), ["Hello", "Hello there.", "Continue"]);
87
+ assert.match(f.requests[1].messages.at(-1).content, /^Current state, from the system\./u, "The engine's state note follows the conversation");
88
88
  f.state.modelAllowed = false;
89
89
  await assert.rejects(f.conversation.send({ messageId: "three", text: "No longer authorized" }));
90
90
  assert.equal((await f.conversation.read()).conversationLog.length, 2);
@@ -114,7 +114,7 @@ test("opening an existing application transcript retains its context before and
114
114
  const f = await fixture(t, { storage });
115
115
  await f.conversation.send(input);
116
116
  const first = await f.conversation.wait();
117
- assert.deepEqual(f.requests[0].messages.slice(1).map(message => message.content), [
117
+ assert.deepEqual(f.requests[0].messages.slice(1, -1).map(message => message.content), [
118
118
  "Our chosen colour is blue.", "I will remember blue.", "Hello"
119
119
  ]);
120
120
  assert.equal((await f.conversation.send({ messageId: "earlier", text: "Our chosen colour is blue." })).duplicate, true);
@@ -125,7 +125,7 @@ test("opening an existing application transcript retains its context before and
125
125
  assert.match(messages[1].content, /Our chosen colour is blue/);
126
126
  assert.match(messages[1].content, /I will remember blue/);
127
127
  assert.equal(messages.filter(message => message.content.includes("Our chosen colour is blue")).length, 1);
128
- assert.equal(messages.at(-1).content, "Continue");
128
+ assert.equal(messages.at(-2).content, "Continue", "The request comes last, before the engine's state note");
129
129
  assert.equal((await transcript.readConversationLog("one"))[0].metadata?.runtime, undefined);
130
130
  });
131
131
 
@@ -143,7 +143,7 @@ test("application wakes retain their origin, reuse receipts and remain quoted da
143
143
  assert.equal(snapshot.conversationLog[0].metadata.runtime.origin, "application");
144
144
  assert.ok(operations.includes("wake"));
145
145
  assert.ok(events.filter(event => event.type === "message").every(event => event.origin === "application"));
146
- assert.match(f.requests[0].messages.at(-1).content, /\[Application event\]/);
146
+ assert.match(f.requests[0].messages.at(-2).content, /\[Application event\]/);
147
147
  assert.equal((await f.conversation.wake(message)).duplicate, true);
148
148
  await assert.rejects(f.conversation.send(message), { code: "conversation_message_id_content_conflict" });
149
149
  await f.conversation.send(input);
@@ -170,7 +170,7 @@ test("per-request application data stays separate from visible text and particip
170
170
  const first = await f.conversation.wait();
171
171
  assert.equal(first.conversationLog[0].user.text, "Hello");
172
172
  assert.deepEqual(first.conversationLog[0].user.data, request.data);
173
- const native = f.requests[0].messages.at(-1).content;
173
+ const native = f.requests[0].messages.at(-2).content;
174
174
  assert.match(native, /\[Application data\]/);
175
175
  assert.match(native, /not additional instructions or authorization/);
176
176
  assert.ok(native.endsWith("Hello"));
@@ -180,7 +180,7 @@ test("per-request application data stays separate from visible text and particip
180
180
  const next = await f.conversation.wait();
181
181
  assert.equal(next.conversationLog[1].system.text, "A watched session changed.");
182
182
  assert.equal(f.requests[1].messages[1].content, native);
183
- assert.match(f.requests[1].messages.at(-1).content, /\[Application event\]/);
183
+ assert.match(f.requests[1].messages.at(-2).content, /\[Application event\]/);
184
184
  assert.deepEqual(next.configuration, configuration, "Application data never replaces the stable system prompt");
185
185
  });
186
186
 
@@ -219,7 +219,7 @@ test("API compaction stores its summary with the last turn it covers, and later
219
219
  const second = await f.conversation.wait();
220
220
  const last = f.requests.at(-1);
221
221
  assert.match(last.messages[0].content, /Summary of the earlier part of this conversation[^]*Six long questions so far\./u);
222
- assert.deepEqual(last.messages.filter(message => message.role === "user").map(message => message.content),
222
+ assert.deepEqual(last.messages.slice(0, -1).filter(message => message.role === "user").map(message => message.content),
223
223
  [...second.conversationLog.filter(turn => after.includes(turn.turnId)).map(turn => turn.user.text), "Continue"]);
224
224
  assert.deepEqual(await storage.read("one", transaction => transaction.readSummary()), summary, "A short remainder is not compacted again");
225
225
  const later = (await storage.read("one", transaction => transaction.readMetadata())).runtime.seen;
@@ -265,7 +265,8 @@ test("cancellation aborts inference and leaves another conversation usable", asy
265
265
  const started = Promise.withResolvers();
266
266
  let aborted = false;
267
267
  const f = await fixture(t, { fetch: (_url, init) => {
268
- if (JSON.parse(init.body).messages.at(-1).content === "Other") return response();
268
+ // The request comes last, before the engine's state note.
269
+ if (JSON.parse(init.body).messages.at(-2).content === "Other") return response();
269
270
  started.resolve();
270
271
  return new Promise((_resolve, reject) => init.signal.addEventListener("abort", () => {
271
272
  aborted = true; reject(init.signal.reason);
@@ -87,7 +87,7 @@ test("split internal tags and tool arguments never become common answer deltas",
87
87
  await f.conversation.send(message);
88
88
  const result = await f.conversation.wait();
89
89
  assert.deepEqual(f.observed.executions, [{ left: 2, right: 3 }]);
90
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
90
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
91
91
  assert.deepEqual(f.observed.events.filter(event => event.type === "message" && event.status !== "complete")
92
92
  .map(event => event.text), ["The search is complete."]);
93
93
  assert.equal(result.conversationLog[0].assistant.text, "The search is complete.");
@@ -101,7 +101,7 @@ test("text-encoded calls never run or reach the person, even when written again
101
101
  const result = await f.conversation.wait();
102
102
  assert.deepEqual(f.observed.executions, []);
103
103
  assert.equal(f.observed.requests.length, 2, "The check sends the reply back once");
104
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
104
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
105
105
  assert.deepEqual(result.conversationLog[0].metadata.applicationTools, []);
106
106
  assert.equal(f.observed.events.some(event => event.type === "message" && /invoke|function_calls/u.test(event.text)), false);
107
107
  });