@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,312 @@
|
|
|
1
|
+
import { normalizeText } from "../normalize.js";
|
|
2
|
+
import { createConversationChangeover } from "../continuity.js";
|
|
3
|
+
|
|
4
|
+
// The host retains profile authority and durable parent receipts. This original
|
|
5
|
+
// coordinator uses its admitted operations before and after native creation.
|
|
6
|
+
export async function runScopedConversationTurn({ operations, scope = {}, input = {}, context: options = {},
|
|
7
|
+
executionProfile: profileSnapshot } = {}) {
|
|
8
|
+
const request = { ...input, ephemeral: true, message: input.prompt || input.message };
|
|
9
|
+
options.signal?.throwIfAborted();
|
|
10
|
+
let conversationId = normalizeText(input.conversationId || input.threadId);
|
|
11
|
+
if (!conversationId) {
|
|
12
|
+
const created = await operations.createEphemeralConversation(scope, request, options);
|
|
13
|
+
if (created?.ok === false) return created;
|
|
14
|
+
conversationId = normalizeText(created.conversationId);
|
|
15
|
+
if (!conversationId) throw new Error("The helper did not return its conversation identity.");
|
|
16
|
+
}
|
|
17
|
+
// Await ownership before starting: Stop or a failed parent write must not
|
|
18
|
+
// leave an unidentified native turn running.
|
|
19
|
+
await options.onEvent?.({ type: "thread", threadId: conversationId });
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
const started = await operations.startEphemeralConversationTurn(scope, { ...request, conversationId }, options);
|
|
22
|
+
if (started?.ok === false) return { ...started, threadId: conversationId };
|
|
23
|
+
const runId = normalizeText(started.runId);
|
|
24
|
+
await options.onEvent?.({ type: "turn", threadId: conversationId, turnId: runId, status: started.status });
|
|
25
|
+
if (options.signal?.aborted) {
|
|
26
|
+
const stopped = await operations.stopEphemeralConversation(scope, { ...request, conversationId, runId }, options);
|
|
27
|
+
if (stopped?.ok === false) return { ...stopped, threadId: conversationId, turnId: runId };
|
|
28
|
+
options.signal.throwIfAborted();
|
|
29
|
+
}
|
|
30
|
+
let stop;
|
|
31
|
+
const interrupt = () => {
|
|
32
|
+
stop ||= operations.stopEphemeralConversation(scope, { ...request, conversationId, runId }, options);
|
|
33
|
+
void stop.catch(() => {});
|
|
34
|
+
};
|
|
35
|
+
options.signal?.addEventListener("abort", interrupt, { once: true });
|
|
36
|
+
let result;
|
|
37
|
+
let failure;
|
|
38
|
+
try {
|
|
39
|
+
result = await operations.waitForEphemeralConversationTurn(scope, { ...request, conversationId, runId }, options);
|
|
40
|
+
options.signal?.throwIfAborted();
|
|
41
|
+
} catch (error) { failure = error; }
|
|
42
|
+
finally {
|
|
43
|
+
options.signal?.removeEventListener("abort", interrupt);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const stopped = await stop;
|
|
47
|
+
if (stopped?.ok === false) throw Object.assign(new Error(stopped.error || "The helper could not confirm a stop."), { code: stopped.code });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (failure && error !== failure) error.cause = failure;
|
|
50
|
+
failure = error;
|
|
51
|
+
}
|
|
52
|
+
if (failure) throw failure;
|
|
53
|
+
return { ...result, threadId: conversationId, turnId: runId,
|
|
54
|
+
text: result.rawText || result.text || result.message || "", executionProfile: profileSnapshot,
|
|
55
|
+
...(["failed", "cancelled", "interrupted"].includes(result.status) ? { ok: false,
|
|
56
|
+
error: result.error || "The helper stopped before completing its answer." } : {}) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const messageWriters = {
|
|
60
|
+
assistant: "writeConversationAssistantMessage",
|
|
61
|
+
commentary: "writeConversationCommentaryMessage",
|
|
62
|
+
thinking: "writeConversationThinkingMessage",
|
|
63
|
+
system: "writeConversationSystemMessage"
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function requireSuccess(result) {
|
|
67
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Assistant conversation operation failed."), result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function qualifyTranscript(turns, record, response, receipts) {
|
|
72
|
+
return turns.map(turn => {
|
|
73
|
+
if (!turn.user || receipts.persisted(turn) ||
|
|
74
|
+
response.admitted === true && turn.user.messageId === record.messageId) return turn;
|
|
75
|
+
const user = { ...turn.user, receipt: false };
|
|
76
|
+
return { ...turn, user, messages: turn.messages.map(message => message.role === "user" ? { ...message, receipt: false } : message) };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function publishTranscript({ sessionId, store, receipts, publish }, context, record, turns, response = {}) {
|
|
81
|
+
const scope = { sessionId, conversationId: record.conversationId };
|
|
82
|
+
const identities = new Set(turns.flatMap(turn => turn.messages.flatMap(message => [message.messageId, message.outputId].filter(Boolean))));
|
|
83
|
+
for (const message of store.readConversationStream(scope).messages) {
|
|
84
|
+
if (identities.has(message.messageId) || identities.has(message.outputId)) store.completeConversationStreamMessage(scope, message.messageId);
|
|
85
|
+
}
|
|
86
|
+
for (const turn of qualifyTranscript(turns, record, response, receipts)) {
|
|
87
|
+
await publish(context, record.conversationId, { payload: {
|
|
88
|
+
conversationLogPatch: { type: "upsert-turn", turn }, conversationStream: store.readConversationStream(scope)
|
|
89
|
+
} });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function streamMessage({ sessionId, store, projection, publish }, context, record, event) {
|
|
94
|
+
const message = event.message;
|
|
95
|
+
if (!message || event.threadId !== record.providerConversationId || !event.turnId ||
|
|
96
|
+
!["assistant", "commentary"].includes(message.role || "assistant")) return;
|
|
97
|
+
const value = projection.message(message, record, "stream");
|
|
98
|
+
if (!value) return;
|
|
99
|
+
const scope = { sessionId, conversationId: record.conversationId };
|
|
100
|
+
const conversationStream = store.updateConversationStream(scope, {
|
|
101
|
+
...value, messageId: value.messageId || value.id,
|
|
102
|
+
turnId: `${event.threadId}:${event.turnId}`,
|
|
103
|
+
nativeIdentity: { threadId: event.threadId, turnId: event.turnId }
|
|
104
|
+
});
|
|
105
|
+
if (conversationStream) void publish(context, record.conversationId, {
|
|
106
|
+
reason: "assistant-stream", payload: { conversationStream }
|
|
107
|
+
}).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// These original persistent-conversation operations run inside the host's
|
|
111
|
+
// existing write lease. Their record, transcript and native operations are the
|
|
112
|
+
// same facilities used by its retained conversation; no state is copied here.
|
|
113
|
+
export async function readPersistentConversation(options) {
|
|
114
|
+
const { sessionId, operations, store, records, projection, receipts, publish,
|
|
115
|
+
includeAccess = false, transcriptQuery } = options;
|
|
116
|
+
let { record } = options;
|
|
117
|
+
const context = projection.context(options.context, record);
|
|
118
|
+
const scope = { sessionId, conversationId: record.conversationId };
|
|
119
|
+
let response = {};
|
|
120
|
+
let turns = await store.readConversationLog(scope);
|
|
121
|
+
const requestTurn = turns.findLast((turn) => turn.user?.messageId === record.messageId);
|
|
122
|
+
const messageScope = projection.messageScope(context, scope, requestTurn);
|
|
123
|
+
const savedIds = new Set(turns.flatMap((turn) => turn.messages.map((message) => message.messageId)));
|
|
124
|
+
const writtenTurnIds = new Set();
|
|
125
|
+
let appended = false;
|
|
126
|
+
if (record.providerConversationId && record.state !== "closing") {
|
|
127
|
+
try {
|
|
128
|
+
response = requireSuccess(await operations.readConversation(sessionId, records.input(record), context));
|
|
129
|
+
for (const message of response.messages || []) {
|
|
130
|
+
const operation = messageWriters[message.role];
|
|
131
|
+
if (operation && message.text && message.complete !== false && !savedIds.has(message.id)) {
|
|
132
|
+
const written = await store[operation](messageScope, {
|
|
133
|
+
...projection.message(message, record, "stored"), messageId: message.id
|
|
134
|
+
});
|
|
135
|
+
if (written) writtenTurnIds.add(written.turnId);
|
|
136
|
+
appended = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const patch = {
|
|
140
|
+
status: response.status || record.status,
|
|
141
|
+
runId: response.runId || record.runId,
|
|
142
|
+
error: response.error || ""
|
|
143
|
+
};
|
|
144
|
+
if (record.status === "starting" && response.admitted) Object.assign(patch, { draft: "", attachments: [] });
|
|
145
|
+
if (Object.entries(patch).some(([key, value]) => record[key] !== value)) record = await records.save(context, record, patch);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// A failed read cannot establish that work stopped. Keep Stop available.
|
|
148
|
+
response = { error: error.message, readError: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (appended) turns = await store.readConversationLog(scope);
|
|
152
|
+
if (writtenTurnIds.size) await publishTranscript(options, context, record, turns.filter(turn => writtenTurnIds.has(turn.turnId)), response);
|
|
153
|
+
if (!response.readError && response.status && !["starting", "inProgress"].includes(response.status) &&
|
|
154
|
+
store.readConversationStream(scope).messages.length) {
|
|
155
|
+
await publish(context, record.conversationId, { payload: { conversationStream: store.clearConversationStream(scope) } });
|
|
156
|
+
}
|
|
157
|
+
const messages = turns.flatMap((turn) => turn.messages.map((message) => ({
|
|
158
|
+
...message,
|
|
159
|
+
id: message.messageId || `${turn.turnId}:${message.role}:${message.at}`,
|
|
160
|
+
...(message.role === "user" ? { attachments: turn.user?.attachments || [] } : {}),
|
|
161
|
+
...(message.role === "user" && !receipts.persisted(turn) &&
|
|
162
|
+
!(response.admitted === true && message.messageId === record.messageId) ? { receipt: false } : {}),
|
|
163
|
+
status: "completed",
|
|
164
|
+
...projection.messageMetadata(turn)
|
|
165
|
+
})));
|
|
166
|
+
for (const message of response.messages || []) {
|
|
167
|
+
if (message.complete === false && !messages.some((saved) => saved.id === message.id)) {
|
|
168
|
+
const projected = projection.message(message, record, "incomplete");
|
|
169
|
+
if (projected) messages.push({ ...projected, status: response.status });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return projection.snapshot({ context, record, response, messages, includeAccess,
|
|
173
|
+
readPage: transcriptQuery ? async () => {
|
|
174
|
+
const page = await store.readConversationLogPage(scope, transcriptQuery);
|
|
175
|
+
return { ...page, conversationLog: qualifyTranscript(page.conversationLog, record, response, receipts) };
|
|
176
|
+
} : null
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function startPersistentConversationTurn(options) {
|
|
181
|
+
const { sessionId, input, operations, store, records, prepare, receipts } = options;
|
|
182
|
+
let { record, context } = options;
|
|
183
|
+
const previous = await readPersistentConversation({ ...options, record: { ...record, messageId: input.messageId } });
|
|
184
|
+
if (previous.readError) throw new Error(previous.error);
|
|
185
|
+
if (previous.admitted) return { ...previous, delivered: true, turnId: previous.runId };
|
|
186
|
+
const steering = ["starting", "inProgress"].includes(previous.status);
|
|
187
|
+
const selected = await prepare.selection(sessionId, input, record, steering, context);
|
|
188
|
+
const { settings, assistantSelection } = selected;
|
|
189
|
+
context = selected.context;
|
|
190
|
+
if (!record.providerConversationId) {
|
|
191
|
+
const created = requireSuccess(await operations.createConversation(sessionId, selected.creation, context));
|
|
192
|
+
record = await records.bind(context, record, { providerConversationId: created.conversationId }, {
|
|
193
|
+
...record, providerConversationId: created.conversationId, assistantSelection, agentSettings: settings
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const prepared = await prepare.message(sessionId, input, record, context, assistantSelection);
|
|
197
|
+
let authoredTurn;
|
|
198
|
+
const writeReceipt = async () => {
|
|
199
|
+
authoredTurn = await store.writeConversationUserMessage({ sessionId, conversationId: record.conversationId }, prepared.receipt);
|
|
200
|
+
return authoredTurn;
|
|
201
|
+
};
|
|
202
|
+
if (!receipts.afterAdmission(input)) await writeReceipt();
|
|
203
|
+
record = await records.save(context, record, {
|
|
204
|
+
...prepared.presentation,
|
|
205
|
+
agentSettings: settings,
|
|
206
|
+
assistantSelection,
|
|
207
|
+
messageId: input.messageId,
|
|
208
|
+
status: steering ? previous.status : "starting",
|
|
209
|
+
runId: steering ? previous.runId : "",
|
|
210
|
+
error: ""
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
const result = requireSuccess(await operations.startConversationTurn(sessionId,
|
|
214
|
+
records.input(record, { ...input, ...prepared.input, steer: steering }), { ...context, attachmentsPrepared: true,
|
|
215
|
+
onEvent(event) { streamMessage(options, context, record, event); return context.onEvent?.(event); }
|
|
216
|
+
}));
|
|
217
|
+
if (receipts.afterAdmission(input)) await writeReceipt();
|
|
218
|
+
await records.bind(context, record, { runId: result.runId, status: result.status || "inProgress", draft: "", attachments: [] },
|
|
219
|
+
{ ...record, runId: result.runId });
|
|
220
|
+
if (authoredTurn) await publishTranscript(options, context, record, [authoredTurn], { admitted: true });
|
|
221
|
+
return { ...result, delivered: true, turnId: result.runId, conversationId: record.conversationId };
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const observed = await readPersistentConversation({ ...options, context, record });
|
|
224
|
+
if (observed.admitted) {
|
|
225
|
+
if (receipts.afterAdmission(input)) await writeReceipt();
|
|
226
|
+
return { ...observed, delivered: true, turnId: observed.runId };
|
|
227
|
+
}
|
|
228
|
+
if (observed.readError && !receipts.afterAdmission(input)) return {
|
|
229
|
+
...observed, ok: false, delivered: false, status: "uncertain", messageId: input.messageId, error: error.message
|
|
230
|
+
};
|
|
231
|
+
await records.save(context, record, { error: error.message, status: "failed" });
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function inspectPersistentConversationAdmission(options) {
|
|
237
|
+
const { sessionId, record, input, operations, inputFor } = options;
|
|
238
|
+
if (input.threadId && input.threadId !== record.providerConversationId) return { admission: "unknown", turnId: "" };
|
|
239
|
+
if (!record.providerConversationId) return { admission: "unknown", turnId: "" };
|
|
240
|
+
const state = requireSuccess(await operations.readConversation(sessionId,
|
|
241
|
+
inputFor(record, { messageId: input.messageId }), options.context));
|
|
242
|
+
return { admission: state.admitted ? "accepted" : "unknown", turnId: state.runId };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function preparePersistentConversationChangeover(options) {
|
|
246
|
+
const { sessionId, operations, records, prepare, context, changeoverContext } = options;
|
|
247
|
+
let { record } = options;
|
|
248
|
+
const observed = await readPersistentConversation(options);
|
|
249
|
+
if (observed.readError) throw new Error(observed.error);
|
|
250
|
+
const current = await records.read(context, record.conversationId);
|
|
251
|
+
const native = current.providerConversationId ? requireSuccess(await operations.readConversation(sessionId,
|
|
252
|
+
records.input(current), context)) : { status: "ready" };
|
|
253
|
+
if (["starting", "inProgress"].includes(native.status)) throw new Error("Stop this temporary conversation before changing its AI.");
|
|
254
|
+
record = await records.read(context, record.conversationId);
|
|
255
|
+
if (record.providerConversationId) {
|
|
256
|
+
requireSuccess(await operations.stopConversation(sessionId, records.input(record, { runId: record.runId }), context));
|
|
257
|
+
record = await records.bind(changeoverContext, record, {}, record);
|
|
258
|
+
}
|
|
259
|
+
const { engineId, messages, ...continuity } = await prepare.continuity(sessionId, changeoverContext, { remember: true });
|
|
260
|
+
await createConversationChangeover(continuity).remember({ engineId, messages });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function inspectPersistentConversationDelivery(options) {
|
|
264
|
+
const { sessionId, input, context, store, operations, prepare, receipts } = options;
|
|
265
|
+
let turns = await store.readConversationLog(sessionId);
|
|
266
|
+
let row = turns.find(turn => turn.user?.messageId === input.messageId);
|
|
267
|
+
let accepted = options.delivered === true;
|
|
268
|
+
const pending = receipts.pending(context);
|
|
269
|
+
if (pending?.messageId === input.messageId && pending.attempted) {
|
|
270
|
+
const inspectMessageAdmission = operations.inspectMessageAdmission;
|
|
271
|
+
const { engineId, messages, turnMetadata, ...continuity } = await prepare.continuity(sessionId, context);
|
|
272
|
+
const result = await createConversationChangeover({ ...continuity, agent: {
|
|
273
|
+
inspectMessageAdmission: request => inspectMessageAdmission(sessionId, request, context),
|
|
274
|
+
sendMessage() { throw new Error("Receipt inspection cannot submit another native prompt."); }
|
|
275
|
+
} }).send({ engineId, messages, input: { messageId: input.messageId }, turnMetadata });
|
|
276
|
+
accepted = result?.delivered === true;
|
|
277
|
+
} else if (!accepted) {
|
|
278
|
+
if (row && row.user?.receipt !== false) return { status: "accepted", messageId: input.messageId, turnId: row.turnId, duplicate: true };
|
|
279
|
+
const receipt = await operations.inspectMessageAdmission(sessionId, { messageId: input.messageId }, context).catch(() => null);
|
|
280
|
+
accepted = receipt?.admission === "accepted";
|
|
281
|
+
}
|
|
282
|
+
if (!accepted) return { status: "unknown", messageId: input.messageId };
|
|
283
|
+
turns = await store.readConversationLog(sessionId);
|
|
284
|
+
row = turns.find(turn => turn.user?.messageId === input.messageId);
|
|
285
|
+
return { status: "accepted", messageId: input.messageId, ...(row ? { turnId: row.turnId } : {}), duplicate: true };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function stopPersistentConversation({ sessionId, record, context, operations, records, projection }) {
|
|
289
|
+
if (record.providerConversationId) requireSuccess(await operations.stopConversation(sessionId,
|
|
290
|
+
records.input(record, { runId: record.runId }), projection.context(context, record)));
|
|
291
|
+
await records.save(context, record, { status: "interrupted", error: "" });
|
|
292
|
+
return { ok: true, status: "interrupted", conversationId: record.conversationId };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function deletePersistentConversation({ sessionId, record, context, operations, records, projection }) {
|
|
296
|
+
record = await records.save(context, record, { state: "closing", error: "" });
|
|
297
|
+
context = projection.context(context, record);
|
|
298
|
+
try {
|
|
299
|
+
for (const [key, binding] of records.retained(record)) {
|
|
300
|
+
const retained = records.select(record, binding);
|
|
301
|
+
const selected = projection.context(context, retained);
|
|
302
|
+
requireSuccess(await operations.stopConversation(sessionId, records.input(retained, { runId: retained.runId }), selected));
|
|
303
|
+
requireSuccess(await operations.deleteConversation(sessionId, records.input(retained), selected));
|
|
304
|
+
record = await records.release(context, record, key, binding);
|
|
305
|
+
}
|
|
306
|
+
await records.delete(context, record, sessionId);
|
|
307
|
+
return { ok: true, deleted: true, conversationId: record.conversationId };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
await records.save(context, record, { error: error.message });
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|