@jskit-ai/assistant-core 0.2.6 → 0.2.8
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/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +18 -12
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -71,7 +71,7 @@ test("renewal preserves logical history and receipts while subsequent inference
|
|
|
71
71
|
const retry = await f.conversation.replace(operation);
|
|
72
72
|
assert.equal(retry.duplicate, true);
|
|
73
73
|
assert.equal(retry.segmentId, result.segmentId);
|
|
74
|
-
await assert.rejects(f.conversation.replace({ ...operation, briefing: "Different" }), { code: "
|
|
74
|
+
await assert.rejects(f.conversation.replace({ ...operation, briefing: "Different" }), { code: "conversation_replacement_operation_conflict" });
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test("completed historical branch markers retain history filtering and admission receipts", async t => {
|
|
@@ -105,8 +105,8 @@ test("unfinished historical Undo operations require offline inspection without c
|
|
|
105
105
|
saved.runtime.replacement = { request: { ...operation, operation: "replace" }, reason: "renewal", engine: "api",
|
|
106
106
|
configuration, segmentId: "historical-successor", seen: {}, ...patch };
|
|
107
107
|
await f.storage.write("one", transaction => transaction.writeMetadata(saved));
|
|
108
|
-
await assert.rejects(f.conversation.replace(operation), { code: "
|
|
109
|
-
await assert.rejects(f.runtime().open({ id: "one" }), { code: "
|
|
108
|
+
await assert.rejects(f.conversation.replace(operation), { code: "conversation_rewind_inspection_before_replace" });
|
|
109
|
+
await assert.rejects(f.runtime().open({ id: "one" }), { code: "conversation_rewind_inspection_before_open" });
|
|
110
110
|
assert.deepEqual(await f.storage.read("one", transaction => transaction.readMetadata()), saved);
|
|
111
111
|
assert.equal((await f.conversation.read()).conversationLog.length, 1);
|
|
112
112
|
assert.equal(f.requests.length, 1);
|
|
@@ -120,8 +120,8 @@ test("an interrupted replacement blocks sending and resumes exactly once after a
|
|
|
120
120
|
f.state.failCommit = true;
|
|
121
121
|
await assert.rejects(f.conversation.replace(operation), /Storage offline/);
|
|
122
122
|
assert.equal((await f.conversation.read()).status, "replacement-pending");
|
|
123
|
-
await assert.rejects(f.conversation.send({ text: "Too early", messageId: "early" }), { code: "
|
|
124
|
-
await assert.rejects(f.conversation.configure({ systemPrompt: "Too early" }), { code: "
|
|
123
|
+
await assert.rejects(f.conversation.send({ text: "Too early", messageId: "early" }), { code: "conversation_replacement_pending_before_send" });
|
|
124
|
+
await assert.rejects(f.conversation.configure({ systemPrompt: "Too early" }), { code: "conversation_replacement_pending_before_settings" });
|
|
125
125
|
await f.first.close();
|
|
126
126
|
f.state.failCommit = false;
|
|
127
127
|
const resumed = await f.runtime().open({ id: "one" });
|
|
@@ -137,11 +137,11 @@ test("an interrupted replacement blocks sending and resumes exactly once after a
|
|
|
137
137
|
test("invalid, stale and forbidden replacement requests do not alter the conversation", async t => {
|
|
138
138
|
const f = await fixture(t);
|
|
139
139
|
const operation = await f.replacement();
|
|
140
|
-
await assert.rejects(f.conversation.replace({ ...operation, expectedSegmentId: "stale" }), { code: "
|
|
141
|
-
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind" }), { code: "
|
|
142
|
-
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind", throughTurnId: "missing" }), { code: "
|
|
143
|
-
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind", throughTurnId: null }), { code: "
|
|
144
|
-
await assert.rejects(f.conversation.replace({ ...operation, throughTurnId: null }), { code: "
|
|
140
|
+
await assert.rejects(f.conversation.replace({ ...operation, expectedSegmentId: "stale" }), { code: "conversation_predecessor_changed" });
|
|
141
|
+
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind" }), { code: "conversation_replacement_request_invalid" });
|
|
142
|
+
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind", throughTurnId: "missing" }), { code: "conversation_replacement_request_invalid" });
|
|
143
|
+
await assert.rejects(f.conversation.replace({ ...operation, reason: "rewind", throughTurnId: null }), { code: "conversation_replacement_request_invalid" });
|
|
144
|
+
await assert.rejects(f.conversation.replace({ ...operation, throughTurnId: null }), { code: "conversation_replacement_request_invalid" });
|
|
145
145
|
await assert.rejects(f.conversation.replace({ ...operation, engine: "missing" }), /does not yet support/);
|
|
146
146
|
f.state.allowed = false;
|
|
147
147
|
await assert.rejects(f.conversation.replace(operation), { code: "conversation_forbidden" });
|
|
@@ -167,7 +167,7 @@ test("host scope belongs to a conversation and cannot be changed through another
|
|
|
167
167
|
const host = { workdir: "/application/one", env: { HOME: "/accounts/one" } };
|
|
168
168
|
const one = await f.first.open({ id: "scoped-one", host, configuration });
|
|
169
169
|
const two = await f.first.open({ id: "scoped-two", host: { workdir: "/application/two" }, configuration });
|
|
170
|
-
await assert.rejects(f.first.open({ id: "scoped-one", host: { workdir: "/application/other" } }), { code: "
|
|
170
|
+
await assert.rejects(f.first.open({ id: "scoped-one", host: { workdir: "/application/other" } }), { code: "conversation_host_scope_mismatch" });
|
|
171
171
|
await one.send({ messageId: "one", text: "One" });
|
|
172
172
|
await two.send({ messageId: "two", text: "Two" });
|
|
173
173
|
assert.equal((await one.wait()).conversationLog.length, 1);
|
|
@@ -196,7 +196,7 @@ test("interrupted OpenCode integration selection retries the original fresh bind
|
|
|
196
196
|
assert.notEqual(pending.runtime.replacement.binding.directory, metadata.runtime.binding.directory);
|
|
197
197
|
assert.equal((await f.conversation.read()).status, "replacement-pending");
|
|
198
198
|
await assert.rejects(f.conversation.send({ messageId: "premature", text: "Too early" }),
|
|
199
|
-
{ code: "
|
|
199
|
+
{ code: "conversation_replacement_pending_before_send" });
|
|
200
200
|
await f.first.close();
|
|
201
201
|
f.state.failCommit = false;
|
|
202
202
|
const reopened = await f.runtime().open({ id: "one" });
|
|
@@ -230,7 +230,7 @@ test("interrupted fresh native selection retains its consumer policy and success
|
|
|
230
230
|
assert.notEqual(pending.runtime.replacement.binding.directory, original.runtime.binding.directory);
|
|
231
231
|
assert.equal((await f.conversation.read()).status, "replacement-pending");
|
|
232
232
|
await assert.rejects(f.conversation.send({ messageId: "too-early", text: "Wait for the change" }),
|
|
233
|
-
{ code: "
|
|
233
|
+
{ code: "conversation_replacement_pending_before_send" });
|
|
234
234
|
await f.first.close();
|
|
235
235
|
f.state.failCommit = false;
|
|
236
236
|
const reopened = await f.runtime().open({ id: "one" });
|
|
@@ -4,7 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createConversationHookBridge, createConversationRuntime } from "../src/server/conversation/index.js";
|
|
7
|
-
import { createClaudeConversationTurn } from "../src/server/conversation/
|
|
7
|
+
import { createClaudeConversationTurn } from "../src/server/conversation/claudeNativeTurn.js";
|
|
8
8
|
import { createCodexConversationAdapter } from "../src/server/conversation/providers/codex.js";
|
|
9
9
|
import { createOpenCodeConversationAdapter } from "../src/server/conversation/providers/opencode.js";
|
|
10
10
|
import { readPersistentConversation } from "../src/server/conversation/providers/scoped.js";
|
|
@@ -97,7 +97,7 @@ test("saved persistent history projects receipts and pages without native observ
|
|
|
97
97
|
assert.equal(result.page.conversationLog[0].user.receipt, false);
|
|
98
98
|
assert.deepEqual(calls.map(([kind]) => kind), ["log", "page"]);
|
|
99
99
|
await runtime.close();
|
|
100
|
-
await assert.rejects(runtime.projectStoredPersistentConversation(options), { code: "
|
|
100
|
+
await assert.rejects(runtime.projectStoredPersistentConversation(options), { code: "conversation_runtime_closed" });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test("a standalone Claude consumer reuses, updates and replaces processes without changing native history", async () => {
|
|
@@ -197,7 +197,11 @@ function codexHost({ readInstructions } = {}) {
|
|
|
197
197
|
verifyEnvironment: async () => {},
|
|
198
198
|
publishGoal: (threadId, goal) => requests.push({ method: "host.goal", input: { threadId, goal } }),
|
|
199
199
|
recordInterruption: (threadId, turnId) => requests.push({ method: "host.recordInterruption", input: { threadId, turnId } }),
|
|
200
|
-
prepareResume: async (id, params) => { requests.push({ method: "host.prepareResume", input: { id } }); return params; }
|
|
200
|
+
prepareResume: async (id, params) => { requests.push({ method: "host.prepareResume", input: { id } }); return params; },
|
|
201
|
+
prepareParameters: () => undefined,
|
|
202
|
+
beforeResume: async () => {},
|
|
203
|
+
onRecoveryFailure: () => {},
|
|
204
|
+
log: () => {}
|
|
201
205
|
});
|
|
202
206
|
return { runtime, state, requests, client };
|
|
203
207
|
}
|
|
@@ -725,8 +729,8 @@ test("a deleted logical conversation retires its open entry without opening anot
|
|
|
725
729
|
const handle = await runtime.open({ id: "session", context: { chat: "one" } });
|
|
726
730
|
await handle.subscribe(() => {});
|
|
727
731
|
assert.deepEqual(await runtime.retireOpenConversation({ namespace: "temporary-one", sessionId: "session" }), { retired: true });
|
|
728
|
-
await assert.rejects(handle.read(), { code: "
|
|
729
|
-
await assert.rejects(handle.subscribe(() => {}), { code: "
|
|
732
|
+
await assert.rejects(handle.read(), { code: "conversation_handle_closed" });
|
|
733
|
+
await assert.rejects(handle.subscribe(() => {}), { code: "conversation_handle_closed" });
|
|
730
734
|
assert.deepEqual(await runtime.retireOpenConversation({ namespace: "temporary-one", sessionId: "session" }), { retired: false },
|
|
731
735
|
"an absent entry is already retired; nothing is opened to retire it");
|
|
732
736
|
assert.deepEqual(await runtime.retireOpenConversation({ namespace: "temporary-two", sessionId: "session" }), { retired: false });
|
|
@@ -29,7 +29,7 @@ async function fixture(t, options = {}) {
|
|
|
29
29
|
authorize: ({ context: actor, conversationId, operation }) => state.allowed && actor === context && ["one", "two"].includes(conversationId) &&
|
|
30
30
|
(options.authorize?.({ operation, context: actor, conversationId }) ?? true),
|
|
31
31
|
fetch: (url, init) => { requests.push(JSON.parse(init.body)); return options.fetch?.(url, init) || response(); },
|
|
32
|
-
limits: options.limits
|
|
32
|
+
limits: options.limits, ...(options.attachments ? { attachments: options.attachments } : {})
|
|
33
33
|
});
|
|
34
34
|
t.after(() => runtime.close());
|
|
35
35
|
const conversation = await runtime.open({ id: "one", context, configuration: options.resume ? undefined : configuration });
|
|
@@ -71,7 +71,7 @@ test("API runtime persists one admission for concurrent identical sends and stre
|
|
|
71
71
|
assert.equal(events.find(event => event.type === "accepted").messageId, input.messageId);
|
|
72
72
|
assert.equal(events.at(-1).type, "settled");
|
|
73
73
|
unsubscribe();
|
|
74
|
-
await assert.rejects(f.conversation.send({ ...input, text: "Changed content" }), { code: "
|
|
74
|
+
await assert.rejects(f.conversation.send({ ...input, text: "Changed content" }), { code: "conversation_message_id_content_conflict" });
|
|
75
75
|
assert.equal(f.requests.length, 1);
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -82,7 +82,8 @@ 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.
|
|
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");
|
|
86
87
|
assert.deepEqual(f.requests[1].messages.slice(1).map(message => message.content), ["Hello", "Hello there.", "Continue"]);
|
|
87
88
|
f.state.modelAllowed = false;
|
|
88
89
|
await assert.rejects(f.conversation.send({ messageId: "three", text: "No longer authorized" }));
|
|
@@ -144,7 +145,7 @@ test("application wakes retain their origin, reuse receipts and remain quoted da
|
|
|
144
145
|
assert.ok(events.filter(event => event.type === "message").every(event => event.origin === "application"));
|
|
145
146
|
assert.match(f.requests[0].messages.at(-1).content, /\[Application event\]/);
|
|
146
147
|
assert.equal((await f.conversation.wake(message)).duplicate, true);
|
|
147
|
-
await assert.rejects(f.conversation.send(message), { code: "
|
|
148
|
+
await assert.rejects(f.conversation.send(message), { code: "conversation_message_id_content_conflict" });
|
|
148
149
|
await f.conversation.send(input);
|
|
149
150
|
await f.conversation.wait();
|
|
150
151
|
assert.equal(f.requests.length, 2);
|
|
@@ -174,7 +175,7 @@ test("per-request application data stays separate from visible text and particip
|
|
|
174
175
|
assert.match(native, /not additional instructions or authorization/);
|
|
175
176
|
assert.ok(native.endsWith("Hello"));
|
|
176
177
|
assert.equal((await f.conversation.send(request)).duplicate, true);
|
|
177
|
-
await assert.rejects(f.conversation.send({ ...request, data: { focus: { project: "two" } } }), { code: "
|
|
178
|
+
await assert.rejects(f.conversation.send({ ...request, data: { focus: { project: "two" } } }), { code: "conversation_message_id_content_conflict" });
|
|
178
179
|
await f.conversation.wake({ messageId: "watch-data", text: "A watched session changed.", data: { status: "complete" } });
|
|
179
180
|
const next = await f.conversation.wait();
|
|
180
181
|
assert.equal(next.conversationLog[1].system.text, "A watched session changed.");
|
|
@@ -193,8 +194,63 @@ test("application data must fit the message limit before reserving or dispatchin
|
|
|
193
194
|
assert.equal((await f.conversation.read()).pendingRequest, null);
|
|
194
195
|
});
|
|
195
196
|
|
|
196
|
-
test("
|
|
197
|
-
const
|
|
197
|
+
test("API compaction stores its summary with the last turn it covers, and later turns read only the turns after it", async t => {
|
|
198
|
+
const storage = createMemoryConversationStorage();
|
|
199
|
+
const transcript = createConversationTranscript({ storage });
|
|
200
|
+
for (let index = 0; index < 6; index++) {
|
|
201
|
+
await transcript.writeConversationUserMessage("one", { messageId: `earlier-${index}`, text: `Question ${index} ${"x".repeat(4000)}` });
|
|
202
|
+
await transcript.writeConversationAssistantMessage("one", { text: `Answer ${index}` });
|
|
203
|
+
}
|
|
204
|
+
const f = await fixture(t, { storage, limits: { contextTokens: 10_000 }, fetch: (_url, init) => JSON.parse(init.body).stream ? undefined
|
|
205
|
+
: Response.json({ choices: [{ index: 0, message: { role: "assistant", content: "Six long questions so far." }, finish_reason: "stop" }] }) });
|
|
206
|
+
const phases = [];
|
|
207
|
+
await f.conversation.subscribe(event => { if (event.type === "phase") phases.push(event.phase); });
|
|
208
|
+
await f.conversation.send(input);
|
|
209
|
+
const first = await f.conversation.wait();
|
|
210
|
+
assert.equal(first.conversationLog.at(-1).assistant.text, "Hello there.");
|
|
211
|
+
assert.ok(phases.includes("compacting"));
|
|
212
|
+
const summary = await storage.read("one", transaction => transaction.readSummary());
|
|
213
|
+
assert.equal(summary.text, "Six long questions so far.");
|
|
214
|
+
const turnIds = first.conversationLog.map(turn => turn.turnId);
|
|
215
|
+
const after = turnIds.slice(turnIds.indexOf(summary.throughTurnId) + 1);
|
|
216
|
+
assert.ok(after.length >= 2 && after.length < turnIds.length);
|
|
217
|
+
const seen = (await storage.read("one", transaction => transaction.readMetadata())).runtime.seen;
|
|
218
|
+
await f.conversation.send({ messageId: "two", text: "Continue" });
|
|
219
|
+
const second = await f.conversation.wait();
|
|
220
|
+
const last = f.requests.at(-1);
|
|
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),
|
|
223
|
+
[...second.conversationLog.filter(turn => after.includes(turn.turnId)).map(turn => turn.user.text), "Continue"]);
|
|
224
|
+
assert.deepEqual(await storage.read("one", transaction => transaction.readSummary()), summary, "A short remainder is not compacted again");
|
|
225
|
+
const later = (await storage.read("one", transaction => transaction.readMetadata())).runtime.seen;
|
|
226
|
+
assert.ok(Object.keys(seen).every(id => Object.hasOwn(later, id)), "Versions saved before the summary still stand");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("API history sends files again only within the newest 20 messages; older turns name theirs", async t => {
|
|
230
|
+
const resolved = [];
|
|
231
|
+
const attachments = { async resolve({ attachmentIds }) {
|
|
232
|
+
resolved.push([...attachmentIds]);
|
|
233
|
+
return { attachments: attachmentIds.map(attachmentId => ({ attachmentId, fileName: `${attachmentId}.txt`, size: 4 })),
|
|
234
|
+
content: attachmentIds.map(attachmentId => ({ type: "text", text: `Bytes of ${attachmentId}` })) };
|
|
235
|
+
} };
|
|
236
|
+
const f = await fixture(t, { attachments });
|
|
237
|
+
const send = async message => { await f.conversation.send(message); await f.conversation.wait(); };
|
|
238
|
+
await send({ messageId: "old", text: "Old file", attachmentIds: ["old"] });
|
|
239
|
+
for (let index = 0; index < 10; index++) await send({ messageId: `m${index}`, text: `Message ${index}` });
|
|
240
|
+
await send({ messageId: "recent", text: "Recent file", attachmentIds: ["recent"] });
|
|
241
|
+
resolved.length = 0;
|
|
242
|
+
await send({ messageId: "last", text: "Last" });
|
|
243
|
+
assert.deepEqual(resolved, [["recent"]], "Only the recent turn's file is read again");
|
|
244
|
+
const messages = f.requests.at(-1).messages;
|
|
245
|
+
assert.deepEqual(messages.find(message => Array.isArray(message.content) && message.content[0].text === "Old file").content,
|
|
246
|
+
[{ type: "text", text: "Old file" }, { type: "text", text: "(Files attached to this message, no longer shown: old.txt.)" }]);
|
|
247
|
+
assert.deepEqual(messages.find(message => Array.isArray(message.content) && message.content[0].text === "Recent file").content,
|
|
248
|
+
[{ type: "text", text: "Recent file" }, { type: "text", text: "Bytes of recent" }]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("provider errors before anything was done are status information after the engine's retries, never assistant replies", async t => {
|
|
252
|
+
const f = await fixture(t, { fetch: () => new Response('{"error":{"message":"Capacity unavailable"}}',
|
|
253
|
+
{ status: 429, headers: { "content-type": "application/json", "retry-after-ms": "0" } }) });
|
|
198
254
|
const receipt = await f.conversation.send(input);
|
|
199
255
|
assert.equal(receipt.status, "accepted");
|
|
200
256
|
const result = await f.conversation.wait();
|
|
@@ -202,7 +258,7 @@ test("provider errors are status information, never assistant replies or automat
|
|
|
202
258
|
assert.equal(result.conversationLog[0].metadata.runtime.status, "failed");
|
|
203
259
|
assert.match(result.error, /Capacity unavailable/);
|
|
204
260
|
assert.equal((await f.conversation.send(input)).duplicate, true);
|
|
205
|
-
assert.equal(f.requests.length,
|
|
261
|
+
assert.equal(f.requests.length, 7, "The engine tries a failed call again six times, then the turn fails");
|
|
206
262
|
});
|
|
207
263
|
|
|
208
264
|
test("cancellation aborts inference and leaves another conversation usable", async t => {
|
|
@@ -297,7 +353,7 @@ test("native creation authorizes its parent scope and uses only the configured o
|
|
|
297
353
|
assert.equal(acquisitions, 1);
|
|
298
354
|
|
|
299
355
|
await runtime.close();
|
|
300
|
-
await assert.rejects(runtime.createNativeConversation(request), { code: "
|
|
356
|
+
await assert.rejects(runtime.createNativeConversation(request), { code: "conversation_handle_closed" });
|
|
301
357
|
assert.equal(accesses.length, 2);
|
|
302
358
|
assert.equal(hostCalls, 1);
|
|
303
359
|
assert.equal(acquisitions, 1);
|
|
@@ -358,9 +414,9 @@ test("native detached execution separates admitted context from host-approved or
|
|
|
358
414
|
await entered.promise;
|
|
359
415
|
await runtime.close();
|
|
360
416
|
preparing.resolve();
|
|
361
|
-
await assert.rejects(pending, { code: "
|
|
417
|
+
await assert.rejects(pending, { code: "conversation_runtime_closed" });
|
|
362
418
|
assert.deepEqual(calls.slice(6), ["authorize", "host"]);
|
|
363
|
-
await assert.rejects(runtime.runNativeDetachedConversation(request), { code: "
|
|
419
|
+
await assert.rejects(runtime.runNativeDetachedConversation(request), { code: "conversation_handle_closed" });
|
|
364
420
|
assert.equal(calls.length, 8);
|
|
365
421
|
});
|
|
366
422
|
|
|
@@ -492,8 +548,8 @@ test("native readiness authorizes its configured owner and rejects closure durin
|
|
|
492
548
|
await preparation.entered.promise;
|
|
493
549
|
await runtime.close();
|
|
494
550
|
preparation.finished.resolve();
|
|
495
|
-
await assert.rejects(pending, { code: "
|
|
496
|
-
await assert.rejects(runtime.ensureNativeConversation(request), { code: "
|
|
551
|
+
await assert.rejects(pending, { code: "conversation_runtime_closed" });
|
|
552
|
+
await assert.rejects(runtime.ensureNativeConversation(request), { code: "conversation_handle_closed" });
|
|
497
553
|
assert.deepEqual([hostCalls, acquisitions, accounts, saves], [2, 1, 1, 1]);
|
|
498
554
|
assert.equal(owner.entries.size, 1, "Readiness did not create a retained runtime handle to dispose.");
|
|
499
555
|
});
|
|
@@ -651,7 +707,7 @@ test("native invalidation preserves synchronous fences and failures without open
|
|
|
651
707
|
});
|
|
652
708
|
|
|
653
709
|
test("renewal proof cleanup preserves OpenCode failure, consumption and retry through the configured owner", async t => {
|
|
654
|
-
const owner = createOpenCodeSharedRuntime();
|
|
710
|
+
const owner = createOpenCodeSharedRuntime({});
|
|
655
711
|
const calls = [];
|
|
656
712
|
const id = "renewal-session";
|
|
657
713
|
const requestContext = { sessionId: id };
|
|
@@ -910,9 +966,9 @@ test("renewal authorizes the existing session without opening a conversation or
|
|
|
910
966
|
assert.equal(await runtime[method](request), result);
|
|
911
967
|
assert.deepEqual(calls.slice(3), ["authorize", "host", "prepare", "acquire", "native"]);
|
|
912
968
|
closeDuringPreparation = true;
|
|
913
|
-
await assert.rejects(runtime[method](request), { code: "
|
|
969
|
+
await assert.rejects(runtime[method](request), { code: "conversation_runtime_closed" });
|
|
914
970
|
assert.deepEqual(calls.slice(8), ["authorize", "host"]);
|
|
915
|
-
await assert.rejects(runtime[method](request), { code: "
|
|
971
|
+
await assert.rejects(runtime[method](request), { code: "conversation_handle_closed" });
|
|
916
972
|
assert.deepEqual(calls.slice(8), ["authorize", "host"]);
|
|
917
973
|
});
|
|
918
974
|
});
|
|
@@ -1124,7 +1180,7 @@ test("disposed handles cannot alter the next owner of the same conversation", as
|
|
|
1124
1180
|
const next = await f.runtime.open({ id: "one", context });
|
|
1125
1181
|
for (const run of [() => f.conversation.read(), () => f.conversation.send(input), () => f.conversation.dispose(),
|
|
1126
1182
|
() => f.conversation.configure({ systemPrompt: "Stale" }), () => f.conversation.subscribe(() => {})]) {
|
|
1127
|
-
await assert.rejects(run(), { code: "
|
|
1183
|
+
await assert.rejects(run(), { code: "conversation_handle_closed" });
|
|
1128
1184
|
}
|
|
1129
1185
|
await next.send(input);
|
|
1130
1186
|
assert.equal((await next.wait()).conversationLog.length, 1);
|
|
@@ -1162,11 +1218,11 @@ test("a saved running turn is marked interrupted and its accepted message is not
|
|
|
1162
1218
|
|
|
1163
1219
|
test("limits and unsupported operations fail explicitly without silently changing the request", async t => {
|
|
1164
1220
|
const f = await fixture(t, { limits: { maxOutputCharacters: 3 } });
|
|
1165
|
-
await assert.rejects(f.conversation.send({ ...input, attachments: [{ id: "one" }] }), { code: "
|
|
1166
|
-
await assert.rejects(f.conversation.send({ ...input, steer: true }), { code: "
|
|
1221
|
+
await assert.rejects(f.conversation.send({ ...input, attachments: [{ id: "one" }] }), { code: "conversation_attachment_ids_only" });
|
|
1222
|
+
await assert.rejects(f.conversation.send({ ...input, steer: true }), { code: "conversation_steering_unsupported" });
|
|
1167
1223
|
assert.equal(await f.conversation.readGoal(), null);
|
|
1168
1224
|
await assert.rejects(f.conversation.updateGoal({ action: "set", messageId: "goal", objective: "Keep working",
|
|
1169
|
-
expectedSegmentId: (await f.conversation.read()).segmentId }), { code: "
|
|
1225
|
+
expectedSegmentId: (await f.conversation.read()).segmentId }), { code: "conversation_goals_unsupported" });
|
|
1170
1226
|
await f.conversation.send(input);
|
|
1171
1227
|
const result = await f.conversation.wait();
|
|
1172
1228
|
assert.equal(result.conversationLog[0].assistant, null);
|
|
@@ -1276,7 +1332,7 @@ test("native settings authorize their scope and reject replaced or closed host p
|
|
|
1276
1332
|
await gate.entered.promise;
|
|
1277
1333
|
await runtime.close();
|
|
1278
1334
|
gate.finished.resolve();
|
|
1279
|
-
await assert.rejects(pending, { code: "
|
|
1280
|
-
await assert.rejects(runtime.updateNativeConversationSettings(request), { code: "
|
|
1335
|
+
await assert.rejects(pending, { code: "conversation_runtime_closed" });
|
|
1336
|
+
await assert.rejects(runtime.updateNativeConversationSettings(request), { code: "conversation_handle_closed" });
|
|
1281
1337
|
assert.deepEqual([acquisitions, updates], [1, 1]);
|
|
1282
1338
|
});
|
|
@@ -5,6 +5,7 @@ import { createActionCatalogue } from "@jskit-ai/kernel/server/actions";
|
|
|
5
5
|
import { createConversationRuntime, createMemoryConversationStorage } from "../src/server/conversation/index.js";
|
|
6
6
|
import { createServiceToolCatalog } from "../src/server/lib/serviceToolCatalog.js";
|
|
7
7
|
import { createConversationTools } from "../src/server/conversation/tools.js";
|
|
8
|
+
import { checkTexts } from "@jskit-ai/agent/server";
|
|
8
9
|
|
|
9
10
|
const configuration = { systemPrompt: "Use the available application operations when needed.", integrationId: "assistant" };
|
|
10
11
|
const context = { actor: { id: "owner" }, surface: "app", applicationId: "test", subjectId: "owner" };
|
|
@@ -19,7 +20,7 @@ function actionCatalog(execute) {
|
|
|
19
20
|
const actions = createActionCatalogue();
|
|
20
21
|
actions.register({ contributorId: "test.tools", domain: "test", actions: [{
|
|
21
22
|
id: "numbers.add", version: 1, kind: "command", channels: ["automation"], surfaces: ["app"],
|
|
22
|
-
permission: { require: "authenticated" },
|
|
23
|
+
permission: { require: "authenticated" },
|
|
23
24
|
input: { schema: createSchema({ left: { type: "number", required: true }, right: { type: "number", required: true } }), mode: "replace" },
|
|
24
25
|
output: { schema: createSchema({ total: { type: "number", required: true } }), mode: "replace" }, execute
|
|
25
26
|
}] });
|
|
@@ -73,6 +74,8 @@ test("API tools use authorized action contracts, save results and resume the sam
|
|
|
73
74
|
|
|
74
75
|
// Carried from assistant-runtime's original split-tag lifecycle case. Only the
|
|
75
76
|
// action name/arguments and confirmed API framing adapt to the common fixture.
|
|
77
|
+
// A call written out as text never runs: the reply carries on once, told so, and
|
|
78
|
+
// the model then calls the action itself.
|
|
76
79
|
test("split internal tags and tool arguments never become common answer deltas", async t => {
|
|
77
80
|
const f = await fixture(t, { response: (_body, round, name) => round === 1 ? new Response([
|
|
78
81
|
"<th", "ink>private reasoning", "</thi", "nk>", "<|DSML|function_",
|
|
@@ -80,31 +83,35 @@ test("split internal tags and tool arguments never become common answer deltas",
|
|
|
80
83
|
"</|DSML|invoke></|DSML|function_calls>"
|
|
81
84
|
].map(content => frame({ content })).join("") + frame({}, "stop"), {
|
|
82
85
|
headers: { "content-type": "text/event-stream" }
|
|
83
|
-
}) : response({ text: "The search is complete." }) });
|
|
86
|
+
}) : round === 2 ? response({ name }) : response({ text: "The search is complete." }) });
|
|
84
87
|
await f.conversation.send(message);
|
|
85
88
|
const result = await f.conversation.wait();
|
|
86
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);
|
|
87
91
|
assert.deepEqual(f.observed.events.filter(event => event.type === "message" && event.status !== "complete")
|
|
88
92
|
.map(event => event.text), ["The search is complete."]);
|
|
89
93
|
assert.equal(result.conversationLog[0].assistant.text, "The search is complete.");
|
|
90
94
|
});
|
|
91
95
|
|
|
92
|
-
test("text-encoded calls
|
|
96
|
+
test("text-encoded calls never run or reach the person, even when written again after the check", async t => {
|
|
93
97
|
const f = await fixture(t, { response: (_body, round, name) => round < 3 ? new Response(frame({ content:
|
|
94
98
|
`<function_calls><invoke name="${name}">${round === 1 ? call.arguments : '{"left":4,"right":5}'}</invoke></function_calls>`
|
|
95
99
|
}) + frame({}, "stop"), { headers: { "content-type": "text/event-stream" } }) : response() });
|
|
96
100
|
await f.conversation.send(message);
|
|
97
101
|
const result = await f.conversation.wait();
|
|
98
|
-
assert.deepEqual(f.observed.executions, [
|
|
99
|
-
assert.equal(
|
|
100
|
-
assert.equal(
|
|
102
|
+
assert.deepEqual(f.observed.executions, []);
|
|
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);
|
|
105
|
+
assert.deepEqual(result.conversationLog[0].metadata.applicationTools, []);
|
|
106
|
+
assert.equal(f.observed.events.some(event => event.type === "message" && /invoke|function_calls/u.test(event.text)), false);
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
test("text-encoded calls still require confirmed completion and raw-output limits", async t => {
|
|
104
110
|
for (const oversized of [false, true]) {
|
|
105
111
|
const f = await fixture(t, { limits: oversized ? { maxOutputCharacters: 100 } : {},
|
|
106
112
|
response: (_body, _round, name) => new Response(frame({ content:
|
|
107
|
-
|
|
113
|
+
// Inline <think> reasoning is reasoning, bounded by tokens; the limit counts reply text.
|
|
114
|
+
(oversized ? "Working on it. ".repeat(10) : "") +
|
|
108
115
|
`<function_calls><invoke name="${name}">${call.arguments}</invoke></function_calls>`
|
|
109
116
|
}) + (oversized ? frame({}, "stop") : ""), { headers: { "content-type": "text/event-stream" } }) });
|
|
110
117
|
await f.conversation.send(message);
|
|
@@ -186,18 +193,13 @@ test("the runtime applies application tool policy to schemas and an unsolicited
|
|
|
186
193
|
assert.equal(result.conversationLog[0].metadata.runtime.status, "complete");
|
|
187
194
|
assert.equal(result.error, "");
|
|
188
195
|
assert.equal(result.conversationLog[0].assistant.text, "The total is five.");
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
assert.
|
|
192
|
-
assert.equal(recorded.id, call.id);
|
|
193
|
-
assert.equal(recorded.name, f.name);
|
|
194
|
-
assert.equal(recorded.arguments, call.arguments);
|
|
195
|
-
assert.equal(recorded.status, "complete");
|
|
196
|
-
assert.deepEqual(recorded.result, refusal);
|
|
196
|
+
// The engine answers a call to a tool it was not given as a tool error; no
|
|
197
|
+
// application receipt is made, since nothing could run.
|
|
198
|
+
assert.deepEqual(result.conversationLog[0].metadata.applicationTools, []);
|
|
197
199
|
const feedback = f.observed.requests[1].messages.filter(message => message.role === "tool");
|
|
198
200
|
assert.equal(feedback.length, 1);
|
|
199
201
|
assert.equal(feedback[0].tool_call_id, call.id);
|
|
200
|
-
assert.
|
|
202
|
+
assert.match(feedback[0].content, new RegExp(`unavailable tool '${f.name}'`, "u"));
|
|
201
203
|
});
|
|
202
204
|
|
|
203
205
|
test("incomplete model tool streams cannot execute application actions", async t => {
|
|
@@ -211,7 +213,7 @@ test("incomplete model tool streams cannot execute application actions", async t
|
|
|
211
213
|
});
|
|
212
214
|
|
|
213
215
|
test("tool limits bound continuation even when the model keeps requesting actions", async t => {
|
|
214
|
-
const f = await fixture(t, { limits: { maxToolCalls: 2 }, response: (_body, round, name) => response({ name, id: `call_${round}` }) });
|
|
216
|
+
const f = await fixture(t, { limits: { maxToolCalls: 2, maxSteps: 5 }, response: (_body, round, name) => response({ name, id: `call_${round}` }) });
|
|
215
217
|
await f.conversation.send(message);
|
|
216
218
|
const result = await f.conversation.wait();
|
|
217
219
|
assert.equal(f.observed.executions.length, 2);
|
|
@@ -219,6 +221,25 @@ test("tool limits bound continuation even when the model keeps requesting action
|
|
|
219
221
|
assert.match(result.error, /tool-call limit/);
|
|
220
222
|
});
|
|
221
223
|
|
|
224
|
+
test("by default the step limit stays within the action-call limit, so the model gives its own account", async t => {
|
|
225
|
+
const account = "I added the numbers twice and stopped there.";
|
|
226
|
+
const answer = content => Response.json({ id: "once", object: "chat.completion", created: 0, model: "exact-model",
|
|
227
|
+
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }] });
|
|
228
|
+
const f = await fixture(t, { limits: { maxToolCalls: 2 }, response: (body, round, name) => {
|
|
229
|
+
if (body.stream) return response({ name, id: `call_${round}` });
|
|
230
|
+
return /^You check an assistant's reply/u.test(body.messages[0].content) ? answer('{"unsupported": []}') : answer(account);
|
|
231
|
+
} });
|
|
232
|
+
await f.conversation.send(message);
|
|
233
|
+
const result = await f.conversation.wait();
|
|
234
|
+
assert.equal(f.observed.executions.length, 2);
|
|
235
|
+
assert.deepEqual(f.observed.requests.map(body => body.stream === true), [true, true, false, false],
|
|
236
|
+
"Two steps, then the engine's account of where it got to and its check of that account's claims");
|
|
237
|
+
assert.match(f.observed.requests[2].messages[0].content, /stopped early because I reached the most steps/u);
|
|
238
|
+
assert.equal(result.error, "");
|
|
239
|
+
assert.equal(result.conversationLog[0].metadata.runtime.status, "complete");
|
|
240
|
+
assert.equal(result.conversationLog[0].assistant.text, account);
|
|
241
|
+
});
|
|
242
|
+
|
|
222
243
|
test("cancel waits for an admitted application action to settle and saves its result", async t => {
|
|
223
244
|
const entered = Promise.withResolvers(), complete = Promise.withResolvers();
|
|
224
245
|
const f = await fixture(t, { execute: () => { entered.resolve(); return complete.promise; } });
|
|
@@ -732,7 +753,7 @@ test("bound admission current guard fences a held host effect after steering or
|
|
|
732
753
|
await f.conversation.send({ messageId: "guard-steer", text: "A new instruction.", steer: true });
|
|
733
754
|
await f.currentCommand(1);
|
|
734
755
|
boundary.resolve();
|
|
735
|
-
await assert.rejects(held, { code: "
|
|
756
|
+
await assert.rejects(held, { code: "conversation_application_tool_message_stale" });
|
|
736
757
|
await f.tools().execute({ id: "guard-contract", name: "assistant_action_contract",
|
|
737
758
|
arguments: JSON.stringify({ actionId: "numbers.add", version: 1 }) });
|
|
738
759
|
const current = f.observed.mappings.at(-1).admission;
|
|
@@ -740,7 +761,7 @@ test("bound admission current guard fences a held host effect after steering or
|
|
|
740
761
|
assert.equal(current.messageId, "guard-steer");
|
|
741
762
|
f.finish.resolve();
|
|
742
763
|
await f.conversation.wait();
|
|
743
|
-
assert.throws(() => current.assertCurrent(), { code: "
|
|
764
|
+
assert.throws(() => current.assertCurrent(), { code: "conversation_application_tool_message_stale" });
|
|
744
765
|
assert.equal(f.observed.executions.length, 1, "Inspecting current authority must not dispatch or repeat work");
|
|
745
766
|
});
|
|
746
767
|
|
|
@@ -826,7 +847,7 @@ test("bound admission completion guard retains only its latest successful native
|
|
|
826
847
|
const original = f.observed.mappings[0].admission;
|
|
827
848
|
assert.equal(typeof original.assertCompletionCurrent, "function");
|
|
828
849
|
assert.equal(JSON.parse(JSON.stringify(original)).assertCompletionCurrent, undefined);
|
|
829
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
850
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" },
|
|
830
851
|
"A working native turn cannot authorize completion");
|
|
831
852
|
if (boundary === "stopped") {
|
|
832
853
|
await f.conversation.cancel();
|
|
@@ -839,16 +860,16 @@ test("bound admission completion guard retains only its latest successful native
|
|
|
839
860
|
}
|
|
840
861
|
f.finish.resolve();
|
|
841
862
|
await f.conversation.wait();
|
|
842
|
-
assert.throws(() => original.assertCurrent(), { code: "
|
|
863
|
+
assert.throws(() => original.assertCurrent(), { code: "conversation_application_tool_message_stale" },
|
|
843
864
|
"Native settlement never restores live tool authority");
|
|
844
865
|
if (boundary === "steered") {
|
|
845
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
866
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" });
|
|
846
867
|
return;
|
|
847
868
|
}
|
|
848
869
|
original.assertCompletionCurrent();
|
|
849
870
|
if (boundary === "closed") {
|
|
850
871
|
await f.runtime.close();
|
|
851
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
872
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" });
|
|
852
873
|
} else if (boundary === "rejected-successor") {
|
|
853
874
|
f.binding.prepareInput = async () => { throw new Error("The newer input was rejected before native dispatch."); };
|
|
854
875
|
await assert.rejects(f.conversation.send({ messageId: "unaccepted-successor", text: "Never admitted." }),
|
|
@@ -865,7 +886,7 @@ test("bound admission completion guard retains only its latest successful native
|
|
|
865
886
|
const receipt = await f.conversation.send({ messageId: "recovered-successor", text: "An original recovered native receipt." });
|
|
866
887
|
assert.equal(receipt.recovered, true);
|
|
867
888
|
await f.conversation.wait();
|
|
868
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
889
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" },
|
|
869
890
|
"Verified recovered admission also retires the previous completion receipt");
|
|
870
891
|
} else if (boundary.startsWith("successor-")) {
|
|
871
892
|
const gate = Promise.withResolvers();
|
|
@@ -878,11 +899,11 @@ test("bound admission completion guard retains only its latest successful native
|
|
|
878
899
|
await f.conversation.send({ messageId: "accepted-successor", text: "A genuinely accepted later input." });
|
|
879
900
|
await f.currentCommand(1);
|
|
880
901
|
if (boundary === "successor-active") {
|
|
881
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
902
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" });
|
|
882
903
|
gate.resolve();
|
|
883
904
|
}
|
|
884
905
|
await f.conversation.wait();
|
|
885
|
-
assert.throws(() => original.assertCompletionCurrent(), { code: "
|
|
906
|
+
assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_native_completion_message_stale" },
|
|
886
907
|
"A later accepted receipt stays current after its active slot clears");
|
|
887
908
|
} finally { gate.resolve(); }
|
|
888
909
|
}
|
|
@@ -19,13 +19,19 @@ async function fixture(t) {
|
|
|
19
19
|
test("file conversations satisfy the shared storage contract and survive a new process", async (t) => {
|
|
20
20
|
const f = await fixture(t);
|
|
21
21
|
const { scope, transcript } = await verifyConversationStorageContract(f.storage, { runtime: true });
|
|
22
|
-
const
|
|
22
|
+
const log = await transcript.readConversationLog(scope);
|
|
23
|
+
const expected = { log, summary: await f.storage.read(scope, transaction => transaction.readSummary()),
|
|
24
|
+
after: await transcript.readConversationLog(scope, { afterTurnId: log[0].turnId }) };
|
|
25
|
+
assert.ok(expected.summary);
|
|
26
|
+
assert.deepEqual(expected.after, log.slice(1));
|
|
23
27
|
const { stdout } = await promisify(execFile)(process.execPath, ["--input-type=module", "-e", `
|
|
24
28
|
import { createFileConversationStorage } from ${JSON.stringify(new URL("../src/server/conversation/fileStorage.js", import.meta.url).href)};
|
|
25
29
|
import { createConversationTranscript } from ${JSON.stringify(new URL("../src/server/conversation/transcript.js", import.meta.url).href)};
|
|
26
30
|
const storage = createFileConversationStorage({ directory: process.argv[1] });
|
|
27
31
|
const transcript = createConversationTranscript({ storage });
|
|
28
|
-
|
|
32
|
+
const log = await transcript.readConversationLog(process.argv[2]);
|
|
33
|
+
process.stdout.write(JSON.stringify({ log, summary: await storage.read(process.argv[2], transaction => transaction.readSummary()),
|
|
34
|
+
after: await transcript.readConversationLog(process.argv[2], { afterTurnId: log[0].turnId }) }));
|
|
29
35
|
`, f.directory, scope]);
|
|
30
36
|
assert.deepEqual(JSON.parse(stdout), expected);
|
|
31
37
|
assert.equal((await readdir(f.directory)).some((name) => name.endsWith(".tmp")), false);
|
|
@@ -54,7 +60,8 @@ test("damaged or unsupported stored data fails explicitly and is never overwritt
|
|
|
54
60
|
await f.transcript.writeConversationUserMessage("one", { text: "Saved" });
|
|
55
61
|
const [filename] = await readdir(f.directory);
|
|
56
62
|
const path = join(f.directory, filename);
|
|
57
|
-
for (const damaged of ["{", "null", '{"version":2}', '{"version":1,"scope":"other","turns":[]}'
|
|
63
|
+
for (const damaged of ["{", "null", '{"version":2}', '{"version":1,"scope":"other","turns":[]}',
|
|
64
|
+
'{"version":1,"scope":"one","turns":[],"summary":{"text":"Earlier","throughTurnId":"000009"}}']) {
|
|
58
65
|
await writeFile(path, damaged);
|
|
59
66
|
await assert.rejects(f.transcript.readConversationLog("one"), /Conversation storage/);
|
|
60
67
|
await assert.rejects(f.transcript.writeConversationUserMessage("one", { text: "Do not replace" }), /Conversation storage/);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
|
|
3
|
-
CODEX_HELPER_THREAD_LIFECYCLES
|
|
4
|
-
|
|
5
|
-
} from "../../src/server/conversation/codexHelperThreadLedger.js";
|
|
3
|
+
CODEX_HELPER_THREAD_LIFECYCLES
|
|
4
|
+
} from "../../src/server/conversation/codexHelperThreadRecord.js";
|
|
5
|
+
import { createCodexHelperThreadLedgerOwner } from "../../src/server/conversation/codexHelperThreadLedger.js";
|
|
6
6
|
|
|
7
7
|
// This is the original test's resolved profile data, not application policy.
|
|
8
8
|
// Public keeps and tests its original strict codec against its own catalogue.
|