@jskit-ai/assistant-core 0.1.200 → 0.1.202
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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const engines = new Set(["api", "claude", "codex", "opencode"]);
|
|
2
|
+
const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
|
|
4
|
+
function invalid(message) {
|
|
5
|
+
throw Object.assign(new Error(message), { code: "conversation_runtime_upgrade_unavailable" });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function hasUnfinishedConversationRewind(runtime) {
|
|
9
|
+
const pending = runtime?.replacement;
|
|
10
|
+
return pending?.reason === "rewind" || pending?.request?.reason === "rewind" ||
|
|
11
|
+
object(pending?.request) && Object.hasOwn(pending.request, "throughTurnId") ||
|
|
12
|
+
Array.isArray(pending?.supersededTurns) && pending.supersededTurns.length > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateSegment(segment) {
|
|
16
|
+
if (!object(segment) || !engines.has(segment.engine) || typeof segment.segmentId !== "string" ||
|
|
17
|
+
!segment.segmentId || !object(segment.configuration) || !object(segment.seen) ||
|
|
18
|
+
Object.values(segment.seen).some(value => typeof value !== "string") ||
|
|
19
|
+
Object.hasOwn(segment, "preparedAt") && typeof segment.preparedAt !== "string") {
|
|
20
|
+
invalid("Conversation runtime has an unsupported segment. Inspect its saved state before upgrading.");
|
|
21
|
+
}
|
|
22
|
+
if (segment.engine !== "api" && !object(segment.binding)) {
|
|
23
|
+
invalid("A native conversation has no saved binding. Restore its exact identity before upgrading.");
|
|
24
|
+
}
|
|
25
|
+
if (segment.engine === "opencode" && segment.binding.sessionId &&
|
|
26
|
+
(typeof segment.binding.databasePath !== "string" || !segment.binding.databasePath)) {
|
|
27
|
+
invalid("This OpenCode conversation uses an older private database binding. A complete offline native-history upgrade is required before this metadata upgrade; its database was not changed.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nativeIdentity(segment) {
|
|
32
|
+
return segment.engine === "codex" ? segment.binding.threadId
|
|
33
|
+
: segment.engine === "claude" ? segment.binding.conversationId : segment.binding.sessionId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function inspectRequest(request, segment, version, warnings) {
|
|
37
|
+
if (request === undefined) return;
|
|
38
|
+
if (!object(request) || typeof request.messageId !== "string" || !request.messageId ||
|
|
39
|
+
typeof request.text !== "string" || !Array.isArray(request.attachments) ||
|
|
40
|
+
typeof request.at !== "string") {
|
|
41
|
+
invalid("Conversation runtime has an unsupported pending request. Inspect it before upgrading.");
|
|
42
|
+
}
|
|
43
|
+
if (segment.engine === "api") return;
|
|
44
|
+
const threadId = nativeIdentity(segment);
|
|
45
|
+
if (version === 3 && !Object.hasOwn(request, "inspectionOnly")) {
|
|
46
|
+
if (typeof request.attempted !== "boolean" || request.attempted &&
|
|
47
|
+
(typeof threadId !== "string" || !threadId || request.threadId !== threadId || typeof request.message !== "string") ||
|
|
48
|
+
Object.hasOwn(request, "message") && (typeof request.message !== "string" || typeof request.displayMessage !== "string" ||
|
|
49
|
+
!Array.isArray(request.displayAttachments) || !Array.isArray(request.attachmentIds) || !object(request.seen)) ||
|
|
50
|
+
Object.hasOwn(request, "contextText") && (typeof request.contextText !== "string" || !Array.isArray(request.contextAttachments)) ||
|
|
51
|
+
Object.hasOwn(request, "attachmentManifest") && typeof request.attachmentManifest !== "string") {
|
|
52
|
+
invalid("A native delivery journal has an unsupported frozen prompt or identity. Inspect it before upgrading.");
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof threadId !== "string" || !threadId) {
|
|
57
|
+
invalid("A pending native request has no exact saved conversation identity. Resolve its delivery with the previous candidate before upgrading; it cannot be replayed safely.");
|
|
58
|
+
}
|
|
59
|
+
if (version === 3) {
|
|
60
|
+
if (request.inspectionOnly !== true || request.threadId !== threadId || Object.hasOwn(request, "message")) {
|
|
61
|
+
invalid("An inspection-only request conflicts with its saved native identity or prompt. Inspect it before upgrading.");
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (["message", "displayMessage", "displayAttachments", "attachmentIds", "attempted", "threadId", "turnMetadata", "inspectionOnly",
|
|
66
|
+
"contextText", "contextAttachments", "attachmentManifest"]
|
|
67
|
+
.some(name => Object.hasOwn(request, name))) {
|
|
68
|
+
invalid("A version 2 pending request contains unknown native delivery fields. Inspect it before upgrading.");
|
|
69
|
+
}
|
|
70
|
+
// Version 2 saved authored content and a cursor, but never the rendered prompt.
|
|
71
|
+
// The existing exact binding is enough for receipt inspection, never replay.
|
|
72
|
+
request.inspectionOnly = true;
|
|
73
|
+
request.threadId = threadId;
|
|
74
|
+
warnings.push(`The retained ${segment.engine} request requires native receipt inspection. Its missing prompt was not reconstructed and will not be replayed.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Pure offline transform. The application owns enumeration, backups and publication. */
|
|
78
|
+
export function upgradeConversationRuntimeState({ metadata, conversationLog }) {
|
|
79
|
+
if (!object(metadata) || !Array.isArray(conversationLog)) {
|
|
80
|
+
invalid("Conversation runtime upgrade requires metadata and the complete stored transcript.");
|
|
81
|
+
}
|
|
82
|
+
if (metadata.runtime === undefined) return { metadata, changed: false, warnings: [] };
|
|
83
|
+
const source = metadata.runtime;
|
|
84
|
+
if (!object(source) || ![2, 3].includes(source.version) || !Array.isArray(source.predecessors)) {
|
|
85
|
+
invalid("This conversation has an unsupported runtime version. Inspect its original format and provide an offline conversion before opening it.");
|
|
86
|
+
}
|
|
87
|
+
if (hasUnfinishedConversationRewind(source)) {
|
|
88
|
+
invalid("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before upgrading; no history was changed.");
|
|
89
|
+
}
|
|
90
|
+
if (source.version === 3 && (typeof source.lastEngine !== "string" || source.lastEngine && !engines.has(source.lastEngine))) {
|
|
91
|
+
invalid("Conversation runtime has an invalid last-delivered engine. Inspect it before upgrading.");
|
|
92
|
+
}
|
|
93
|
+
const runtime = structuredClone(source);
|
|
94
|
+
const warnings = [];
|
|
95
|
+
for (const segment of [...runtime.predecessors, runtime]) {
|
|
96
|
+
validateSegment(segment);
|
|
97
|
+
inspectRequest(segment.request, segment, source.version, warnings);
|
|
98
|
+
}
|
|
99
|
+
if (runtime.replacement !== undefined) {
|
|
100
|
+
const pending = runtime.replacement;
|
|
101
|
+
if (!object(pending) || !engines.has(pending.engine) || typeof pending.segmentId !== "string" ||
|
|
102
|
+
!pending.segmentId || !object(pending.configuration) ||
|
|
103
|
+
Object.hasOwn(pending, "preparedAt") && typeof pending.preparedAt !== "string") {
|
|
104
|
+
invalid("Conversation runtime has an unsupported unfinished replacement. Finish or inspect it before upgrading.");
|
|
105
|
+
}
|
|
106
|
+
if (pending.binding) {
|
|
107
|
+
validateSegment(pending);
|
|
108
|
+
inspectRequest(pending.submission, pending, source.version, warnings);
|
|
109
|
+
} else if (pending.submission !== undefined) {
|
|
110
|
+
invalid("An unfinished replacement has a request without its exact saved binding. Inspect it before upgrading.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (source.version === 3) return { metadata, changed: false, warnings: [] };
|
|
114
|
+
let lastEngine = "";
|
|
115
|
+
for (const turn of conversationLog) {
|
|
116
|
+
if (!object(turn) || !Array.isArray(turn.messages)) invalid("Conversation history has an unsupported turn. Inspect it before upgrading.");
|
|
117
|
+
const attribution = turn.metadata?.runtime;
|
|
118
|
+
if (!attribution || attribution.supersededBy || !turn.messages.some(message => ["user", "system"].includes(message.role))) continue;
|
|
119
|
+
if (!engines.has(attribution.engine)) invalid("An accepted conversation turn has an unknown engine. Inspect its attribution before upgrading.");
|
|
120
|
+
lastEngine = attribution.engine;
|
|
121
|
+
}
|
|
122
|
+
runtime.version = 3;
|
|
123
|
+
runtime.lastEngine = lastEngine;
|
|
124
|
+
return { metadata: { ...metadata, runtime }, changed: true, warnings };
|
|
125
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
const conversationMessageRoles = ["assistant", "commentary", "system", "thinking", "user"];
|
|
4
|
+
|
|
5
|
+
export function createConversationOperationLease() {
|
|
6
|
+
let resolveIdle = () => null;
|
|
7
|
+
const idle = new Promise((resolve) => {
|
|
8
|
+
resolveIdle = resolve;
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
idle,
|
|
12
|
+
operations: 0,
|
|
13
|
+
resolveIdle
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function beginConversationOperation(lease) {
|
|
18
|
+
lease.operations += 1;
|
|
19
|
+
return {
|
|
20
|
+
active: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function finishConversationOperation(lease, participant) {
|
|
25
|
+
participant.active = false;
|
|
26
|
+
lease.operations -= 1;
|
|
27
|
+
if (lease.operations === 0) {
|
|
28
|
+
lease.resolveIdle();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Nested native store operations share the host's current transaction. The
|
|
33
|
+
* supplied storage remains the only writer queue and commit/rollback owner.
|
|
34
|
+
*/
|
|
35
|
+
export function createReentrantConversationStorage(storage) {
|
|
36
|
+
if (typeof storage?.read !== "function" || typeof storage?.write !== "function") {
|
|
37
|
+
throw new TypeError("Conversation storage requires read(scope, callback) and write(scope, callback).");
|
|
38
|
+
}
|
|
39
|
+
const mutationContext = new AsyncLocalStorage();
|
|
40
|
+
|
|
41
|
+
function inherited(scope) {
|
|
42
|
+
const context = mutationContext.getStore();
|
|
43
|
+
return context?.scope === scope && context.participant?.active === true ? context : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function participate(context, operation) {
|
|
47
|
+
const participant = beginConversationOperation(context.lease);
|
|
48
|
+
try {
|
|
49
|
+
return await mutationContext.run({ ...context, participant }, () => operation(context.transaction));
|
|
50
|
+
} finally {
|
|
51
|
+
finishConversationOperation(context.lease, participant);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...storage,
|
|
57
|
+
read(scope, operation) {
|
|
58
|
+
const context = inherited(scope);
|
|
59
|
+
return context ? participate(context, operation) : storage.read(scope, operation);
|
|
60
|
+
},
|
|
61
|
+
write(scope, operation) {
|
|
62
|
+
const context = inherited(scope);
|
|
63
|
+
if (context) return participate(context, operation);
|
|
64
|
+
return storage.write(scope, async transaction => {
|
|
65
|
+
const lease = createConversationOperationLease();
|
|
66
|
+
const participant = beginConversationOperation(lease);
|
|
67
|
+
try {
|
|
68
|
+
return await mutationContext.run({ scope, transaction, lease, participant }, () => operation(transaction));
|
|
69
|
+
} finally {
|
|
70
|
+
finishConversationOperation(lease, participant);
|
|
71
|
+
await lease.idle;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
...(typeof storage.deleteConversation === "function"
|
|
76
|
+
? { deleteConversation: scope => storage.deleteConversation(scope) } : {})
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function conversationMessageStorageKey({ role, at, messageId } = {}) {
|
|
81
|
+
const normalizedRole = String(role || "").trim();
|
|
82
|
+
if (!conversationMessageRoles.includes(normalizedRole)) return null;
|
|
83
|
+
const date = at instanceof Date ? at : new Date(at);
|
|
84
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
85
|
+
const timestamp = date.toISOString().replace(/[-:]/gu, "").replace(".", "");
|
|
86
|
+
const normalizedMessageId = String(messageId || "").trim();
|
|
87
|
+
const idSuffix = normalizedMessageId ? `.${normalizedMessageId}` : "";
|
|
88
|
+
// The original transcript overwrote this same role/timestamp/id file. Its
|
|
89
|
+
// filesystem suffix restrictions do not apply to record-native message IDs.
|
|
90
|
+
return `${normalizedRole}.${timestamp}${idSuffix}.md`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Transaction implementation for single-writer record adapters. This serializes
|
|
94
|
+
* one instance's writes; a database with multiple writers must also hold its own
|
|
95
|
+
* transaction/lock across readRecord and writeRecord, or implement read/write directly.
|
|
96
|
+
*/
|
|
97
|
+
export function createConversationStorage({ readRecord, writeRecord, deleteRecord }) {
|
|
98
|
+
const pending = new Map();
|
|
99
|
+
|
|
100
|
+
function key(scope) {
|
|
101
|
+
if (typeof scope !== "string" || !scope.trim()) throw new TypeError("Conversation scope must be a nonempty string.");
|
|
102
|
+
return scope;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function transaction(record) {
|
|
106
|
+
const turns = record.turns;
|
|
107
|
+
return {
|
|
108
|
+
async readMetadata() { return structuredClone(record.metadata); },
|
|
109
|
+
async writeMetadata(value) { record.metadata = structuredClone(value); },
|
|
110
|
+
async updateTurnMetadata(id, patch) {
|
|
111
|
+
const turn = turns.get(id);
|
|
112
|
+
if (!turn) throw new Error("The conversation turn does not exist.");
|
|
113
|
+
turn.metadata = { ...turn.metadata, ...structuredClone(patch) };
|
|
114
|
+
},
|
|
115
|
+
async listTurnIds() { return [...turns.keys()]; },
|
|
116
|
+
async nextTurnId() { return String(Math.max(0, ...[...turns.keys()].map(Number).filter(Number.isSafeInteger)) + 1).padStart(6, "0"); },
|
|
117
|
+
async hasMessage(id) {
|
|
118
|
+
return Boolean(id) && [...turns.values()].some((turn) => turn.messages.some((message) => message.messageId === id));
|
|
119
|
+
},
|
|
120
|
+
async readTurn(id) {
|
|
121
|
+
const turn = turns.get(id);
|
|
122
|
+
if (!turn) return null;
|
|
123
|
+
const find = (role) => turn.messages.find((message) => message.role === role) || null;
|
|
124
|
+
const thinking = turn.messages.filter((message) => message.role === "thinking");
|
|
125
|
+
const commentary = turn.messages.filter((message) => message.role === "commentary");
|
|
126
|
+
const activity = [...thinking, ...commentary].sort((a, b) => a.at.localeCompare(b.at));
|
|
127
|
+
const system = find("system"), user = find("user"), assistant = find("assistant");
|
|
128
|
+
return structuredClone({
|
|
129
|
+
turnId: id, user, assistant, thinking, commentary,
|
|
130
|
+
...(system ? { system } : {}), ...(turn.metadata ? { metadata: turn.metadata } : {}),
|
|
131
|
+
messages: [system, user, ...activity, assistant].filter(Boolean)
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
async appendMessage(id, { turnMetadata, ...message }) {
|
|
135
|
+
const turn = turns.get(id) || { messages: [] };
|
|
136
|
+
if (turnMetadata) turn.metadata = structuredClone(turnMetadata);
|
|
137
|
+
const identity = conversationMessageStorageKey(message);
|
|
138
|
+
const existing = identity === null ? -1 : turn.messages.findIndex((previous) =>
|
|
139
|
+
conversationMessageStorageKey(previous) === identity);
|
|
140
|
+
if (existing < 0) turn.messages.push(structuredClone(message));
|
|
141
|
+
else turn.messages[existing] = structuredClone(message);
|
|
142
|
+
turns.set(id, turn);
|
|
143
|
+
},
|
|
144
|
+
async replaceAssistant(id, message) {
|
|
145
|
+
if (!id) throw new TypeError("Assistant replacement requires a turn id.");
|
|
146
|
+
const turn = turns.get(id) || { messages: [] };
|
|
147
|
+
const previous = turn.messages.find((entry) => entry.role === "assistant");
|
|
148
|
+
turn.messages = turn.messages.filter((entry) => entry.role !== "assistant");
|
|
149
|
+
turn.messages.push({ ...previous, ...structuredClone(message), at: previous?.at || message.at });
|
|
150
|
+
turns.set(id, turn);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async function write(scope, callback, remove = false) {
|
|
157
|
+
const id = key(scope);
|
|
158
|
+
const previous = pending.get(id) || Promise.resolve();
|
|
159
|
+
const operation = previous.then(async () => {
|
|
160
|
+
const draft = structuredClone(await readRecord(id));
|
|
161
|
+
const view = transaction(draft);
|
|
162
|
+
const result = await callback(view);
|
|
163
|
+
if (remove) await deleteRecord(id);
|
|
164
|
+
else await writeRecord(id, draft, view);
|
|
165
|
+
return result;
|
|
166
|
+
});
|
|
167
|
+
const settled = operation.then(() => undefined, () => undefined);
|
|
168
|
+
pending.set(id, settled);
|
|
169
|
+
try { return await operation; }
|
|
170
|
+
finally { if (pending.get(id) === settled) pending.delete(id); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
write,
|
|
175
|
+
async read(scope, callback) {
|
|
176
|
+
const id = key(scope);
|
|
177
|
+
await pending.get(id);
|
|
178
|
+
return callback(transaction(structuredClone(await readRecord(id))));
|
|
179
|
+
},
|
|
180
|
+
async deleteConversation(scope) {
|
|
181
|
+
await write(scope, async () => {}, true);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -11,7 +11,7 @@ export function createConversationStreams({ clock = () => new Date() } = {}) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function update(scope, { turnId, messageId, role = "assistant", text, delta, at } = {}) {
|
|
14
|
+
function update(scope, { turnId, messageId, role = "assistant", text, delta, at, origin, nativeIdentity, outputId } = {}) {
|
|
15
15
|
if (!turnId || !messageId || !["assistant", "commentary"].includes(role)) return null;
|
|
16
16
|
let stream = streams.get(scope);
|
|
17
17
|
if (!stream || stream.turnId !== turnId) {
|
|
@@ -22,12 +22,17 @@ export function createConversationStreams({ clock = () => new Date() } = {}) {
|
|
|
22
22
|
const previous = stream.messages.get(messageId);
|
|
23
23
|
const next = {
|
|
24
24
|
messageId,
|
|
25
|
+
...((previous?.outputId || outputId) ? { outputId: previous?.outputId || outputId } : {}),
|
|
26
|
+
// Native grouping is private even when its authored origin is known.
|
|
27
|
+
// Ordinary runtime output already carries the exact authored row identity.
|
|
28
|
+
...(["user", "application"].includes(origin || previous?.origin)
|
|
29
|
+
? { ...(!nativeIdentity ? { turnId } : {}), origin: origin || previous.origin } : {}),
|
|
25
30
|
role: previous?.role || role,
|
|
26
31
|
at: previous?.at || at || clock().toISOString(),
|
|
27
32
|
text: typeof delta === "string" ? (previous?.text || "") + delta : (text ?? previous?.text ?? ""),
|
|
28
33
|
status: "inProgress"
|
|
29
34
|
};
|
|
30
|
-
if (previous?.text === next.text && previous.role === next.role) return null;
|
|
35
|
+
if (previous?.text === next.text && previous.role === next.role && previous.outputId === next.outputId) return null;
|
|
31
36
|
stream.messages.set(messageId, next);
|
|
32
37
|
revision += 1;
|
|
33
38
|
return read(scope);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const OUTPUT_SCHEMA_MAX_BYTES = 64 * 1024;
|
|
2
|
+
const OUTPUT_SCHEMA_MAX_DEPTH = 8;
|
|
3
|
+
const OUTPUT_SCHEMA_MAX_PROPERTIES = 64;
|
|
4
|
+
const OUTPUT_SCHEMA_MAX_ENUM_VALUES = 64;
|
|
5
|
+
const OUTPUT_SCHEMA_MAX_ALTERNATIVES = 64;
|
|
6
|
+
|
|
7
|
+
function isPlainRecord(value) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const prototype = Object.getPrototypeOf(value);
|
|
12
|
+
return prototype === Object.prototype || prototype === null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Original bounded native-helper schema contract. The host supplies its existing
|
|
16
|
+
// error policy; native and API process ownership is outside this validator.
|
|
17
|
+
export function validateConversationOutputSchema(outputSchema, {
|
|
18
|
+
maxOutputCharacters,
|
|
19
|
+
createError = (message, details) => Object.assign(new TypeError(message), { details })
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (!Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) {
|
|
22
|
+
throw new TypeError("Structured output requires a finite positive output limit.");
|
|
23
|
+
}
|
|
24
|
+
function assertOutputSchemaKeys(schema = {}, allowed = [], schemaPath = "$") {
|
|
25
|
+
const allowedKeys = new Set(["description", "title", "type", ...allowed]);
|
|
26
|
+
if (Object.keys(schema).some((key) => !allowedKeys.has(key))) {
|
|
27
|
+
throw createError(
|
|
28
|
+
`Helper output schema ${schemaPath} contains an unsupported keyword.`,
|
|
29
|
+
{ field: "outputSchema" }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function strictOutputSchemaMaximumCharacters(schema = null, schemaPath = "$", depth = 0) {
|
|
35
|
+
if (!isPlainRecord(schema)) {
|
|
36
|
+
throw createError(
|
|
37
|
+
`Helper output schema ${schemaPath} must be an object.`,
|
|
38
|
+
{ field: "outputSchema" }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (depth > OUTPUT_SCHEMA_MAX_DEPTH) {
|
|
42
|
+
throw createError(
|
|
43
|
+
"Helper output schema is nested too deeply.",
|
|
44
|
+
{ field: "outputSchema" }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (Object.hasOwn(schema, "anyOf")) {
|
|
48
|
+
assertOutputSchemaKeys(schema, ["anyOf"], schemaPath);
|
|
49
|
+
if (depth === 0 || Object.hasOwn(schema, "type") || !Array.isArray(schema.anyOf) ||
|
|
50
|
+
schema.anyOf.length < 1 || schema.anyOf.length > OUTPUT_SCHEMA_MAX_ALTERNATIVES ||
|
|
51
|
+
schema.anyOf.some((alternative) => alternative?.type !== "object")) {
|
|
52
|
+
throw createError(
|
|
53
|
+
`Helper output schema ${schemaPath} requires bounded, closed object alternatives below its root.`,
|
|
54
|
+
{ field: "outputSchema" }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return Math.max(...schema.anyOf.map((alternative, index) =>
|
|
58
|
+
strictOutputSchemaMaximumCharacters(alternative, `${schemaPath}.anyOf[${index}]`, depth + 1)));
|
|
59
|
+
}
|
|
60
|
+
if (typeof schema.type !== "string") {
|
|
61
|
+
throw createError(
|
|
62
|
+
`Helper output schema ${schemaPath} must declare one type.`,
|
|
63
|
+
{ field: "outputSchema" }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (schema.type === "object") {
|
|
67
|
+
assertOutputSchemaKeys(
|
|
68
|
+
schema,
|
|
69
|
+
["additionalProperties", "properties", "required"],
|
|
70
|
+
schemaPath
|
|
71
|
+
);
|
|
72
|
+
if (schema.additionalProperties !== false || !isPlainRecord(schema.properties)) {
|
|
73
|
+
throw createError(
|
|
74
|
+
`Helper object schema ${schemaPath} must declare properties and reject additional properties.`,
|
|
75
|
+
{ field: "outputSchema" }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const propertyNames = Object.keys(schema.properties);
|
|
79
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
80
|
+
if (
|
|
81
|
+
propertyNames.length === 0 ||
|
|
82
|
+
propertyNames.length > OUTPUT_SCHEMA_MAX_PROPERTIES ||
|
|
83
|
+
propertyNames.some((name) => !name || name.length > 128) ||
|
|
84
|
+
required.length !== propertyNames.length ||
|
|
85
|
+
new Set(required).size !== required.length ||
|
|
86
|
+
propertyNames.some((name) => !required.includes(name))
|
|
87
|
+
) {
|
|
88
|
+
throw createError(
|
|
89
|
+
`Helper object schema ${schemaPath} must require every declared property.`,
|
|
90
|
+
{ field: "outputSchema" }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return propertyNames.reduce((total, name, index) => (
|
|
94
|
+
total +
|
|
95
|
+
(index === 0 ? 0 : 1) +
|
|
96
|
+
JSON.stringify(name).length +
|
|
97
|
+
1 +
|
|
98
|
+
strictOutputSchemaMaximumCharacters(schema.properties[name], `${schemaPath}.${name}`, depth + 1)
|
|
99
|
+
), 2);
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === "array") {
|
|
102
|
+
assertOutputSchemaKeys(schema, ["items", "maxItems", "minItems"], schemaPath);
|
|
103
|
+
if (!Number.isSafeInteger(schema.maxItems) || schema.maxItems < 0) {
|
|
104
|
+
throw createError(
|
|
105
|
+
`Helper array schema ${schemaPath} must have a finite maxItems value.`,
|
|
106
|
+
{ field: "outputSchema" }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
schema.minItems !== undefined &&
|
|
111
|
+
(!Number.isSafeInteger(schema.minItems) || schema.minItems < 0 || schema.minItems > schema.maxItems)
|
|
112
|
+
) {
|
|
113
|
+
throw createError(
|
|
114
|
+
`Helper array schema ${schemaPath} has an invalid minItems value.`,
|
|
115
|
+
{ field: "outputSchema" }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const itemMaximum = strictOutputSchemaMaximumCharacters(schema.items, `${schemaPath}[]`, depth + 1);
|
|
119
|
+
const maximum = 2 + (schema.maxItems * itemMaximum) + Math.max(0, schema.maxItems - 1);
|
|
120
|
+
if (!Number.isSafeInteger(maximum)) {
|
|
121
|
+
throw createError(
|
|
122
|
+
"Helper output schema exceeds its finite bound.",
|
|
123
|
+
{ field: "outputSchema" }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return maximum;
|
|
127
|
+
}
|
|
128
|
+
if (schema.type === "string") {
|
|
129
|
+
assertOutputSchemaKeys(schema, ["enum", "maxLength", "minLength"], schemaPath);
|
|
130
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
131
|
+
if (
|
|
132
|
+
schema.enum.length > OUTPUT_SCHEMA_MAX_ENUM_VALUES ||
|
|
133
|
+
schema.enum.some((value) => typeof value !== "string")
|
|
134
|
+
) {
|
|
135
|
+
throw createError(
|
|
136
|
+
`Helper string schema ${schemaPath} has an invalid enum.`,
|
|
137
|
+
{ field: "outputSchema" }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return Math.max(...schema.enum.map((value) => JSON.stringify(value).length));
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isSafeInteger(schema.maxLength) || schema.maxLength <= 0) {
|
|
143
|
+
throw createError(
|
|
144
|
+
`Helper string schema ${schemaPath} must have a finite positive maxLength.`,
|
|
145
|
+
{ field: "outputSchema" }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
schema.minLength !== undefined &&
|
|
150
|
+
(!Number.isSafeInteger(schema.minLength) || schema.minLength < 0 || schema.minLength > schema.maxLength)
|
|
151
|
+
) {
|
|
152
|
+
throw createError(
|
|
153
|
+
`Helper string schema ${schemaPath} has an invalid minLength value.`,
|
|
154
|
+
{ field: "outputSchema" }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
// JSON may encode each UTF-16 code unit as a six-character `\uXXXX` escape.
|
|
158
|
+
// Count that worst case so the schema can never admit raw JSON beyond the
|
|
159
|
+
// resolved response limit even when every string character needs escaping.
|
|
160
|
+
const maximum = (schema.maxLength * 6) + 2;
|
|
161
|
+
if (!Number.isSafeInteger(maximum)) {
|
|
162
|
+
throw createError(
|
|
163
|
+
"Helper output schema exceeds its finite bound.",
|
|
164
|
+
{ field: "outputSchema" }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return maximum;
|
|
168
|
+
}
|
|
169
|
+
if (schema.type === "boolean" || schema.type === "null") {
|
|
170
|
+
assertOutputSchemaKeys(schema, [], schemaPath);
|
|
171
|
+
return 5;
|
|
172
|
+
}
|
|
173
|
+
throw createError(
|
|
174
|
+
`Helper output schema ${schemaPath} uses an unsupported type.`,
|
|
175
|
+
{ field: "outputSchema" }
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let schemaBytes = 0;
|
|
180
|
+
try {
|
|
181
|
+
schemaBytes = Buffer.byteLength(JSON.stringify(outputSchema), "utf8");
|
|
182
|
+
} catch {
|
|
183
|
+
throw createError(
|
|
184
|
+
"Helper output schema is not serializable.",
|
|
185
|
+
{ field: "outputSchema" }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (schemaBytes > OUTPUT_SCHEMA_MAX_BYTES) {
|
|
189
|
+
throw createError(
|
|
190
|
+
"Helper output schema exceeds its request limit.",
|
|
191
|
+
{ field: "outputSchema" }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const maximumCharacters = strictOutputSchemaMaximumCharacters(outputSchema);
|
|
195
|
+
if (maximumCharacters > maxOutputCharacters) {
|
|
196
|
+
throw createError(
|
|
197
|
+
"Helper output schema can exceed the resolved output limit.",
|
|
198
|
+
{
|
|
199
|
+
maximumCharacters,
|
|
200
|
+
maxOutputCharacters
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return outputSchema;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Catalogue schemas carry validator metadata that is not part of the native
|
|
208
|
+
// structured response contract. Keep validation keywords and field names intact.
|
|
209
|
+
export function conversationOutputSchema(schema) {
|
|
210
|
+
const result = Object.fromEntries(Object.entries(schema).filter(([key]) =>
|
|
211
|
+
key !== "$schema" && key !== "x-json-rest-schema"));
|
|
212
|
+
if (schema.properties) result.properties = Object.fromEntries(Object.entries(schema.properties)
|
|
213
|
+
.map(([key, value]) => [key, conversationOutputSchema(value)]));
|
|
214
|
+
if (schema.items) result.items = conversationOutputSchema(schema.items);
|
|
215
|
+
if (schema.anyOf) result.anyOf = schema.anyOf.map(conversationOutputSchema);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate the same finite schema subset used for native Helper responses before
|
|
220
|
+
// allowing a structured reply to select an application operation.
|
|
221
|
+
export function parseConversationOutput(text, schema) {
|
|
222
|
+
function matches(value, definition) {
|
|
223
|
+
if (definition.anyOf) return definition.anyOf.some((alternative) => matches(value, alternative));
|
|
224
|
+
if (definition.type === "object") {
|
|
225
|
+
return isPlainRecord(value) && Object.keys(value).every((key) => Object.hasOwn(definition.properties, key)) &&
|
|
226
|
+
definition.required.every((key) => Object.hasOwn(value, key)) &&
|
|
227
|
+
Object.entries(definition.properties).every(([key, field]) => matches(value[key], field));
|
|
228
|
+
}
|
|
229
|
+
if (definition.type === "array") return Array.isArray(value) && value.length <= definition.maxItems &&
|
|
230
|
+
value.length >= (definition.minItems || 0) && value.every((item) => matches(item, definition.items));
|
|
231
|
+
if (definition.type === "string") return typeof value === "string" &&
|
|
232
|
+
(definition.enum ? definition.enum.includes(value) : value.length <= definition.maxLength) &&
|
|
233
|
+
value.length >= (definition.minLength || 0);
|
|
234
|
+
if (definition.type === "boolean") return typeof value === "boolean";
|
|
235
|
+
return definition.type === "null" && value === null;
|
|
236
|
+
}
|
|
237
|
+
const value = JSON.parse(String(text || ""));
|
|
238
|
+
if (!matches(value, schema)) throw new TypeError("The assistant returned an invalid structured response.");
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One instruction binding per native conversation. The adapter owns delivery
|
|
3
|
+
* (native hooks, process configuration or a request's system field), not the
|
|
4
|
+
* application or its message history.
|
|
5
|
+
*/
|
|
6
|
+
export function createConversationSystemPrompt() {
|
|
7
|
+
let installed;
|
|
8
|
+
let generation = 0;
|
|
9
|
+
let pending = Promise.resolve();
|
|
10
|
+
|
|
11
|
+
function invalidate() {
|
|
12
|
+
generation += 1;
|
|
13
|
+
installed = undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isCurrent({ systemPrompt, contextIdentity } = {}) {
|
|
17
|
+
return Boolean(installed && installed.systemPrompt === systemPrompt && installed.contextIdentity === contextIdentity);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensure({ systemPrompt, contextIdentity, install, retained = true } = {}) {
|
|
21
|
+
if (typeof systemPrompt !== "string" || !systemPrompt.trim()) {
|
|
22
|
+
return Promise.reject(new TypeError("Conversation systemPrompt must be a non-empty string."));
|
|
23
|
+
}
|
|
24
|
+
if (typeof contextIdentity !== "string" || !contextIdentity) {
|
|
25
|
+
return Promise.reject(new TypeError("Conversation contextIdentity is required."));
|
|
26
|
+
}
|
|
27
|
+
if (typeof install !== "function") {
|
|
28
|
+
return Promise.reject(new TypeError("Conversation system prompt requires an installation adapter."));
|
|
29
|
+
}
|
|
30
|
+
const operation = pending.catch(() => {}).then(async () => {
|
|
31
|
+
const promptChanged = !installed || installed.systemPrompt !== systemPrompt;
|
|
32
|
+
const contextChanged = !installed || installed.contextIdentity !== contextIdentity;
|
|
33
|
+
if (retained && isCurrent({ systemPrompt, contextIdentity })) return installed.value;
|
|
34
|
+
const startedGeneration = generation;
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = await install({ systemPrompt, promptChanged, contextChanged });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
invalidate();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
// Do not let a caller start inference against an obsolete installation.
|
|
43
|
+
if (generation !== startedGeneration) {
|
|
44
|
+
throw new Error("Conversation context changed while installing its system prompt. Retry before sending.");
|
|
45
|
+
}
|
|
46
|
+
installed = { systemPrompt, contextIdentity, value };
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
pending = operation;
|
|
50
|
+
return operation;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Object.freeze({ ensure, isCurrent, invalidate, whenIdle: () => pending });
|
|
54
|
+
}
|