@jskit-ai/assistant-core 0.1.205 → 0.1.206
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/fixtures/responsive-assistant/App.vue +12 -2
- package/package.json +19 -9
- package/src/client/conversation/AssistantTranscript.vue +177 -16
- package/src/server/conversation/claudeTurn.js +16 -7
- package/src/server/conversation/codexConversations.js +94 -0
- package/src/server/conversation/codexProvider.js +15 -5
- package/src/server/conversation/codexProviderOwner.js +70 -47
- package/src/server/conversation/codexProviderSelection.js +5 -2
- package/src/server/conversation/codexRuntimeLifecycle.js +2 -1
- package/src/server/conversation/codexTurn.js +10 -1
- package/src/server/conversation/codexTurnControl.js +1 -1
- package/src/server/conversation/codexTurnRecovery.js +21 -8
- package/src/server/conversation/openCodeRuntime.js +107 -64
- package/src/server/conversation/openCodeTurn.js +33 -16
- package/src/server/conversation/providers/api.js +1 -0
- package/src/server/conversation/providers/claudeDriver.js +13 -0
- package/src/server/conversation/providers/codex.js +90 -47
- package/src/server/conversation/providers/codexDriver.js +4 -1
- package/src/server/conversation/providers/opencodeDriver.js +3 -0
- package/src/server/conversation/runtime.js +32 -5
- package/src/server/hosts/codexEvents.js +15 -0
- package/src/server/hosts/codexTurn.js +5 -0
- package/src/server/hosts/openCodeClient.js +4 -0
- package/src/server/lib/aiConnectionClient.js +9 -11
- package/test/aiConnectionClient.test.js +209 -0
- package/test/assistantScroll.browser.test.js +255 -1
- package/test/claudeConversationSession.test.js +78 -0
- package/test/claudeTurn.test.js +18 -0
- package/test/codexConversationSession.test.js +318 -0
- package/test/codexTurn.test.js +203 -2
- package/test/conversationConfiguration.test.js +27 -0
- package/test/conversationReplacement.test.js +76 -1
- package/test/conversationRuntime.test.js +173 -4
- package/test/conversationSession.test.js +139 -0
- package/test/conversationTools.test.js +17 -3
- package/test/opencodeConversationSession.test.js +200 -1
- package/test/packageMetadata.test.js +75 -0
- package/test/serviceToolCatalog.test.js +31 -0
|
@@ -340,7 +340,7 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
|
|
|
340
340
|
if (result?.complete && (await client.sessionStatus(conversationId, { signal })).type === "idle" &&
|
|
341
341
|
expectedInputMessageId === text(resolveInputMessageId())) {
|
|
342
342
|
if (completedInputMessageId === expectedInputMessageId) {
|
|
343
|
-
return { messages, result };
|
|
343
|
+
return { messages, result, inputMessageId: expectedInputMessageId };
|
|
344
344
|
}
|
|
345
345
|
completedInputMessageId = expectedInputMessageId;
|
|
346
346
|
} else {
|
|
@@ -353,18 +353,34 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
|
|
|
353
353
|
export async function waitForOpenCodeFinalResponse(client, conversationId, turn, {
|
|
354
354
|
agent, model, recoveryMessageId, beforeRecovery = null, ...options
|
|
355
355
|
} = {}) {
|
|
356
|
-
const waitForCompletion = () =>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
356
|
+
const waitForCompletion = async () => {
|
|
357
|
+
for (;;) {
|
|
358
|
+
const completion = await waitForOpenCodeMessages(
|
|
359
|
+
client, conversationId, () => turn.inputMessageId, options
|
|
360
|
+
);
|
|
361
|
+
// Keep native polling/progress live while an admitted input's local
|
|
362
|
+
// commit is pending, but do not accept its terminal result yet.
|
|
363
|
+
const admission = turn.admission;
|
|
364
|
+
await admission?.promise;
|
|
365
|
+
options.signal?.throwIfAborted();
|
|
366
|
+
if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
return completion;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
for (;;) {
|
|
373
|
+
const completion = await waitForCompletion();
|
|
374
|
+
// Work around https://github.com/anomalyco/opencode/issues/37073. Some
|
|
375
|
+
// reasoning models complete successfully without a user-facing text part.
|
|
376
|
+
if (turn.interruptRequested || completion.result?.error || text(completion.result?.text)) return completion;
|
|
367
377
|
await beforeRecovery?.(completion);
|
|
378
|
+
const admission = turn.admission;
|
|
379
|
+
await admission?.promise;
|
|
380
|
+
options.signal?.throwIfAborted();
|
|
381
|
+
if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
368
384
|
const admitted = await client.prompt(conversationId, {
|
|
369
385
|
agent,
|
|
370
386
|
delivery: "queue",
|
|
@@ -375,11 +391,12 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
|
|
|
375
391
|
},
|
|
376
392
|
resume: true
|
|
377
393
|
}, { signal: options.signal });
|
|
378
|
-
turn.inputMessageId
|
|
379
|
-
|
|
380
|
-
|
|
394
|
+
if (admission === turn.admission && completion.inputMessageId === text(turn.inputMessageId)) {
|
|
395
|
+
turn.inputMessageId = text(admitted?.id) || recoveryMessageId;
|
|
396
|
+
turn.updatedAt = new Date().toISOString();
|
|
397
|
+
}
|
|
398
|
+
return waitForCompletion();
|
|
381
399
|
}
|
|
382
|
-
return completion;
|
|
383
400
|
}
|
|
384
401
|
|
|
385
402
|
// Production detached-turn lifetime. Applications project the completed native
|
|
@@ -69,6 +69,7 @@ export function createApiConversationDriver({ connections, apiClientFactory, fet
|
|
|
69
69
|
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
70
70
|
validateConnectionModel(configuration, connection);
|
|
71
71
|
client = createAiConnectionClient(connection, { fetch, effort: configuration.effort,
|
|
72
|
+
reportUnavailableToolCalls: Boolean(tools),
|
|
72
73
|
maxOutputTokens: limits.maxOutputTokens, timeoutMs: limits.timeoutMs });
|
|
73
74
|
}
|
|
74
75
|
signal.throwIfAborted();
|
|
@@ -271,6 +271,19 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
271
271
|
async createConversation({ native, input, context }) {
|
|
272
272
|
return native.owner.createConversation(input, { context, acquire: native.owner.acquire });
|
|
273
273
|
},
|
|
274
|
+
async runDetachedConversation({ native, input, context }) {
|
|
275
|
+
const conversationId = input.conversationId || input.threadId || randomUUID();
|
|
276
|
+
const created = !input.conversationId && !input.threadId;
|
|
277
|
+
const { owner } = native;
|
|
278
|
+
const entry = await owner.acquire(context, conversationId, { create: created });
|
|
279
|
+
const executionProfile = native.executionProfile;
|
|
280
|
+
if (created && context.assistantScope) entry.profile = executionProfile;
|
|
281
|
+
if (executionProfile) await context.onEvent?.({ type: "execution-profile", executionProfile });
|
|
282
|
+
const request = { ...input, conversationId };
|
|
283
|
+
await owner.startTurn(await owner.acquire(context, conversationId, { operation: "start", input: request }), request);
|
|
284
|
+
const result = await owner.wait(await owner.acquire(context, conversationId), request, { context, acquire: owner.acquire });
|
|
285
|
+
return executionProfile ? { ...result, executionProfile } : result;
|
|
286
|
+
},
|
|
274
287
|
async createBinding() {
|
|
275
288
|
return { conversationId: randomUUID(), workdir: await realpath(workdir), configRoot, executionId: "", sent: false };
|
|
276
289
|
},
|
|
@@ -41,8 +41,13 @@ export function createCodexConversationAdapter(host) {
|
|
|
41
41
|
const previous = tasks.get(threadId) || Promise.resolve();
|
|
42
42
|
const task = previous.catch(() => null).then(() => host.runRequest(async () => {
|
|
43
43
|
const client = await host.client();
|
|
44
|
-
params = await prepareInstructions(params, threadId);
|
|
45
44
|
const generation = contextGeneration;
|
|
45
|
+
const bound = bindings.get(threadId);
|
|
46
|
+
const requestedParams = params;
|
|
47
|
+
const retainInstalledInstructions = bound?.params.developerInstructions != null && !params.ephemeral && !bound.params.ephemeral;
|
|
48
|
+
params = await prepareInstructions(retainInstalledInstructions
|
|
49
|
+
? { ...params, systemPrompt: undefined, developerInstructions: bound.params.developerInstructions }
|
|
50
|
+
: params, threadId);
|
|
46
51
|
const signal = AbortSignal.timeout(host.timeoutMs || 10_000);
|
|
47
52
|
const assertRecoveryOpen = () => {
|
|
48
53
|
signal.throwIfAborted();
|
|
@@ -54,7 +59,6 @@ export function createCodexConversationAdapter(host) {
|
|
|
54
59
|
assertRecoveryOpen();
|
|
55
60
|
return client.request(method, input, { signal });
|
|
56
61
|
};
|
|
57
|
-
const bound = bindings.get(threadId);
|
|
58
62
|
const fixedPolicy = params.config?.shell_environment_policy ||
|
|
59
63
|
(bound?.managed === false ? bound.params.config.shell_environment_policy : null);
|
|
60
64
|
if (fixedPolicy && (params.developerInstructions == null || params.ephemeral || bound?.params.ephemeral)) {
|
|
@@ -63,7 +67,7 @@ export function createCodexConversationAdapter(host) {
|
|
|
63
67
|
}
|
|
64
68
|
return operation(threadParameters({ ...bound?.params, ...params }, {}));
|
|
65
69
|
}
|
|
66
|
-
|
|
70
|
+
let environment = instructionEnvironment(params, fixedPolicy?.set || await host.prepareEnvironment(
|
|
67
71
|
bound?.params?.config?.shell_environment_policy?.set || host.environment || {}
|
|
68
72
|
));
|
|
69
73
|
const previousConfig = { ...bound?.params?.config };
|
|
@@ -94,7 +98,7 @@ export function createCodexConversationAdapter(host) {
|
|
|
94
98
|
nativeHistoryPath = thread?.path || "";
|
|
95
99
|
return typeof thread?.status === "string" ? thread.status : thread?.status?.type;
|
|
96
100
|
};
|
|
97
|
-
|
|
101
|
+
let nativeStatus = await readStatus();
|
|
98
102
|
if (!bound && !requestParams.modelProvider && nativeModelProvider) {
|
|
99
103
|
// A restored observer has no saved request parameters. Recover the
|
|
100
104
|
// native thread's provider configuration before resuming its controls.
|
|
@@ -122,14 +126,29 @@ export function createCodexConversationAdapter(host) {
|
|
|
122
126
|
processChanged: Boolean(bound?.executionId)
|
|
123
127
|
}) || requestParams;
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
let bindingMatches = !providerChanged && bound?.client === client &&
|
|
126
130
|
bound.params.developerInstructions === requestParams.developerInstructions &&
|
|
127
131
|
bound.params.config.openai_base_url === requestParams.config.openai_base_url &&
|
|
128
132
|
JSON.stringify(bound.params.config.model_providers) === JSON.stringify(requestParams.config.model_providers) &&
|
|
129
133
|
Object.keys(requestParams.config).filter((key) => key.startsWith("model_providers.")).every((key) =>
|
|
130
134
|
JSON.stringify(bound.params.config[key]) === JSON.stringify(requestParams.config[key])) &&
|
|
131
135
|
JSON.stringify(bound.params.config.shell_environment_policy) === JSON.stringify(requestParams.config.shell_environment_policy);
|
|
132
|
-
const
|
|
136
|
+
const controlBindingMatches = bindingMatches;
|
|
137
|
+
const installedRequestParams = requestParams;
|
|
138
|
+
if (retainInstalledInstructions && (!bindingMatches || nativeStatus !== "active")) {
|
|
139
|
+
// An active healthy turn keeps its installed prompt. Real control
|
|
140
|
+
// recovery and the next idle admission still read current instructions.
|
|
141
|
+
params = await prepareInstructions(requestedParams, threadId);
|
|
142
|
+
environment = instructionEnvironment(params, environment);
|
|
143
|
+
requestParams = { ...requestParams,
|
|
144
|
+
...(Object.hasOwn(params, "developerInstructions") ? { developerInstructions: params.developerInstructions } : {}),
|
|
145
|
+
config: { ...requestParams.config, shell_environment_policy: { ...requestParams.config.shell_environment_policy, set: environment } }
|
|
146
|
+
};
|
|
147
|
+
bindingMatches = bindingMatches && bound.params.developerInstructions === requestParams.developerInstructions &&
|
|
148
|
+
JSON.stringify(bound.params.config.shell_environment_policy) === JSON.stringify(requestParams.config.shell_environment_policy);
|
|
149
|
+
}
|
|
150
|
+
const instructionOnly = retainInstalledInstructions && controlBindingMatches && !bindingMatches;
|
|
151
|
+
let instructionsChanged = requestParams.developerInstructions != null &&
|
|
133
152
|
instructionPrompts.get(threadId) !== requestParams.developerInstructions;
|
|
134
153
|
let pausedGoal = null;
|
|
135
154
|
let resumed = null;
|
|
@@ -138,6 +157,16 @@ export function createCodexConversationAdapter(host) {
|
|
|
138
157
|
goal.createdAt === original.createdAt && goal.objective === original.objective;
|
|
139
158
|
const log = (event, fields = {}) => host.log?.(event, fields, { threadId, environment });
|
|
140
159
|
try {
|
|
160
|
+
if (instructionOnly) {
|
|
161
|
+
// A native goal can start work while the host reads its prompt.
|
|
162
|
+
nativeStatus = await readStatus();
|
|
163
|
+
if (nativeStatus === "active") {
|
|
164
|
+
requestParams = installedRequestParams;
|
|
165
|
+
environment = requestParams.config.shell_environment_policy.set;
|
|
166
|
+
bindingMatches = true;
|
|
167
|
+
instructionsChanged = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
141
170
|
if (!bindingMatches || nativeStatus === "notLoaded") {
|
|
142
171
|
log("reload_started", { reason: bound ? "control-environment-changed" : "unverified-thread-binding", nativeStatus });
|
|
143
172
|
if (nativeStatus !== "notLoaded") {
|
|
@@ -150,57 +179,71 @@ export function createCodexConversationAdapter(host) {
|
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
if (await readStatus() !== "idle") {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
if (instructionOnly) {
|
|
183
|
+
// Never interrupt work solely to refresh its instructions.
|
|
184
|
+
requestParams = installedRequestParams;
|
|
185
|
+
environment = requestParams.config.shell_environment_policy.set;
|
|
186
|
+
bindingMatches = true;
|
|
187
|
+
instructionsChanged = false;
|
|
188
|
+
} else {
|
|
189
|
+
const page = await request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "summary" });
|
|
190
|
+
const turnId = page.data?.[0]?.id;
|
|
191
|
+
if (!turnId) throw new Error("Codex's running turn could not be identified for control recovery.");
|
|
192
|
+
assertRecoveryOpen();
|
|
193
|
+
host.recordInterruption?.(threadId, turnId);
|
|
194
|
+
await request("turn/interrupt", { threadId, turnId });
|
|
195
|
+
while (await readStatus() !== "idle") {
|
|
196
|
+
signal.throwIfAborted();
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
198
|
+
}
|
|
160
199
|
}
|
|
161
200
|
}
|
|
162
201
|
if (pausedGoal) {
|
|
163
202
|
const { goal } = await request("thread/goal/get", { threadId });
|
|
164
203
|
pausedGoal = isSamePausedGoal(goal, pausedGoal) ? goal : null;
|
|
165
204
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
if (!bindingMatches) {
|
|
206
|
+
const detached = await request("thread/unsubscribe", { threadId });
|
|
207
|
+
if (!["unsubscribed", "notSubscribed", "notLoaded"].includes(detached?.status)) {
|
|
208
|
+
throw new Error("Codex did not confirm detachment before control recovery.");
|
|
209
|
+
}
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
if (!bindingMatches || nativeStatus === "notLoaded") {
|
|
213
|
+
bindings.delete(threadId);
|
|
214
|
+
await host.beforeResume?.(threadId);
|
|
215
|
+
resumed = await request("thread/resume", { excludeTurns: true, ...requestParams, threadId });
|
|
216
|
+
if (requestParams.modelProvider && resumed.modelProvider !== requestParams.modelProvider) {
|
|
217
|
+
throw Object.assign(new Error("Another native subscriber retained the previous model provider. Close the native assistant terminal and retry."), {
|
|
218
|
+
code: "assistant_codex_provider_binding_retained"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!["idle", "active"].includes(await readStatus())) throw new Error("Codex did not confirm the thread loaded.");
|
|
222
|
+
if (nativeStatus !== "notLoaded") {
|
|
223
|
+
await host.verifyEnvironment(threadId, environment, request, signal, {
|
|
224
|
+
requiredKeys: requestParams.developerInstructions == null ? [] : [INSTRUCTION_REVISION_ENV]
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const checked = fixedPolicy ? environment : instructionEnvironment(params, await host.prepareEnvironment(environment));
|
|
228
|
+
if (JSON.stringify(checked) !== JSON.stringify(environment)) {
|
|
229
|
+
throw new Error("Managed controls changed again during thread recovery.");
|
|
230
|
+
}
|
|
231
|
+
if (instructionsChanged) {
|
|
232
|
+
// Resume config supplies the next compacted context, but existing
|
|
233
|
+
// history retains its original developer message. A revision in
|
|
234
|
+
// the developer lane makes this change effective before new work.
|
|
235
|
+
// An unknown binding also needs restoration; ordinary turns and
|
|
236
|
+
// socket reconnects with an acknowledged binding do not repeat it.
|
|
237
|
+
await request("thread/inject_items", { threadId, items: [{ type: "message", role: "developer", content: [{
|
|
238
|
+
type: "input_text", text: "These are the current application instructions. They replace earlier application instructions; other instructions remain in effect.\n\n" + requestParams.developerInstructions
|
|
239
|
+
}] }] });
|
|
240
|
+
assertRecoveryOpen();
|
|
241
|
+
instructionPrompts.set(threadId, requestParams.developerInstructions);
|
|
242
|
+
}
|
|
198
243
|
assertRecoveryOpen();
|
|
199
|
-
|
|
244
|
+
bindings.set(threadId, { client, executionId, params: requestParams, managed: !fixedPolicy });
|
|
245
|
+
log("ready");
|
|
200
246
|
}
|
|
201
|
-
assertRecoveryOpen();
|
|
202
|
-
bindings.set(threadId, { client, executionId, params: requestParams, managed: !fixedPolicy });
|
|
203
|
-
log("ready");
|
|
204
247
|
}
|
|
205
248
|
} catch (cause) {
|
|
206
249
|
host.onRecoveryFailure?.(threadId);
|
|
@@ -214,6 +214,9 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
214
214
|
async createConversation({ sessionId, native, input, context }) {
|
|
215
215
|
return native.runOwner.createConversation(sessionId, input, context);
|
|
216
216
|
},
|
|
217
|
+
async runDetachedConversation({ sessionId, native, input, options }) {
|
|
218
|
+
return native.runOwner.runDetachedConversation(sessionId, input, options);
|
|
219
|
+
},
|
|
217
220
|
async createBinding() {
|
|
218
221
|
return { threadId: "", workdir: await realpath(workdir), configRoot, executionId: "" };
|
|
219
222
|
},
|
|
@@ -452,7 +455,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
452
455
|
},
|
|
453
456
|
complete: completeObservation
|
|
454
457
|
}
|
|
455
|
-
}));
|
|
458
|
+
}), providerKey, providerOptions);
|
|
456
459
|
}
|
|
457
460
|
|
|
458
461
|
async function stop() {
|
|
@@ -109,6 +109,9 @@ export function createOpenCodeConversationDriver({ connections, host = {}, limit
|
|
|
109
109
|
async createConversation({ native, input, context }) {
|
|
110
110
|
return native.owner.createPreparedConversation(await native.preparation.creation(input, context));
|
|
111
111
|
},
|
|
112
|
+
async runDetachedConversation({ native, input, options }) {
|
|
113
|
+
return native.owner.runPreparedConversationTurn(await native.preparation.turn(input, options));
|
|
114
|
+
},
|
|
112
115
|
async createBinding() {
|
|
113
116
|
const directory = path.join(stateDirectory, randomUUID());
|
|
114
117
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -1497,11 +1497,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1497
1497
|
return entry.conversation.commands.select(input, context);
|
|
1498
1498
|
}
|
|
1499
1499
|
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original selected profile.", "conversation_unsupported");
|
|
1500
|
-
const keys = selection ? ["operationId", "expectedSegmentId", "engine", "configuration"]
|
|
1500
|
+
const keys = selection ? ["operationId", "expectedSegmentId", "engine", "configuration", "retireNative"]
|
|
1501
1501
|
: ["operationId", "expectedSegmentId", "reason", "engine", "configuration", "briefing"];
|
|
1502
1502
|
if (!input || !/^[\w-]{1,128}$/u.test(input.operationId || "") || typeof input.expectedSegmentId !== "string" ||
|
|
1503
1503
|
!input.expectedSegmentId || (selection ? typeof input.engine !== "string" : !["engine-change", "model-change", "renewal"].includes(input.reason)) ||
|
|
1504
1504
|
input.briefing !== undefined && typeof input.briefing !== "string" ||
|
|
1505
|
+
input.retireNative !== undefined && typeof input.retireNative !== "boolean" ||
|
|
1505
1506
|
Object.keys(input).some(key => !keys.includes(key))) {
|
|
1506
1507
|
throw failure("Replacement needs an operationId, exact predecessor segment, and an engine-change, model-change or renewal reason.", "conversation_invalid_replacement", 400);
|
|
1507
1508
|
}
|
|
@@ -1569,7 +1570,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1569
1570
|
const predecessorId = nativeIdentity(state);
|
|
1570
1571
|
const nativeReplacement = !selection && state.engine !== "api" && predecessorId &&
|
|
1571
1572
|
(state.engine !== "claude" || state.binding.sent);
|
|
1572
|
-
|
|
1573
|
+
const retainNativeBinding = input.retireNative !== true && engine === state.engine &&
|
|
1574
|
+
(engine !== "opencode" || configuration.integrationId === state.configuration.integrationId);
|
|
1575
|
+
if (selection && engine !== "api" && retainNativeBinding && !pending) {
|
|
1573
1576
|
// The production native owners apply compatible settings to the running
|
|
1574
1577
|
// conversation themselves. Selecting a model is not process retirement.
|
|
1575
1578
|
await storage.write(entry.id, async transaction => {
|
|
@@ -1587,8 +1590,14 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1587
1590
|
}
|
|
1588
1591
|
if (!pending) {
|
|
1589
1592
|
const history = (await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
|
|
1590
|
-
|
|
1591
|
-
|
|
1593
|
+
let destination = null;
|
|
1594
|
+
if (selection) {
|
|
1595
|
+
if (retainNativeBinding) destination = state;
|
|
1596
|
+
else if (input.retireNative !== true && engine !== state.engine && driver.createBinding) {
|
|
1597
|
+
destination = retainedSegments(state).findLast(segment => segment.engine === engine &&
|
|
1598
|
+
(engine !== "opencode" || segment.configuration.integrationId === configuration.integrationId));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1592
1601
|
if (engine !== "api" && (input.briefing || "").length > maximumContinuity) throw new Error("The continuity briefing exceeds the configured history budget.");
|
|
1593
1602
|
const continuity = destination ? { text: destination.continuity, attachments: destination.continuityAttachments || [] }
|
|
1594
1603
|
: engine === "api" ? conversationContinuity({ history, briefing: input.briefing, maximumCharacters: maximumContinuity })
|
|
@@ -1660,7 +1669,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1660
1669
|
if (value) {
|
|
1661
1670
|
if (engine !== "api") state.seen = value.engines[engine].seen;
|
|
1662
1671
|
state.lastEngine = value.lastEngine;
|
|
1663
|
-
} else if (
|
|
1672
|
+
} else if (engine !== "api" &&
|
|
1673
|
+
(!selection || input.retireNative === true || engine === "opencode" && !pending.binding.sessionId)) {
|
|
1674
|
+
state.lastEngine = "";
|
|
1675
|
+
}
|
|
1664
1676
|
delete state.replacement;
|
|
1665
1677
|
await transaction.writeMetadata(metadata);
|
|
1666
1678
|
}
|
|
@@ -1881,6 +1893,21 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1881
1893
|
}
|
|
1882
1894
|
return createDriver(prepared.engine, defaultHost).createConversation(prepared);
|
|
1883
1895
|
},
|
|
1896
|
+
async runNativeDetachedConversation({ id, context, input = {}, options } = {}) {
|
|
1897
|
+
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
1898
|
+
await access(context, id, "runDetachedConversation");
|
|
1899
|
+
if (typeof defaultHost?.conversation !== "function") {
|
|
1900
|
+
throw new TypeError("Native detached execution requires a configured conversation host.");
|
|
1901
|
+
}
|
|
1902
|
+
const prepared = await defaultHost.conversation({ id, context, input,
|
|
1903
|
+
...(options === undefined ? {} : { options }), operation: "runDetachedConversation" });
|
|
1904
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
1905
|
+
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
1906
|
+
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context") || !Object.hasOwn(prepared, "options")) {
|
|
1907
|
+
throw new TypeError("Native detached execution requires its authorized host preparation.");
|
|
1908
|
+
}
|
|
1909
|
+
return createDriver(prepared.engine, defaultHost).runDetachedConversation(prepared);
|
|
1910
|
+
},
|
|
1884
1911
|
async open({ id, context, configuration, engine, host, representation = "canonical" } = {}) {
|
|
1885
1912
|
if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
|
|
1886
1913
|
await access(context, id, "open");
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
export {
|
|
2
|
+
classifyCodexAppServerEvent,
|
|
3
|
+
codexAppServerAssistantItemText,
|
|
4
|
+
codexAppServerContentText,
|
|
5
|
+
codexAppServerErrorText,
|
|
6
|
+
codexAppServerNotificationError,
|
|
7
|
+
codexAppServerNotificationEventPayload,
|
|
8
|
+
codexAppServerNotificationEventType,
|
|
9
|
+
codexAppServerNotificationItem,
|
|
10
|
+
codexAppServerNotificationParams,
|
|
11
|
+
codexAppServerNotificationThreadId,
|
|
12
|
+
codexAppServerNotificationTurnStatus,
|
|
13
|
+
codexAppServerNotificationUsageLimitExceeded,
|
|
14
|
+
codexAppServerOutputOwnerTurnId,
|
|
15
|
+
codexAppServerStatusFromValue,
|
|
16
|
+
codexAppServerUserMessageText,
|
|
2
17
|
codexContextUsageFromNotification,
|
|
3
18
|
codexAppServerContextRefreshReason,
|
|
4
19
|
codexAppServerNotificationEvent,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export {
|
|
2
|
+
codexAppServerTurnStatusIsActive,
|
|
3
|
+
codexAppServerTurnStatusIsComplete,
|
|
4
|
+
codexAppServerTurnStatusIsProviderFailure,
|
|
5
|
+
codexAppServerTurnStatusIsSuccessfulComplete,
|
|
6
|
+
createCodexAppServerDetachedTurnWatcher,
|
|
2
7
|
codexAppServerAgentRun,
|
|
3
8
|
codexAppServerConversationTurnIsActive,
|
|
4
9
|
codexAppServerFrozenTurnInterruptResponse,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateText, jsonSchema, streamText } from "ai";
|
|
1
|
+
import { generateText, jsonSchema, NoSuchToolError, streamText } from "ai";
|
|
2
2
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
3
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
4
4
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
@@ -47,7 +47,7 @@ function sdkMessages(messages) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Accept only the server-side result of an authorized AI connection resolver. */
|
|
50
|
-
export function createAiConnectionClient(connection, { fetch, timeoutMs = 120_000, maxOutputTokens, effort } = {}) {
|
|
50
|
+
export function createAiConnectionClient(connection, { fetch, timeoutMs = 120_000, maxOutputTokens, effort, reportUnavailableToolCalls = false } = {}) {
|
|
51
51
|
if (!connection?.apiKey || !connection.model || !Object.hasOwn(providers, connection.sdkPackage)) {
|
|
52
52
|
throw new TypeError("An authorized AI connection with a supported SDK and model is required.");
|
|
53
53
|
}
|
|
@@ -75,23 +75,21 @@ export function createAiConnectionClient(connection, { fetch, timeoutMs = 120_00
|
|
|
75
75
|
async *createChatCompletionStream(input) {
|
|
76
76
|
const result = streamText({ ...options(input), onError() {} });
|
|
77
77
|
let toolIndex = 0;
|
|
78
|
-
let failure;
|
|
79
78
|
for await (const part of result.fullStream) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (part.type === "error" || (part.type === "tool-call" && part.invalid)) {
|
|
83
|
-
failure ||= part.error || new Error("The model returned an invalid tool call.");
|
|
84
|
-
}
|
|
85
|
-
if (part.type === "abort") failure ||= input?.signal?.reason || new Error("Assistant request was aborted.");
|
|
86
|
-
if (failure) continue;
|
|
79
|
+
if (part.type === "error") throw part.error;
|
|
80
|
+
if (part.type === "abort") throw input?.signal?.reason || new Error("Assistant request was aborted.");
|
|
87
81
|
if (part.type === "text-delta") yield { choices: [{ delta: { content: part.text } }] };
|
|
88
82
|
if (part.type === "finish") yield { choices: [{ delta: {}, finish_reason: part.finishReason }], usage: part.totalUsage };
|
|
89
83
|
if (part.type === "tool-call") {
|
|
84
|
+
// Only the application catalogue can reject an unavailable action
|
|
85
|
+
// and persist its no-effect receipt. Other invalid calls stay errors.
|
|
86
|
+
const unavailable = reportUnavailableToolCalls === true && NoSuchToolError.isInstance(part.error) &&
|
|
87
|
+
!part.providerExecuted && part.input !== null && typeof part.input === "object" && !Array.isArray(part.input);
|
|
88
|
+
if (part.invalid && !unavailable) throw part.error;
|
|
90
89
|
yield { choices: [{ delta: { tool_calls: [{ index: toolIndex++, id: part.toolCallId,
|
|
91
90
|
function: { name: part.toolName, arguments: JSON.stringify(part.input) } }] } }] };
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
if (failure) throw failure;
|
|
95
93
|
}
|
|
96
94
|
});
|
|
97
95
|
}
|