@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createMemoryConversationStorage } from "../src/server/conversation/memoryStorage.js";
|
|
4
|
+
import { createConversationStorage } from "../src/server/conversation/storage.js";
|
|
4
5
|
import { createConversationTranscript } from "../src/server/conversation/transcript.js";
|
|
5
6
|
import { conversationTurnsFromMessages } from "../src/shared/conversation/turns.js";
|
|
6
7
|
import { verifyConversationStorageContract } from "../src/testing/conversationStorageContract.js";
|
|
7
8
|
|
|
9
|
+
test("new output identity persists on its original message while older records remain unchanged", async () => {
|
|
10
|
+
let record = { turns: new Map(), metadata: {} };
|
|
11
|
+
const options = { readRecord: () => record, writeRecord: (_scope, next) => { record = next; } };
|
|
12
|
+
const transcript = createConversationTranscript({ storage: createConversationStorage(options) });
|
|
13
|
+
await transcript.writeConversationUserMessage("chat", { messageId: "old-user", text: "Old question" });
|
|
14
|
+
await transcript.writeConversationAssistantMessage("chat", { messageId: "old-answer", text: "Old answer" });
|
|
15
|
+
const old = structuredClone(record.turns.get("000001"));
|
|
16
|
+
await transcript.writeConversationUserMessage("chat", { messageId: "new-user", text: "New question" });
|
|
17
|
+
await transcript.writeConversationCommentaryMessage("chat", { messageId: "saved-progress", outputId: "native-progress", text: "Checking." });
|
|
18
|
+
const saved = await transcript.writeConversationAssistantMessage("chat", { messageId: "saved-answer", outputId: "native-answer", text: "New answer" });
|
|
19
|
+
await transcript.upsertConversationAssistantMessage("chat", { turnId: saved.turnId, text: "Corrected new answer" });
|
|
20
|
+
const reopened = createConversationTranscript({ storage: createConversationStorage(options) });
|
|
21
|
+
const rows = await reopened.readConversationLog("chat");
|
|
22
|
+
assert.deepEqual(record.turns.get("000001"), old);
|
|
23
|
+
assert.equal(Object.hasOwn(rows[0].assistant, "outputId"), false);
|
|
24
|
+
assert.equal(rows[1].commentary[0].messageId, "saved-progress");
|
|
25
|
+
assert.equal(rows[1].commentary[0].outputId, "native-progress");
|
|
26
|
+
assert.equal(rows[1].assistant.messageId, "saved-answer");
|
|
27
|
+
assert.equal(rows[1].assistant.outputId, "native-answer");
|
|
28
|
+
assert.equal(rows[1].assistant.text, "Corrected new answer");
|
|
29
|
+
});
|
|
30
|
+
|
|
8
31
|
test("memory conversation storage satisfies the replaceable storage contract", async () => {
|
|
9
32
|
await verifyConversationStorageContract(createMemoryConversationStorage());
|
|
10
33
|
});
|
|
@@ -55,3 +78,90 @@ test("message adaptation retains progress, attachments, system actions and SQL m
|
|
|
55
78
|
assert.equal(turns[1].system.repair, true);
|
|
56
79
|
assert.equal(turns[2].assistant.sql, "select 1");
|
|
57
80
|
});
|
|
81
|
+
|
|
82
|
+
test("record storage preserves original role, timestamp and message-id overwrite identity", async () => {
|
|
83
|
+
let record = { turns: new Map(), metadata: {} };
|
|
84
|
+
const storage = createConversationStorage({ readRecord: () => record, writeRecord: (_scope, next) => { record = next; } });
|
|
85
|
+
const at = "2026-10-01T01:00:00.123Z";
|
|
86
|
+
const write = message => storage.write("conversation", transaction => transaction.appendMessage("000001", message));
|
|
87
|
+
await write({ role: "thinking", at, text: "Start", messageId: "part:1" });
|
|
88
|
+
await write({ role: "thinking", at: "2026-10-01T09:00:00.123+08:00", text: "Start completed", messageId: " part:1 " });
|
|
89
|
+
assert.equal(record.turns.get("000001").messages.length, 1);
|
|
90
|
+
assert.equal(record.turns.get("000001").messages[0].text, "Start completed");
|
|
91
|
+
await write({ role: "thinking", at, text: "Independent item", messageId: "part:2" });
|
|
92
|
+
await write({ role: "commentary", at, text: "Different role", messageId: "part:1" });
|
|
93
|
+
await write({ role: "thinking", at: "2026-10-01T01:00:00.124Z", text: "Different timestamp", messageId: "part:1" });
|
|
94
|
+
assert.deepEqual(record.turns.get("000001").messages.map(({ text }) => text),
|
|
95
|
+
["Start completed", "Independent item", "Different role", "Different timestamp"]);
|
|
96
|
+
await storage.write("conversation", transaction => transaction.appendMessage("000002",
|
|
97
|
+
{ role: "thinking", at, text: "Another turn", messageId: "part:1" }));
|
|
98
|
+
assert.equal(record.turns.get("000001").messages.length, 4);
|
|
99
|
+
assert.equal(record.turns.get("000002").messages[0].text, "Another turn");
|
|
100
|
+
for (const role of ["assistant", "system", "user"]) {
|
|
101
|
+
await write({ role, at, text: "First", messageId: "same" });
|
|
102
|
+
await write({ role, at, text: "Updated", messageId: "same" });
|
|
103
|
+
assert.deepEqual(record.turns.get("000001").messages.filter(message => message.role === role).map(({ text }) => text), ["Updated"]);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("original thinking fragments update their saved row and preserve commentary boundaries", async () => {
|
|
108
|
+
const storage = createMemoryConversationStorage();
|
|
109
|
+
const transcript = createConversationTranscript({ storage });
|
|
110
|
+
await transcript.writeConversationUserMessage("one", { messageId: "request", text: "Work" });
|
|
111
|
+
const at = "2026-10-01T01:00:00.000Z";
|
|
112
|
+
await transcript.writeConversationThinkingMessage("one", { at, text: "Start" });
|
|
113
|
+
await transcript.writeConversationThinkingMessage("one", { at, text: `Start ${"word ".repeat(1_000)}`.trim() });
|
|
114
|
+
await transcript.writeConversationCommentaryMessage("one", { at: "2026-10-01T01:00:01.000Z", text: "Checking the files." });
|
|
115
|
+
await transcript.writeConversationThinkingMessage("one", { at: "2026-10-01T01:00:02.000Z", text: "next ".repeat(1_000).trim() });
|
|
116
|
+
const messages = (await transcript.readConversationLog("one")).flatMap(row => row.messages)
|
|
117
|
+
.filter(({ role }) => ["thinking", "commentary"].includes(role));
|
|
118
|
+
// Original public coalesced-reasoning assertion, with only store fixture adaptation.
|
|
119
|
+
assert.deepEqual(messages.map(({ text }) => text), [
|
|
120
|
+
`Start ${"word ".repeat(1_000)}`.trim(), "Checking the files.", "next ".repeat(1_000).trim()
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("message identity replacement does not rewrite tool receipts, unknown roles or failed transactions", async () => {
|
|
125
|
+
let record = { turns: new Map(), metadata: { retained: true } };
|
|
126
|
+
const storage = createConversationStorage({ readRecord: () => record, writeRecord: (_scope, next) => { record = next; } });
|
|
127
|
+
const message = { role: "thinking", at: "2026-10-01T01:00:00.000Z", text: "Saved" };
|
|
128
|
+
const tools = [{ id: "effect:1", status: "unknown", arguments: { retained: true } }];
|
|
129
|
+
await storage.write("one", async transaction => {
|
|
130
|
+
await transaction.appendMessage("000001", message);
|
|
131
|
+
await transaction.updateTurnMetadata("000001", { applicationTools: tools });
|
|
132
|
+
await transaction.appendMessage("000001", { role: "tool", at: message.at, messageId: "call:1", text: "First tool data" });
|
|
133
|
+
await transaction.appendMessage("000001", { role: "tool", at: message.at, messageId: "call:1", text: "Second tool data" });
|
|
134
|
+
});
|
|
135
|
+
const before = structuredClone(record);
|
|
136
|
+
await assert.rejects(storage.write("one", async transaction => {
|
|
137
|
+
await transaction.appendMessage("000001", { ...message, text: "Uncommitted replacement" });
|
|
138
|
+
throw new Error("Replacement failed");
|
|
139
|
+
}), /Replacement failed/);
|
|
140
|
+
assert.deepEqual(record, before);
|
|
141
|
+
await storage.write("one", transaction => transaction.appendMessage("000001", { ...message, text: "Saved replacement" }));
|
|
142
|
+
assert.deepEqual(record.turns.get("000001").metadata.applicationTools, tools);
|
|
143
|
+
assert.deepEqual(record.turns.get("000001").messages.filter(({ role }) => role === "tool").map(({ text }) => text),
|
|
144
|
+
["First tool data", "Second tool data"]);
|
|
145
|
+
assert.equal(record.turns.get("000001").messages.filter(({ role }) => role === "thinking").length, 1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
test("application transcript opt-in opens only authored application system turns", async () => {
|
|
150
|
+
for (const applicationTurns of [false, true]) {
|
|
151
|
+
for (const origin of [undefined, "system", "application"]) {
|
|
152
|
+
const storage = createMemoryConversationStorage();
|
|
153
|
+
const transcript = createConversationTranscript({ storage, applicationTurns });
|
|
154
|
+
await storage.write("one", transaction => transaction.appendMessage("000001", {
|
|
155
|
+
role: "system", text: "Saved instruction", at: "2026-10-01", ...(origin ? { origin } : {})
|
|
156
|
+
}));
|
|
157
|
+
const written = await transcript.writeConversationAssistantMessage("one", { text: "Native answer" });
|
|
158
|
+
assert.equal(written.turnId, applicationTurns && origin === "application" ? "000001" : "000002");
|
|
159
|
+
assert.equal((await transcript.readConversationLog("one")).length,
|
|
160
|
+
applicationTurns && origin === "application" ? 1 : 2);
|
|
161
|
+
}
|
|
162
|
+
const storage = createMemoryConversationStorage();
|
|
163
|
+
const transcript = createConversationTranscript({ storage, applicationTurns });
|
|
164
|
+
const authored = await transcript.writeConversationUserMessage("one", { text: "User request" });
|
|
165
|
+
assert.equal((await transcript.writeConversationAssistantMessage("one", { text: "Answer" })).turnId, authored.turnId);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { conversationAgentRunEvent, conversationAgentRunRecord, createCodexConversationRunStore, createCodexConversationStore } from "../src/server/conversation/agentRun.js";
|
|
4
|
+
import { createMemoryConversationStorage } from "../src/server/conversation/memoryStorage.js";
|
|
5
|
+
import { createReentrantConversationStorage } from "../src/server/conversation/storage.js";
|
|
6
|
+
import { createConversationTranscript } from "../src/server/conversation/transcript.js";
|
|
7
|
+
import { createConversationStreams } from "../src/server/conversation/streams.js";
|
|
8
|
+
|
|
9
|
+
const scope = "actor:workspace:conversation";
|
|
10
|
+
const runId = "codex_app_server";
|
|
11
|
+
const start = "2026-10-01T01:00:00.000Z";
|
|
12
|
+
const finish = "2026-10-01T01:00:01.000Z";
|
|
13
|
+
|
|
14
|
+
async function fixture() {
|
|
15
|
+
const memory = createMemoryConversationStorage();
|
|
16
|
+
let writes = 0;
|
|
17
|
+
const storage = createReentrantConversationStorage({
|
|
18
|
+
...memory,
|
|
19
|
+
write(id, operation) { writes += 1; return memory.write(id, operation); }
|
|
20
|
+
});
|
|
21
|
+
await storage.write(scope, transaction => transaction.writeMetadata({
|
|
22
|
+
product: { retained: true },
|
|
23
|
+
runtime: { version: 3, engine: "codex", segmentId: "segment-one", binding: { threadId: "native-thread" },
|
|
24
|
+
request: { messageId: "authored-request", message: "Frozen native prompt", attempted: true } }
|
|
25
|
+
}));
|
|
26
|
+
let current = true;
|
|
27
|
+
const store = createCodexConversationRunStore({ storage, scope, segmentId: "segment-one",
|
|
28
|
+
isCurrent: () => current, clock: () => new Date(start) });
|
|
29
|
+
return { storage, store, writes: () => writes, retire: () => { current = false; } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("original run updater retains event, timestamp, state and unknown-field semantics", () => {
|
|
33
|
+
const initial = conversationAgentRunEvent({ id: runId, events: [], extension: { preserved: true } }, runId, {
|
|
34
|
+
event: { at: ` ${start} `, kind: " claim ", message: " event message ", extra: "event extension" },
|
|
35
|
+
patch: { state: " active ", message: "patch message", updatedAt: finish, startedAt: start,
|
|
36
|
+
providerThreadId: "native-thread", providerTurnId: "native-turn", outerTurnId: "outer-turn", error: "explicit" }
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(initial, {
|
|
39
|
+
id: runId, events: [{ at: start, kind: "claim", message: "event message", extra: "event extension", state: "active" }],
|
|
40
|
+
extension: { preserved: true }, state: "active", message: "patch message", updatedAt: start, startedAt: start,
|
|
41
|
+
providerThreadId: "native-thread", providerTurnId: "native-turn", outerTurnId: "outer-turn", error: "explicit",
|
|
42
|
+
active: true, finishedAt: ""
|
|
43
|
+
});
|
|
44
|
+
const completed = conversationAgentRunEvent(initial, runId, {
|
|
45
|
+
event: { state: "failed", kind: "", message: "" },
|
|
46
|
+
patch: { state: "completed", updatedAt: finish, startedAt: finish, message: " complete ", finishedAt: finish }
|
|
47
|
+
});
|
|
48
|
+
assert.equal(completed.active, false);
|
|
49
|
+
assert.equal(completed.startedAt, start);
|
|
50
|
+
assert.equal(completed.finishedAt, finish);
|
|
51
|
+
assert.equal(completed.error, "explicit");
|
|
52
|
+
assert.deepEqual(completed.events, [...initial.events,
|
|
53
|
+
{ state: "completed", kind: "completed", message: "complete", at: finish }]);
|
|
54
|
+
const next = conversationAgentRunEvent(completed, runId, { patch: { state: "starting" } }, { now: () => new Date(finish) });
|
|
55
|
+
assert.equal(next.startedAt, start);
|
|
56
|
+
assert.equal(next.finishedAt, "");
|
|
57
|
+
assert.equal(next.error, "");
|
|
58
|
+
assert.equal(next.active, true);
|
|
59
|
+
assert.deepEqual(next.extension, initial.extension);
|
|
60
|
+
for (const state of ["cancelled", "completed", "failed", "interrupted", "timed_out"]) {
|
|
61
|
+
const terminal = conversationAgentRunEvent(completed, runId, { patch: { state } }, { now: () => new Date(start) });
|
|
62
|
+
assert.equal(terminal.active, false);
|
|
63
|
+
assert.equal(terminal.finishedAt, finish);
|
|
64
|
+
}
|
|
65
|
+
assert.equal(conversationAgentRunEvent(initial, runId, { patch: { state: "active", error: null } }).error, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("original run reader preserves unknown fields and filters events without inventing a run", async () => {
|
|
69
|
+
const f = await fixture();
|
|
70
|
+
const before = f.writes();
|
|
71
|
+
assert.equal(await f.store.readAgentRun(scope, runId), null);
|
|
72
|
+
assert.deepEqual(await f.store.getSession(scope), { sessionId: scope, agentRuns: [] });
|
|
73
|
+
assert.equal(f.writes(), before);
|
|
74
|
+
for (const value of [undefined, null, [], "invalid"]) assert.equal(conversationAgentRunRecord(value, runId), null);
|
|
75
|
+
assert.deepEqual(conversationAgentRunRecord({ id: "ignored", active: false, events: [null, [], "invalid", { kind: "saved" }],
|
|
76
|
+
pendingUserMessageClientIds: ["request"], extension: { one: 1 } }, runId), {
|
|
77
|
+
id: runId, active: true, events: [{ kind: "saved" }], state: "starting",
|
|
78
|
+
pendingUserMessageClientIds: ["request"], extension: { one: 1 }
|
|
79
|
+
});
|
|
80
|
+
for (const state of ["unknown", false, 0]) {
|
|
81
|
+
assert.throws(() => conversationAgentRunRecord({ state }, runId), { code: "codex_app_server_invalid_run_state" });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("native run mutation joins transcript writes and reads in one existing transaction", { timeout: 3_000 }, async () => {
|
|
86
|
+
const f = await fixture();
|
|
87
|
+
const transcript = createConversationTranscript({ storage: f.storage });
|
|
88
|
+
const before = f.writes();
|
|
89
|
+
const result = await f.store.mutateSession(scope, async () => {
|
|
90
|
+
assert.deepEqual(await f.store.getSession(scope), { sessionId: scope, agentRuns: [] });
|
|
91
|
+
await transcript.writeConversationUserMessage(scope, { messageId: "authored-request", text: "Authored message" });
|
|
92
|
+
const run = await f.store.writeAgentRunEvent(scope, runId, {
|
|
93
|
+
event: { kind: "claimed", at: start },
|
|
94
|
+
patch: { state: "starting", outerTurnId: "outer-turn", pendingUserMessageClientIds: ["authored-request"], retained: [1, 2] }
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual((await f.store.getSession(scope)).agentRuns, [run]);
|
|
97
|
+
assert.equal((await transcript.readConversationLog(scope))[0].user.text, "Authored message");
|
|
98
|
+
return run;
|
|
99
|
+
});
|
|
100
|
+
assert.equal(f.writes(), before + 1, "Nested native and transcript operations use one host writer call.");
|
|
101
|
+
const saved = await f.storage.read(scope, transaction => transaction.readMetadata());
|
|
102
|
+
assert.deepEqual(saved.runtime.binding.codexAppServerRun, result);
|
|
103
|
+
assert.deepEqual(saved.runtime.request, { messageId: "authored-request", message: "Frozen native prompt", attempted: true });
|
|
104
|
+
assert.deepEqual(saved.product, { retained: true });
|
|
105
|
+
result.retained.push(3);
|
|
106
|
+
assert.deepEqual((await f.store.readAgentRun(scope, runId)).retained, [1, 2]);
|
|
107
|
+
await assert.rejects(f.store.mutateSession(scope, async () => {
|
|
108
|
+
await f.store.writeAgentRunEvent(scope, runId, { patch: { state: "active", providerTurnId: "uncommitted" } });
|
|
109
|
+
await transcript.writeConversationAssistantMessage(scope, { text: "Uncommitted answer" });
|
|
110
|
+
throw new Error("commit refused");
|
|
111
|
+
}), /commit refused/);
|
|
112
|
+
assert.equal((await f.store.readAgentRun(scope, runId)).providerTurnId, undefined);
|
|
113
|
+
assert.equal((await transcript.readConversationLog(scope))[0].assistant, null);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("original active-participant lease drains nested work and queues escaped callbacks", { timeout: 3_000 }, async () => {
|
|
117
|
+
const f = await fixture();
|
|
118
|
+
const nestedEntered = Promise.withResolvers();
|
|
119
|
+
const nestedRelease = Promise.withResolvers();
|
|
120
|
+
const escapedRelease = Promise.withResolvers();
|
|
121
|
+
let nested, escaped;
|
|
122
|
+
let outerFinished = false;
|
|
123
|
+
const before = f.writes();
|
|
124
|
+
const outer = f.storage.write(scope, async transaction => {
|
|
125
|
+
const metadata = await transaction.readMetadata();
|
|
126
|
+
metadata.outer = true;
|
|
127
|
+
await transaction.writeMetadata(metadata);
|
|
128
|
+
escaped = escapedRelease.promise.then(() => f.storage.write(scope, async current => {
|
|
129
|
+
const latest = await current.readMetadata();
|
|
130
|
+
latest.escaped = true;
|
|
131
|
+
await current.writeMetadata(latest);
|
|
132
|
+
}));
|
|
133
|
+
nested = f.storage.write(scope, async current => {
|
|
134
|
+
nestedEntered.resolve();
|
|
135
|
+
await nestedRelease.promise;
|
|
136
|
+
const latest = await current.readMetadata();
|
|
137
|
+
latest.nested = true;
|
|
138
|
+
await current.writeMetadata(latest);
|
|
139
|
+
});
|
|
140
|
+
}).then(() => { outerFinished = true; });
|
|
141
|
+
await nestedEntered.promise;
|
|
142
|
+
// This independent scope must not borrow another scope's active draft.
|
|
143
|
+
await f.storage.write("other", transaction => transaction.writeMetadata({ other: true }));
|
|
144
|
+
assert.equal(outerFinished, false);
|
|
145
|
+
nestedRelease.resolve();
|
|
146
|
+
await Promise.all([outer, nested]);
|
|
147
|
+
assert.equal(f.writes(), before + 2);
|
|
148
|
+
escapedRelease.resolve();
|
|
149
|
+
await escaped;
|
|
150
|
+
assert.equal(f.writes(), before + 3, "An inactive inherited participant must re-enter the host queue.");
|
|
151
|
+
const saved = await f.storage.read(scope, transaction => transaction.readMetadata());
|
|
152
|
+
assert.equal(saved.outer, true);
|
|
153
|
+
assert.equal(saved.nested, true);
|
|
154
|
+
assert.equal(saved.escaped, true);
|
|
155
|
+
assert.equal(saved.other, undefined);
|
|
156
|
+
assert.deepEqual(await f.storage.read("other", transaction => transaction.readMetadata()), { other: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("native run mutation refuses stale owners, changed segments and cross-conversation access", { timeout: 3_000 }, async () => {
|
|
160
|
+
const f = await fixture();
|
|
161
|
+
await f.store.writeAgentRunEvent(scope, runId, { patch: { state: "completed", providerTurnId: "old-turn" } });
|
|
162
|
+
await assert.rejects(f.store.readAgentRun("other", runId), /another conversation/);
|
|
163
|
+
await assert.rejects(f.store.writeAgentRunEvent(scope, "another-run", {}), /only its Codex/);
|
|
164
|
+
const blocked = Promise.withResolvers();
|
|
165
|
+
const release = Promise.withResolvers();
|
|
166
|
+
const changing = f.storage.write(scope, async transaction => {
|
|
167
|
+
blocked.resolve();
|
|
168
|
+
await release.promise;
|
|
169
|
+
const metadata = await transaction.readMetadata();
|
|
170
|
+
metadata.runtime.segmentId = "successor";
|
|
171
|
+
metadata.runtime.binding = { threadId: "successor-thread" };
|
|
172
|
+
await transaction.writeMetadata(metadata);
|
|
173
|
+
});
|
|
174
|
+
await blocked.promise;
|
|
175
|
+
const stale = f.store.writeAgentRunEvent(scope, runId, { patch: { state: "active" } });
|
|
176
|
+
release.resolve();
|
|
177
|
+
await changing;
|
|
178
|
+
await assert.rejects(stale, /retired native conversation/);
|
|
179
|
+
assert.deepEqual((await f.storage.read(scope, transaction => transaction.readMetadata())).runtime.binding,
|
|
180
|
+
{ threadId: "successor-thread" });
|
|
181
|
+
f.retire();
|
|
182
|
+
await assert.rejects(f.store.writeAgentRunEvent(scope, runId, {}), /retired provider/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
test("late native authored receipts retain their exact request after the durable request changes", async () => {
|
|
187
|
+
const f = await fixture();
|
|
188
|
+
const transcript = createConversationTranscript({ storage: f.storage });
|
|
189
|
+
const store = createCodexConversationStore({ storage: f.storage, scope, segmentId: "segment-one",
|
|
190
|
+
isCurrent: () => true, transcript, streams: createConversationStreams() });
|
|
191
|
+
const authoredRequest = { messageId: "late-authored", text: "Original authorized message", origin: "application", at: start,
|
|
192
|
+
data: { request: "original" }, attachments: [{ attachmentId: "receipt", fileName: "saved.png", size: 4 }] };
|
|
193
|
+
await f.storage.write(scope, async transaction => {
|
|
194
|
+
const metadata = await transaction.readMetadata();
|
|
195
|
+
metadata.runtime.request = { messageId: "new-request", text: "A different request", data: { request: "new" } };
|
|
196
|
+
await transaction.writeMetadata(metadata);
|
|
197
|
+
});
|
|
198
|
+
const written = await store.writeConversationUserMessage(scope, {
|
|
199
|
+
messageId: authoredRequest.messageId, text: "Rendered native text is not the authored message",
|
|
200
|
+
authoredRequest, nativeIdentity: { threadId: "native-thread", turnId: "accepted-turn" }
|
|
201
|
+
});
|
|
202
|
+
assert.equal(written.system.text, authoredRequest.text);
|
|
203
|
+
assert.equal(written.system.at, start);
|
|
204
|
+
assert.deepEqual(written.system.data, authoredRequest.data);
|
|
205
|
+
assert.deepEqual(written.system.attachments, authoredRequest.attachments);
|
|
206
|
+
assert.equal(written.metadata.runtime.segmentId, "segment-one");
|
|
207
|
+
assert.equal(written.metadata.runtime.origin, "application");
|
|
208
|
+
assert.equal(written.metadata.runtime.nativeTurnId, "accepted-turn");
|
|
209
|
+
assert.equal(JSON.stringify(written).includes("nativeIdentity"), false);
|
|
210
|
+
assert.equal(JSON.stringify(written).includes("Rendered native"), false);
|
|
211
|
+
assert.equal(await store.writeConversationUserMessage(scope, { messageId: authoredRequest.messageId, authoredRequest }), null);
|
|
212
|
+
assert.equal((await transcript.readConversationLog(scope)).length, 1);
|
|
213
|
+
assert.equal((await f.storage.read(scope, transaction => transaction.readMetadata())).runtime.request.messageId, "new-request");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("native output uses its exact notification identity after the stored run advances", async () => {
|
|
217
|
+
const f = await fixture();
|
|
218
|
+
const transcript = createConversationTranscript({ storage: f.storage });
|
|
219
|
+
const store = createCodexConversationStore({ storage: f.storage, scope, segmentId: "segment-one",
|
|
220
|
+
isCurrent: () => true, transcript, streams: createConversationStreams() });
|
|
221
|
+
await store.writeAgentRunEvent(scope, runId, { patch: {
|
|
222
|
+
state: "active", providerThreadId: "native-thread", providerTurnId: "successor-turn"
|
|
223
|
+
} });
|
|
224
|
+
const written = await store.writeConversationAssistantMessage(scope, {
|
|
225
|
+
messageId: "native-response", text: "Exact earlier response",
|
|
226
|
+
nativeIdentity: { threadId: "native-thread", turnId: "earlier-turn" }
|
|
227
|
+
});
|
|
228
|
+
assert.equal(written.metadata.runtime.nativeTurnId, "earlier-turn");
|
|
229
|
+
assert.equal(written.metadata.runtime.origin, undefined);
|
|
230
|
+
assert.equal(JSON.stringify(written).includes("nativeIdentity"), false);
|
|
231
|
+
assert.equal((await store.readAgentRun(scope, runId)).providerTurnId, "successor-turn");
|
|
232
|
+
const corrected = await store.upsertConversationAssistantMessage(scope, {
|
|
233
|
+
turnId: written.turnId, text: "Exact corrected response",
|
|
234
|
+
nativeIdentity: { threadId: "native-thread", turnId: "earlier-turn" }
|
|
235
|
+
});
|
|
236
|
+
assert.equal(corrected.metadata.runtime.nativeTurnId, "earlier-turn");
|
|
237
|
+
assert.equal(corrected.assistant.text, "Exact corrected response");
|
|
238
|
+
assert.equal((await transcript.readConversationLog(scope)).length, 1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("native stream origin uses only its exact authorized pending request before the authored row exists", async () => {
|
|
242
|
+
for (const origin of ["user", "application"]) {
|
|
243
|
+
const f = await fixture();
|
|
244
|
+
const transcript = createConversationTranscript({ storage: f.storage, applicationTurns: true });
|
|
245
|
+
const store = createCodexConversationStore({ storage: f.storage, scope, segmentId: "segment-one",
|
|
246
|
+
isCurrent: () => true, transcript, streams: createConversationStreams() });
|
|
247
|
+
const nativeIdentity = { threadId: "native-thread", turnId: "native-turn" };
|
|
248
|
+
await f.storage.write(scope, async transaction => {
|
|
249
|
+
const metadata = await transaction.readMetadata();
|
|
250
|
+
metadata.runtime.request.origin = origin;
|
|
251
|
+
await transaction.writeMetadata(metadata);
|
|
252
|
+
});
|
|
253
|
+
await store.writeAgentRunEvent(scope, runId, { patch: { state: "active",
|
|
254
|
+
providerThreadId: nativeIdentity.threadId, providerTurnId: nativeIdentity.turnId, outerTurnId: "authored-request" } });
|
|
255
|
+
const stream = await store.updateConversationStream(scope, { nativeIdentity,
|
|
256
|
+
turnId: "native-thread:native-turn", messageId: "first-live-reply", text: "Reply before receipt" });
|
|
257
|
+
assert.equal(stream.messages[0].origin, origin);
|
|
258
|
+
assert.equal(Object.hasOwn(stream.messages[0], "turnId"), false);
|
|
259
|
+
assert.equal(JSON.stringify(stream).includes("nativeIdentity"), false);
|
|
260
|
+
assert.deepEqual(await transcript.readConversationLog(scope), []);
|
|
261
|
+
for (const identity of [{ threadId: "another-thread", turnId: "native-turn" },
|
|
262
|
+
{ threadId: "native-thread", turnId: "earlier-turn" }]) {
|
|
263
|
+
const unproven = await store.updateConversationStream(scope, { nativeIdentity: identity,
|
|
264
|
+
turnId: `${identity.threadId}:${identity.turnId}`, messageId: "unproven", text: "Unproven reply" });
|
|
265
|
+
assert.equal(unproven.messages[0].origin, undefined);
|
|
266
|
+
assert.equal(Object.hasOwn(unproven.messages[0], "turnId"), false);
|
|
267
|
+
}
|
|
268
|
+
await f.storage.write(scope, async transaction => {
|
|
269
|
+
const metadata = await transaction.readMetadata();
|
|
270
|
+
metadata.runtime.request.messageId = "unrelated-pending-request";
|
|
271
|
+
await transaction.writeMetadata(metadata);
|
|
272
|
+
});
|
|
273
|
+
const unrelated = await store.updateConversationStream(scope, { nativeIdentity,
|
|
274
|
+
turnId: "native-thread:native-turn", messageId: "unrelated", text: "No matching request" });
|
|
275
|
+
assert.equal(unrelated.messages[0].origin, undefined);
|
|
276
|
+
assert.equal(Object.hasOwn(unrelated.messages[0], "turnId"), false);
|
|
277
|
+
assert.deepEqual(await transcript.readConversationLog(scope), []);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("native output origin joins only its exact outer authored request and preserves saved grouping", async () => {
|
|
282
|
+
for (const origin of ["user", "application"]) {
|
|
283
|
+
const f = await fixture();
|
|
284
|
+
const transcript = createConversationTranscript({ storage: f.storage, applicationTurns: true });
|
|
285
|
+
const streams = createConversationStreams();
|
|
286
|
+
const store = createCodexConversationStore({ storage: f.storage, scope, segmentId: "segment-one",
|
|
287
|
+
isCurrent: () => true, transcript, streams });
|
|
288
|
+
const nativeIdentity = { threadId: "native-thread", turnId: "native-turn" };
|
|
289
|
+
const authoredRequest = { messageId: "outer-request", text: "Authorized request", origin };
|
|
290
|
+
const authored = await store.writeConversationUserMessage(scope, {
|
|
291
|
+
messageId: authoredRequest.messageId, authoredRequest, nativeIdentity
|
|
292
|
+
});
|
|
293
|
+
await store.writeAgentRunEvent(scope, runId, { patch: { state: "active",
|
|
294
|
+
providerThreadId: nativeIdentity.threadId, providerTurnId: nativeIdentity.turnId, outerTurnId: authoredRequest.messageId } });
|
|
295
|
+
const streaming = await store.updateConversationStream(scope, { nativeIdentity,
|
|
296
|
+
turnId: "native-thread:native-turn", messageId: "live-reply", text: "Live reply" });
|
|
297
|
+
assert.equal(streaming.messages[0].origin, origin);
|
|
298
|
+
assert.equal(Object.hasOwn(streaming.messages[0], "turnId"), false);
|
|
299
|
+
assert.equal(JSON.stringify(streaming).includes("nativeIdentity"), false);
|
|
300
|
+
const first = await store.writeConversationAssistantMessage(scope, {
|
|
301
|
+
nativeIdentity, messageId: "first-reply", text: "First reply"
|
|
302
|
+
});
|
|
303
|
+
const second = await store.writeConversationAssistantMessage(scope, {
|
|
304
|
+
nativeIdentity, messageId: "second-reply", text: "Second reply"
|
|
305
|
+
});
|
|
306
|
+
assert.equal(first.turnId, authored.turnId);
|
|
307
|
+
assert.notEqual(second.turnId, authored.turnId);
|
|
308
|
+
assert.equal(second.user, null);
|
|
309
|
+
assert.equal(second.system, undefined);
|
|
310
|
+
assert.equal(second.metadata.runtime.origin, origin);
|
|
311
|
+
assert.equal((await transcript.readConversationLog(scope)).length, 2);
|
|
312
|
+
for (const identity of [{ threadId: "another-thread", turnId: "native-turn" },
|
|
313
|
+
{ threadId: "native-thread", turnId: "earlier-turn" }]) {
|
|
314
|
+
const snapshot = await store.updateConversationStream(scope, { nativeIdentity: identity,
|
|
315
|
+
turnId: `${identity.threadId}:${identity.turnId}`, messageId: "unproven", text: "Unproven reply" });
|
|
316
|
+
assert.equal(snapshot.messages[0].origin, undefined);
|
|
317
|
+
assert.equal(Object.hasOwn(snapshot.messages[0], "turnId"), false);
|
|
318
|
+
}
|
|
319
|
+
await f.storage.write(scope, transaction => transaction.updateTurnMetadata(authored.turnId,
|
|
320
|
+
{ runtime: { ...authored.metadata.runtime, segmentId: "another-segment" } }));
|
|
321
|
+
const unowned = await store.writeConversationAssistantMessage(scope, {
|
|
322
|
+
nativeIdentity, messageId: "unowned-reply", text: "Reply without a matching authored segment"
|
|
323
|
+
});
|
|
324
|
+
assert.equal(unowned.metadata.runtime.origin, undefined);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createConversationRuntime, createFileConversationStorage, createMemoryConversationStorage } from "../src/server/conversation/index.js";
|
|
7
|
+
|
|
8
|
+
test("API configuration rejects unsupported structured output before resolving an account or sending", async t => {
|
|
9
|
+
let resolved = 0;
|
|
10
|
+
const runtime = createConversationRuntime({ storage: createMemoryConversationStorage(), authorize: () => true,
|
|
11
|
+
apiClientFactory: { resolveClient() { resolved++; assert.fail("Unsupported structured output cannot start inference"); } } });
|
|
12
|
+
t.after(() => runtime.close());
|
|
13
|
+
const configuration = { systemPrompt: "Use the supplied instructions" };
|
|
14
|
+
await assert.rejects(runtime.open({ id: "unsupported", configuration: { ...configuration,
|
|
15
|
+
outputSchema: { type: "string", maxLength: 16 } } }), /API does not support structured output/);
|
|
16
|
+
assert.equal(resolved, 0);
|
|
17
|
+
const ordinary = await runtime.open({ id: "ordinary", configuration });
|
|
18
|
+
assert.equal(ordinary.capabilities.structuredOutput, false);
|
|
19
|
+
await assert.rejects(ordinary.configure({ outputSchema: { type: "string", maxLength: 16 } }), /API does not support structured output/);
|
|
20
|
+
assert.deepEqual((await ordinary.read()).configuration, configuration);
|
|
21
|
+
assert.equal(resolved, 0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("an existing API client keeps opaque receipts path-safe and selected history private without replacing canonical history", async t => {
|
|
25
|
+
const directory = await mkdtemp(join(tmpdir(), "conversation-api-client-"));
|
|
26
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
27
|
+
const requests = [], resolvedAttachments = [];
|
|
28
|
+
const context = { subjectId: "owner", history: [
|
|
29
|
+
{ role: "user", content: "Selected prior context", attachmentIds: ["historical"] },
|
|
30
|
+
{ role: "assistant", content: "Selected prior response" }
|
|
31
|
+
] };
|
|
32
|
+
const runtime = createConversationRuntime({
|
|
33
|
+
storage: createFileConversationStorage({ directory }),
|
|
34
|
+
authorize: ({ context }) => context.subjectId === "owner",
|
|
35
|
+
apiHistory: ({ context }) => context.history,
|
|
36
|
+
apiClientFactory: { resolveClient({ context: actor, configuration }) {
|
|
37
|
+
assert.equal(actor, context);
|
|
38
|
+
assert.equal(configuration.integrationId, undefined, "Existing environment clients need no fabricated integration ID");
|
|
39
|
+
return { enabled: true, defaultModel: "existing-model", async createChatCompletionStream(input) {
|
|
40
|
+
requests.push(structuredClone(input.messages));
|
|
41
|
+
return (async function* () {
|
|
42
|
+
yield { choices: [{ delta: { content: " A complete answer. " } }] };
|
|
43
|
+
yield { choices: [{ delta: {}, finish_reason: "stop" }] };
|
|
44
|
+
})();
|
|
45
|
+
} };
|
|
46
|
+
} },
|
|
47
|
+
attachments: { async resolve({ attachmentIds, context: actor, conversationId }) {
|
|
48
|
+
assert.equal(actor, context);
|
|
49
|
+
assert.equal(conversationId, "sql/conversation");
|
|
50
|
+
resolvedAttachments.push([...attachmentIds]);
|
|
51
|
+
return { attachments: attachmentIds.map(attachmentId => ({ attachmentId, fileName: `${attachmentId}.txt`, size: 12 })),
|
|
52
|
+
content: attachmentIds.map(attachmentId => ({ type: "text", text: `Authorized ${attachmentId} bytes` })) };
|
|
53
|
+
} }
|
|
54
|
+
});
|
|
55
|
+
t.after(() => runtime.close());
|
|
56
|
+
const conversation = await runtime.open({ id: "sql/conversation", context,
|
|
57
|
+
configuration: { systemPrompt: "Existing instructions", model: "existing-model" } });
|
|
58
|
+
const messageId = "../invoice/42:follow-up";
|
|
59
|
+
assert.equal((await conversation.send({ messageId, text: "Current question", attachmentIds: ["current"] })).messageId, messageId);
|
|
60
|
+
const first = await conversation.wait();
|
|
61
|
+
assert.equal(first.conversationLog[0].user.messageId, messageId);
|
|
62
|
+
assert.equal(first.conversationLog[0].assistant.text, " A complete answer. ", "The default API whitespace policy stays unchanged");
|
|
63
|
+
assert.deepEqual(resolvedAttachments, [["current"], ["historical"]]);
|
|
64
|
+
assert.deepEqual(requests[0][1], { role: "user", content: [
|
|
65
|
+
{ type: "text", text: "Selected prior context" }, { type: "text", text: "Authorized historical bytes" }
|
|
66
|
+
] });
|
|
67
|
+
assert.deepEqual(requests[0].at(-1), { role: "user", content: [
|
|
68
|
+
{ type: "text", text: "Current question" }, { type: "text", text: "Authorized current bytes" }
|
|
69
|
+
] });
|
|
70
|
+
const files = await readdir(directory);
|
|
71
|
+
assert.equal(files.length, 1);
|
|
72
|
+
assert.match(files[0], /^[a-f0-9]{64}\.json$/);
|
|
73
|
+
const persisted = await readFile(join(directory, files[0]), "utf8");
|
|
74
|
+
assert.match(persisted, /invoice\/42:follow-up/);
|
|
75
|
+
assert.doesNotMatch(persisted, /Selected prior|Authorized .* bytes/);
|
|
76
|
+
|
|
77
|
+
context.history = undefined;
|
|
78
|
+
await conversation.send({ messageId: "next", text: "Use canonical history" });
|
|
79
|
+
await conversation.wait();
|
|
80
|
+
assert.equal(requests[1].some(message => Array.isArray(message.content) && message.content[0]?.text === "Current question"), true);
|
|
81
|
+
assert.equal(requests[1].some(message => message.content === "Selected prior response"), false);
|
|
82
|
+
|
|
83
|
+
context.history = [];
|
|
84
|
+
await conversation.send({ messageId: "empty-context", text: "No prior model context" });
|
|
85
|
+
const last = await conversation.wait();
|
|
86
|
+
assert.deepEqual(requests[2].map(message => message.content), ["Existing instructions", "No prior model context"]);
|
|
87
|
+
assert.equal(last.conversationLog.length, 3, "Explicitly empty model context never removes saved conversation history");
|
|
88
|
+
await assert.rejects(conversation.send({ messageId: "x".repeat(129), text: "Must not dispatch" }), { code: "conversation_invalid_message" });
|
|
89
|
+
assert.equal(requests.length, 3);
|
|
90
|
+
});
|