@jskit-ai/assistant-core 0.1.211 → 0.1.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/server/conversation/claudeProcess.js +12 -2
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +185 -15
- package/src/server/conversation/openCodeRuntime.js +51 -4
- package/src/server/conversation/runtime.js +6 -1
- package/test/claudeConversationSession.test.js +88 -0
- package/test/claudeProcess.test.js +74 -0
- package/test/claudeTurn.test.js +435 -0
- package/test/conversationTools.test.js +53 -0
- package/test/openCodeRuntime.test.js +294 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.213",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.233",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.235",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.52",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.52"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.213",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.177",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.177",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.233",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.235",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -147,6 +147,10 @@ export function claudeCodeArguments({
|
|
|
147
147
|
} : {} }));
|
|
148
148
|
if (!applicationTools) args.push("--disallowedTools", "mcp__*");
|
|
149
149
|
if (!toolFree) args.push("--tools", "Bash,Read,Edit,Write,Glob,Grep,NotebookEdit");
|
|
150
|
+
} else if (applicationTools) {
|
|
151
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: {
|
|
152
|
+
[CLAUDE_APPLICATION_TOOL_SERVER]: { type: "sdk", name: CLAUDE_APPLICATION_TOOL_SERVER }
|
|
153
|
+
} }));
|
|
150
154
|
}
|
|
151
155
|
if (outputSchema) args.push("--json-schema", JSON.stringify(outputSchema));
|
|
152
156
|
return args;
|
|
@@ -318,8 +322,14 @@ export async function createClaudeCodeProcess({
|
|
|
318
322
|
} } : {});
|
|
319
323
|
return { client, initialization, executionId: native.id, stop };
|
|
320
324
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
// A rejected start may already own an execution and its cleanup proof.
|
|
326
|
+
// Only a returned native handle transfers that cleanup to this owner.
|
|
327
|
+
if (native) {
|
|
328
|
+
error.executionId = native.id;
|
|
329
|
+
try { error.stopProof = await stop(); }
|
|
330
|
+
catch (cleanupError) { error.cleanupError = cleanupError; error.stopProof = { scopeEmpty: false }; }
|
|
331
|
+
if (error.stopProof?.scopeEmpty !== true) error.cleanupFailed = true;
|
|
332
|
+
}
|
|
323
333
|
throw error;
|
|
324
334
|
} finally {
|
|
325
335
|
signal?.removeEventListener("abort", abort);
|
|
@@ -4,7 +4,7 @@ export const CLAUDE_APPLICATION_TOOL_SERVER = "application";
|
|
|
4
4
|
|
|
5
5
|
// The CLI carries this SDK MCP exchange over its existing control pipe. There
|
|
6
6
|
// is no listening socket or separate tool process. The shared executor owns effects.
|
|
7
|
-
export async function claudeApplicationToolResponse(request, { schemas, turn, signal }) {
|
|
7
|
+
export async function claudeApplicationToolResponse(request, { schemas, turn, signal, assertCurrent, onExecutionFailure }) {
|
|
8
8
|
if (request?.subtype !== "mcp_message" || request.server_name !== CLAUDE_APPLICATION_TOOL_SERVER ||
|
|
9
9
|
request.message?.jsonrpc !== "2.0") throw new Error("This Claude control request is not an application tool exchange.");
|
|
10
10
|
const message = request.message;
|
|
@@ -31,7 +31,16 @@ export async function claudeApplicationToolResponse(request, { schemas, turn, si
|
|
|
31
31
|
!isDeepStrictEqual(native.input, params.arguments || {})) {
|
|
32
32
|
throw new Error("Claude's application call does not match an admitted native tool use. Update Claude if its tool identity is unavailable.");
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
// Optional supplied-owner checks run after the unchanged native-use
|
|
35
|
+
// validation. Protocol/stale refusals are not owned executor failures.
|
|
36
|
+
assertCurrent?.(native);
|
|
37
|
+
let value;
|
|
38
|
+
try {
|
|
39
|
+
value = await turn.tools.execute({ id, name: params.name, arguments: JSON.stringify(params.arguments || {}) }, { signal, messageId: native.messageId });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
onExecutionFailure?.(error, native);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
35
44
|
result = { content: [{ type: "text", text: JSON.stringify(value) }], isError: value.ok === false };
|
|
36
45
|
break;
|
|
37
46
|
}
|
|
@@ -149,8 +149,20 @@ export function createClaudeConversationOwner({
|
|
|
149
149
|
phase: entry.turn.active && !entry.observationError ? text(entry.turn.phase) : "" } : null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function currentApplicationCommand(entry, command, owner = command?.nativeOwner) {
|
|
153
|
+
const native = entry?.nativeTurn.read();
|
|
154
|
+
return Boolean(owner && entry && !closing && !closingSessions.has(entry.context.key) &&
|
|
155
|
+
entries.get(entry.key) === entry && entry.command === command && command.entry === entry &&
|
|
156
|
+
command.admitted && !command.signal.aborted && !command.toolFailure &&
|
|
157
|
+
!entry.disposed && !entry.stopping && !entry.stopPending && !entry.observationError &&
|
|
158
|
+
entry.process === owner.process && entry.executionId === owner.executionId &&
|
|
159
|
+
entry.accountIdentity === owner.accountIdentity && entry.id === owner.threadId &&
|
|
160
|
+
entry.turn?.id === owner.turnId && entry.turn.active && native.turnId === owner.turnId && native.active);
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
function readRetainedTurn(contextKey, conversationId, state) {
|
|
153
164
|
const entry = entries.get(`${contextKey}\0${conversationId}`);
|
|
165
|
+
if (state.current) return currentApplicationCommand(entry, entry?.command) ? snapshot(entry) : null;
|
|
154
166
|
const saved = state.saved;
|
|
155
167
|
return state.nativeResult?.turn || (entry ? snapshot(entry) : saved?.turnId ? {
|
|
156
168
|
id: saved.turnId, threadId: conversationId, state: saved.state,
|
|
@@ -158,6 +170,15 @@ export function createClaudeConversationOwner({
|
|
|
158
170
|
} : null);
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
// This lookup uses only the existing current entry and native receipt owner.
|
|
174
|
+
function readFinalAssistantResult(contextKey, conversationId = "", turnId = "") {
|
|
175
|
+
if (!text(contextKey) || !text(conversationId) || !text(turnId) || closing || closingSessions.has(contextKey)) return null;
|
|
176
|
+
const entry = entries.get(`${contextKey}\0${conversationId}`);
|
|
177
|
+
if (!entry || entry.disposed || entry.stopping || entry.observationError ||
|
|
178
|
+
entry.id !== conversationId || entry.context.key !== contextKey || entry.turn?.id !== turnId) return null;
|
|
179
|
+
return entry.nativeTurn.readFinalAssistantResult(conversationId, turnId);
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
function readHistory(entry) {
|
|
162
183
|
return readClaudeHistory({ configRoot, workdir: entry.nativeWorkdir, conversationId: entry.id });
|
|
163
184
|
}
|
|
@@ -231,6 +252,7 @@ export function createClaudeConversationOwner({
|
|
|
231
252
|
const account = preparation.account(entry);
|
|
232
253
|
const identity = await account.identity;
|
|
233
254
|
const providerId = account.providerId;
|
|
255
|
+
if (entry.accountIdentity !== identity) entry.nativeTurn.invalidateFinalAssistantResult();
|
|
234
256
|
try {
|
|
235
257
|
await bindClaudeConversationAccount(entry, { identity, providerId,
|
|
236
258
|
stop: (entry, reason) => entry.nativeTurn.stopProcess(reason), save: saveAccount });
|
|
@@ -310,10 +332,27 @@ export function createClaudeConversationOwner({
|
|
|
310
332
|
}
|
|
311
333
|
await entry.nativeTurn.receive(frame);
|
|
312
334
|
},
|
|
313
|
-
...(store.releaseExecution ? {
|
|
335
|
+
...(store.releaseExecution || entry.command?.tools ? {
|
|
314
336
|
async onControlRequest(request, { signal }) {
|
|
315
337
|
const active = entry.command;
|
|
316
|
-
const
|
|
338
|
+
const suppliedTools = !store.releaseExecution && active?.tools;
|
|
339
|
+
const owner = active?.nativeOwner;
|
|
340
|
+
const work = claudeApplicationToolResponse(request, { schemas: entry.toolSchemas || [], turn: active, signal,
|
|
341
|
+
...(suppliedTools ? {
|
|
342
|
+
assertCurrent(native) {
|
|
343
|
+
if (!currentApplicationCommand(entry, active, owner) || active.nativeOwner !== owner ||
|
|
344
|
+
native.messageId !== active.messageId) throw new Error("Claude's application call belongs to a retired input.");
|
|
345
|
+
},
|
|
346
|
+
onExecutionFailure(error, native) {
|
|
347
|
+
// Already-invoked old work retains its original result. Only
|
|
348
|
+
// this still-current authored input can fail this native run.
|
|
349
|
+
if (currentApplicationCommand(entry, active, owner) && active.nativeOwner === owner &&
|
|
350
|
+
native.messageId === active.messageId) {
|
|
351
|
+
active.toolFailure = error;
|
|
352
|
+
active.failure.reject(error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} : {}) });
|
|
317
356
|
active?.toolWork.add(work);
|
|
318
357
|
try { return await work; }
|
|
319
358
|
finally { active?.toolWork.delete(work); }
|
|
@@ -331,13 +370,23 @@ export function createClaudeConversationOwner({
|
|
|
331
370
|
await entry.nativeTurn.stopProcess(error.message);
|
|
332
371
|
}
|
|
333
372
|
}); } catch (error) {
|
|
373
|
+
if (error.executionId && error.stopProof?.scopeEmpty !== true && !entry.executionId) {
|
|
374
|
+
// Streaming startup can fail after host admission, before onStarted.
|
|
375
|
+
// Retain that custody through the same binding owner for Stop recovery.
|
|
376
|
+
error.cleanupFailed = true;
|
|
377
|
+
entry.executionId = error.executionId;
|
|
378
|
+
try {
|
|
379
|
+
await store.save(entry, { execution: true });
|
|
380
|
+
await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
|
|
381
|
+
} catch (bindingError) { error.bindingError = bindingError; }
|
|
382
|
+
}
|
|
334
383
|
if (store.releaseExecution && error.stopProof?.scopeEmpty) await store.releaseExecution(entry);
|
|
335
384
|
throw error;
|
|
336
385
|
}
|
|
337
386
|
return entry.process;
|
|
338
387
|
}
|
|
339
388
|
|
|
340
|
-
async function send(entry, input = {}, { renewal = false, command } = {}) {
|
|
389
|
+
async function send(entry, input = {}, { renewal = false, command, applicationCommand } = {}) {
|
|
341
390
|
const message = command ? input.text : text(input.message || input.prompt);
|
|
342
391
|
if (!command && !message) throw createError("Enter a message for Claude.");
|
|
343
392
|
const context = entry.context;
|
|
@@ -386,8 +435,17 @@ export function createClaudeConversationOwner({
|
|
|
386
435
|
entry.onEvent = context.onEvent;
|
|
387
436
|
entry.lastMessageId = uuid;
|
|
388
437
|
const actorMetadata = command ? undefined : await onEvent(entry, { type: "message-metadata", input, context });
|
|
438
|
+
const nativeOwner = applicationCommand ? { process: native, executionId: entry.executionId,
|
|
439
|
+
accountIdentity: entry.accountIdentity, threadId: entry.id, turnId: entry.turn.id } : null;
|
|
389
440
|
let conversationTurn;
|
|
441
|
+
function deliveryResult() {
|
|
442
|
+
return { ok: true, delivered: true, deliveryMode: steering ? "steer" : "new_turn",
|
|
443
|
+
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
444
|
+
}
|
|
390
445
|
async function accept() {
|
|
446
|
+
// The new native ACK retires old effect custody while its canonical
|
|
447
|
+
// admission/commit is awaited; it cannot retag a delayed old failure.
|
|
448
|
+
if (applicationCommand) applicationCommand.active.nativeOwner = null;
|
|
391
449
|
if (command) {
|
|
392
450
|
if (!steering) await onEvent(entry, { type: "binding-admitted" });
|
|
393
451
|
await command.accept();
|
|
@@ -401,6 +459,22 @@ export function createClaudeConversationOwner({
|
|
|
401
459
|
if (admission) conversationTurn = await admission;
|
|
402
460
|
}
|
|
403
461
|
if (!command) await entry.nativeTurn.updateState("active");
|
|
462
|
+
if (applicationCommand && conversationTurn) {
|
|
463
|
+
const value = deliveryResult();
|
|
464
|
+
applicationCommand.active.delivery = { value, completion: entry.completion?.promise };
|
|
465
|
+
// The original canonical admission and publication precede Core's same
|
|
466
|
+
// accepted-request gate; native tools cannot race an unaccepted user.
|
|
467
|
+
await applicationCommand.accept({ nativeResult: { value }, conversationTurn, nativeTurnId: entry.turn?.id });
|
|
468
|
+
if (entry.process !== nativeOwner.process || entry.executionId !== nativeOwner.executionId ||
|
|
469
|
+
entry.accountIdentity !== nativeOwner.accountIdentity || entry.id !== nativeOwner.threadId ||
|
|
470
|
+
entry.turn?.id !== nativeOwner.turnId || entry.disposed || entry.stopping || entry.stopPending) {
|
|
471
|
+
throw new Error("Claude's admitted tools no longer belong to this native process.");
|
|
472
|
+
}
|
|
473
|
+
applicationCommand.active.nativeOwner = nativeOwner;
|
|
474
|
+
applicationCommand.active.messageId = messageId;
|
|
475
|
+
if (steering) applicationCommand.active.nativeToolCount = 0;
|
|
476
|
+
applicationCommand.active.admitted = true;
|
|
477
|
+
}
|
|
404
478
|
}
|
|
405
479
|
try {
|
|
406
480
|
if (command) command.attempted = true;
|
|
@@ -411,8 +485,7 @@ export function createClaudeConversationOwner({
|
|
|
411
485
|
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
412
486
|
throw error;
|
|
413
487
|
}
|
|
414
|
-
return
|
|
415
|
-
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
488
|
+
return deliveryResult();
|
|
416
489
|
}
|
|
417
490
|
|
|
418
491
|
async function startTurn(entry, input = {}) {
|
|
@@ -641,10 +714,19 @@ export function createClaudeConversationOwner({
|
|
|
641
714
|
}
|
|
642
715
|
command.signal.throwIfAborted();
|
|
643
716
|
const entry = await acquire(command.context);
|
|
644
|
-
const
|
|
717
|
+
const active = control.current;
|
|
718
|
+
const applicationCommand = active?.tools ? { active, accept: command.accept } : null;
|
|
719
|
+
if (applicationCommand) {
|
|
720
|
+
if (entry.disposed || entry.command && entry.command !== active) throw new Error("This Claude conversation is closed or already working.");
|
|
721
|
+
if (active.entry && active.entry !== entry) throw new Error("Claude's application tools belong to another native conversation.");
|
|
722
|
+
active.entry = entry;
|
|
723
|
+
entry.command = active;
|
|
724
|
+
entry.toolSchemas = active.tools.schemas;
|
|
725
|
+
}
|
|
726
|
+
const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch }, { applicationCommand });
|
|
645
727
|
const completion = value.conversationTurn ? entry.completion?.promise : null;
|
|
646
728
|
const delivered = { value, completion };
|
|
647
|
-
if (value.delivered === true && value.conversationTurn) {
|
|
729
|
+
if (!applicationCommand && value.delivered === true && value.conversationTurn) {
|
|
648
730
|
if (control.current) control.current.delivery = delivered;
|
|
649
731
|
await command.accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
650
732
|
}
|
|
@@ -734,12 +816,29 @@ export function createClaudeConversationOwner({
|
|
|
734
816
|
if (binding) {
|
|
735
817
|
try {
|
|
736
818
|
if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
|
|
737
|
-
} catch (error) {
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (problem && error !== problem) error.cause ||= problem;
|
|
821
|
+
problem = error;
|
|
822
|
+
}
|
|
738
823
|
finally {
|
|
739
824
|
await Promise.allSettled([...active.toolWork]);
|
|
740
825
|
if (entry?.command === active) entry.command = null;
|
|
741
826
|
}
|
|
742
|
-
} else
|
|
827
|
+
} else {
|
|
828
|
+
await cancellation?.catch(() => {});
|
|
829
|
+
if (tools && active.entry) {
|
|
830
|
+
try {
|
|
831
|
+
if (problem || signal.aborted) await active.entry.nativeTurn.stopProcess(problem?.message || "Work stopped.");
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (problem && error !== problem) error.cause ||= problem;
|
|
834
|
+
problem = error;
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
await Promise.allSettled([...active.toolWork]);
|
|
838
|
+
if (active.entry.command === active) active.entry.command = null;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
743
842
|
if (control.current === active) control.current = null;
|
|
744
843
|
if (problem) {
|
|
745
844
|
if (binding && nativeDispatch.attempted && !nativeDispatch.admitted) problem.delivery = "uncertain";
|
|
@@ -748,7 +847,7 @@ export function createClaudeConversationOwner({
|
|
|
748
847
|
if (!binding) return { value: delivered.value };
|
|
749
848
|
}
|
|
750
849
|
|
|
751
|
-
return Object.freeze({ entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readHistory, ensureReady, ensureProcess, stopForTerminal, prepareTerminal, send, startTurn,
|
|
850
|
+
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, stopForTerminal, prepareTerminal, send, startTurn,
|
|
752
851
|
interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
|
|
753
852
|
readGoal, updateGoal, renewal, run, steer, cancel });
|
|
754
853
|
}
|
|
@@ -773,6 +872,36 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
773
872
|
let cancelling = false;
|
|
774
873
|
let steering;
|
|
775
874
|
let maximumOutput = 4 * 1024 * 1024;
|
|
875
|
+
let inputOwner = null;
|
|
876
|
+
let acceptedInput = null;
|
|
877
|
+
let publishedAssistant = null;
|
|
878
|
+
let finalAssistantResult = null;
|
|
879
|
+
|
|
880
|
+
function clearFinalAssistantResult() {
|
|
881
|
+
publishedAssistant = null;
|
|
882
|
+
finalAssistantResult = null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function invalidateFinalAssistantResult() {
|
|
886
|
+
inputOwner = null;
|
|
887
|
+
acceptedInput = null;
|
|
888
|
+
clearFinalAssistantResult();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function currentReceiptOwner(owner) {
|
|
892
|
+
return Boolean(entry && owner && inputOwner === owner && acceptedInput === owner &&
|
|
893
|
+
owner.commandId === currentCommandId && entry.process && entry.process === owner.process &&
|
|
894
|
+
entry.executionId === owner.executionId && entry.accountIdentity === owner.accountIdentity &&
|
|
895
|
+
entry.turn?.id === turnId && !entry.stopping && !entry.stopPending && !entry.disposed &&
|
|
896
|
+
!entry.observationError && !entry.command?.toolWork.size && !entry.command?.toolFailure);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function readFinalAssistantResult(threadId, expectedTurnId) {
|
|
900
|
+
if (threadId !== conversationId || expectedTurnId !== turnId || !finalAssistantResult ||
|
|
901
|
+
active || stopped || cancelling || outcome?.status !== "completed" || outcome.error ||
|
|
902
|
+
inFlight.size || tasks.size || admissions.size || !currentReceiptOwner(acceptedInput)) return null;
|
|
903
|
+
return structuredClone(finalAssistantResult);
|
|
904
|
+
}
|
|
776
905
|
|
|
777
906
|
function read() {
|
|
778
907
|
return { turnId, active, text: result, phase, status: active ? "inProgress" : outcome?.status || "ready",
|
|
@@ -810,6 +939,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
810
939
|
}
|
|
811
940
|
|
|
812
941
|
async function stopProcess(reason = "") {
|
|
942
|
+
invalidateFinalAssistantResult();
|
|
813
943
|
// Runtime bindings commit execution release before dropping the handle.
|
|
814
944
|
// Retained host entries keep their original post-proof checkpoint order.
|
|
815
945
|
const binding = Boolean(process.releaseExecution);
|
|
@@ -864,7 +994,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
864
994
|
if (binding) setState(state, event.error);
|
|
865
995
|
else await updateState(state, event.error);
|
|
866
996
|
}
|
|
867
|
-
await onEvent(event);
|
|
997
|
+
const published = await onEvent(event);
|
|
868
998
|
if (event.type === "provider-event") {
|
|
869
999
|
const frame = event.event;
|
|
870
1000
|
const change = frame.type === "active_goal" ? "goal" : frame.type === "rate_limit_event" ? "usage" : "";
|
|
@@ -874,6 +1004,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
874
1004
|
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
875
1005
|
entry.completion?.resolve(binding ? event : readResult());
|
|
876
1006
|
}
|
|
1007
|
+
return published;
|
|
877
1008
|
}
|
|
878
1009
|
|
|
879
1010
|
function checkOutput(value, role = "assistant") {
|
|
@@ -884,7 +1015,16 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
884
1015
|
|
|
885
1016
|
async function publishMessage(message) {
|
|
886
1017
|
messages.set(message.id, message);
|
|
887
|
-
|
|
1018
|
+
const owner = acceptedInput;
|
|
1019
|
+
const completeAssistant = message.complete && message.role === "assistant";
|
|
1020
|
+
if (completeAssistant) clearFinalAssistantResult();
|
|
1021
|
+
const conversationTurn = await publish({ type: "message", message });
|
|
1022
|
+
if (completeAssistant && message.text && currentReceiptOwner(owner) && !stopped && !cancelling &&
|
|
1023
|
+
conversationTurn && typeof conversationTurn === "object" && !Array.isArray(conversationTurn)) {
|
|
1024
|
+
publishedAssistant = { owner, itemId: message.id, outputId: message.outputId || message.id,
|
|
1025
|
+
text: message.text, conversationTurn: structuredClone(conversationTurn) };
|
|
1026
|
+
}
|
|
1027
|
+
return conversationTurn;
|
|
888
1028
|
}
|
|
889
1029
|
|
|
890
1030
|
async function receive(frame) {
|
|
@@ -908,9 +1048,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
908
1048
|
clearTimeout(pending.timer);
|
|
909
1049
|
try {
|
|
910
1050
|
await pending.accept();
|
|
1051
|
+
if (inputOwner === pending.receiptOwner && !stopped && !cancelling) acceptedInput = pending.receiptOwner;
|
|
911
1052
|
admissions.delete(uuid);
|
|
912
1053
|
pending.resolve();
|
|
913
1054
|
} catch (error) {
|
|
1055
|
+
acceptedInput = null;
|
|
1056
|
+
clearFinalAssistantResult();
|
|
914
1057
|
pending.reject(error);
|
|
915
1058
|
throw error;
|
|
916
1059
|
}
|
|
@@ -988,12 +1131,22 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
988
1131
|
outcome && inFlight.size === 0 && tasks.size === 0) {
|
|
989
1132
|
active = false;
|
|
990
1133
|
phase = "";
|
|
991
|
-
|
|
1134
|
+
const candidate = publishedAssistant;
|
|
1135
|
+
finalAssistantResult = outcome.status === "completed" && !outcome.error && !stopped && !cancelling &&
|
|
1136
|
+
admissions.size === 0 && candidate && currentReceiptOwner(candidate.owner) && candidate.text === result
|
|
1137
|
+
? { threadId: conversationId, turnId, inputMessageId: candidate.owner.commandId,
|
|
1138
|
+
itemId: candidate.itemId, outputId: candidate.outputId, text: candidate.text,
|
|
1139
|
+
conversationTurn: candidate.conversationTurn } : null;
|
|
1140
|
+
try { await publish({ type: "settled", ...read(), source: frame.type }); }
|
|
1141
|
+
catch (error) { clearFinalAssistantResult(); throw error; }
|
|
992
1142
|
}
|
|
993
1143
|
}
|
|
994
1144
|
|
|
995
1145
|
function stop(reason = "Claude stopped before acknowledging the prompt.") {
|
|
996
1146
|
stopped = true;
|
|
1147
|
+
inputOwner = null;
|
|
1148
|
+
acceptedInput = null;
|
|
1149
|
+
clearFinalAssistantResult();
|
|
997
1150
|
for (const pending of admissions.values()) {
|
|
998
1151
|
pending.reject(failure(reason));
|
|
999
1152
|
}
|
|
@@ -1008,12 +1161,13 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1008
1161
|
|
|
1009
1162
|
async function interrupt(client) {
|
|
1010
1163
|
cancelling = true;
|
|
1164
|
+
clearFinalAssistantResult();
|
|
1011
1165
|
if (client) await client.interrupt();
|
|
1012
1166
|
// The process host still must drain its owned scope and call stop().
|
|
1013
1167
|
}
|
|
1014
1168
|
|
|
1015
1169
|
return Object.freeze({
|
|
1016
|
-
receive, read, stop, interrupt,
|
|
1170
|
+
receive, read, stop, interrupt, readFinalAssistantResult, invalidateFinalAssistantResult,
|
|
1017
1171
|
...(entry ? { readResult, updateState, stopProcess } : {}),
|
|
1018
1172
|
...(instructions ? {
|
|
1019
1173
|
prepare: instructions.prepare,
|
|
@@ -1024,6 +1178,9 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1024
1178
|
begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
|
|
1025
1179
|
if (active) throw failure("This Claude turn is still working.");
|
|
1026
1180
|
if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
|
|
1181
|
+
inputOwner = null;
|
|
1182
|
+
acceptedInput = null;
|
|
1183
|
+
clearFinalAssistantResult();
|
|
1027
1184
|
turnId = id;
|
|
1028
1185
|
maximumOutput = maxOutputCharacters;
|
|
1029
1186
|
messages.clear();
|
|
@@ -1041,10 +1198,18 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1041
1198
|
messages.clear();
|
|
1042
1199
|
result = "";
|
|
1043
1200
|
outcome = null;
|
|
1201
|
+
inputOwner = null;
|
|
1202
|
+
acceptedInput = null;
|
|
1203
|
+
clearFinalAssistantResult();
|
|
1044
1204
|
},
|
|
1045
1205
|
async send(client, message, { messageId: uuid, accept, timeoutMs = 30_000 } = {}) {
|
|
1046
1206
|
if (!active || stopped || cancelling) throw failure("Claude has no active turn for this message.");
|
|
1047
1207
|
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
1208
|
+
clearFinalAssistantResult();
|
|
1209
|
+
acceptedInput = null;
|
|
1210
|
+
inputOwner = entry ? { commandId: uuid, process: entry.process, executionId: entry.executionId,
|
|
1211
|
+
accountIdentity: entry.accountIdentity } : null;
|
|
1212
|
+
const receiptOwner = inputOwner;
|
|
1048
1213
|
const admitted = Promise.withResolvers();
|
|
1049
1214
|
void admitted.promise.catch(() => {});
|
|
1050
1215
|
inFlight.add(uuid);
|
|
@@ -1052,9 +1217,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1052
1217
|
failure("Claude has not acknowledged this prompt. Its delivery is uncertain.", "assistant_claude_admission_unknown"),
|
|
1053
1218
|
{ delivery: "uncertain" }
|
|
1054
1219
|
)), timeoutMs);
|
|
1055
|
-
admissions.set(uuid, { ...admitted, accept, timer });
|
|
1220
|
+
admissions.set(uuid, { ...admitted, accept, timer, receiptOwner });
|
|
1056
1221
|
try {
|
|
1057
1222
|
await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId }), admitted.promise]);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (inputOwner === receiptOwner) invalidateFinalAssistantResult();
|
|
1225
|
+
throw error;
|
|
1058
1226
|
} finally {
|
|
1059
1227
|
clearTimeout(timer);
|
|
1060
1228
|
}
|
|
@@ -1064,6 +1232,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1064
1232
|
// Streaming user input otherwise queues behind the entire running turn.
|
|
1065
1233
|
// Claude's supported steering path interrupts generation, then continues
|
|
1066
1234
|
// the same native history with the new instruction.
|
|
1235
|
+
acceptedInput = null;
|
|
1236
|
+
clearFinalAssistantResult();
|
|
1067
1237
|
steering = Promise.withResolvers();
|
|
1068
1238
|
let timer;
|
|
1069
1239
|
try {
|
|
@@ -908,13 +908,17 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
908
908
|
|
|
909
909
|
async function writeConversationProjection(sessionId, messages = null, {
|
|
910
910
|
inputMessageId = "",
|
|
911
|
-
streaming = false
|
|
911
|
+
streaming = false,
|
|
912
|
+
captureFinalAssistantResult = false
|
|
912
913
|
} = {}, projection) {
|
|
913
914
|
let failure = "";
|
|
915
|
+
let finalAssistantResult = null;
|
|
914
916
|
let providerApiFailure = false;
|
|
915
917
|
const rows = (inputMessageId
|
|
916
918
|
? openCodeRowsForInput(messages, inputMessageId)
|
|
917
919
|
: openCodeMessageRows(messages)).filter((message) => message?.type === "assistant");
|
|
920
|
+
const finalNativeMessage = captureFinalAssistantResult && !streaming
|
|
921
|
+
? openCodeLastAssistantResult(rows, { readError: projection.readError }).message : null;
|
|
918
922
|
for (const [index, message] of rows.entries()) {
|
|
919
923
|
failure ||= projection.readError(message);
|
|
920
924
|
providerApiFailure ||= openCodeProviderApiFailure(message.error);
|
|
@@ -967,9 +971,31 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
967
971
|
});
|
|
968
972
|
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
969
973
|
await projection.publishTurn(turn);
|
|
974
|
+
if (message === finalNativeMessage && !projection.readError(message) &&
|
|
975
|
+
(message.time?.completed || message.finish) && turn) {
|
|
976
|
+
finalAssistantResult = { inputMessageId: text(inputMessageId), itemId: text(message.id),
|
|
977
|
+
text: assistantText, conversationTurn: turn };
|
|
978
|
+
}
|
|
970
979
|
}
|
|
971
980
|
}
|
|
972
|
-
return { failure, providerApiFailure };
|
|
981
|
+
return { failure, providerApiFailure, ...(captureFinalAssistantResult ? { finalAssistantResult } : {}) };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Evidence belongs to the exact retained native turn. This read never creates,
|
|
985
|
+
// observes, resumes or projects a native conversation.
|
|
986
|
+
function readFinalAssistantResult(key, threadId = "", turnId = "") {
|
|
987
|
+
const turn = turns.get(key);
|
|
988
|
+
const result = turn?.finalAssistantResult;
|
|
989
|
+
const target = turn?.finalAssistantResultTarget;
|
|
990
|
+
if (!target || processes.get(key) !== target || target.abortController.signal.aborted ||
|
|
991
|
+
!turn.finalAssistantResultProcess || sharedProcess !== turn.finalAssistantResultProcess ||
|
|
992
|
+
target.upstreamSessionId !== turn.threadId) return null;
|
|
993
|
+
if (!text(threadId) || !text(turnId) || !result || turn.active || turn.state !== "completed" ||
|
|
994
|
+
turn.error || turn.observationError || turn.interruptRequested || turn.interruptAcknowledged ||
|
|
995
|
+
turn.threadId !== text(threadId) || turn.id !== text(turnId) ||
|
|
996
|
+
result.threadId !== text(threadId) || result.turnId !== text(turnId) ||
|
|
997
|
+
result.inputMessageId !== text(turn.inputMessageId) || !result.conversationTurn) return null;
|
|
998
|
+
return structuredClone(result);
|
|
973
999
|
}
|
|
974
1000
|
|
|
975
1001
|
async function sendMessage(key, input, options, application) {
|
|
@@ -1232,6 +1258,15 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1232
1258
|
observe, observation, eventReady, readiness, finalResponse,
|
|
1233
1259
|
writeRun, projectMessages, completeResult, onRetired
|
|
1234
1260
|
}) {
|
|
1261
|
+
delete turn.finalAssistantResult;
|
|
1262
|
+
delete turn.finalAssistantResultTarget;
|
|
1263
|
+
delete turn.finalAssistantResultProcess;
|
|
1264
|
+
const registeredTarget = processes.get(key);
|
|
1265
|
+
const finalAssistantResultTarget = registeredTarget &&
|
|
1266
|
+
registeredTarget.abortController === target.abortController &&
|
|
1267
|
+
registeredTarget.server === target.server &&
|
|
1268
|
+
registeredTarget.upstreamSessionId === target.upstreamSessionId ? registeredTarget : null;
|
|
1269
|
+
const finalAssistantResultProcess = finalAssistantResultTarget ? sharedProcess : null;
|
|
1235
1270
|
turns.set(key, turn);
|
|
1236
1271
|
const signal = AbortSignal.any([target.abortController.signal, turn.abortController.signal]);
|
|
1237
1272
|
const monitor = Promise.resolve().then(async () => {
|
|
@@ -1243,6 +1278,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1243
1278
|
}
|
|
1244
1279
|
let finalState = "completed";
|
|
1245
1280
|
let failure = "";
|
|
1281
|
+
let finalAssistantResult = null;
|
|
1246
1282
|
let credentialFailure = false;
|
|
1247
1283
|
let providerApiFailure = false;
|
|
1248
1284
|
try {
|
|
@@ -1285,7 +1321,8 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1285
1321
|
continue;
|
|
1286
1322
|
}
|
|
1287
1323
|
const projection = await projectMessages(completion.messages, {
|
|
1288
|
-
inputMessageId: completion.inputMessageId
|
|
1324
|
+
inputMessageId: completion.inputMessageId,
|
|
1325
|
+
captureFinalAssistantResult: true
|
|
1289
1326
|
});
|
|
1290
1327
|
await turn.admission?.promise;
|
|
1291
1328
|
signal.throwIfAborted();
|
|
@@ -1303,6 +1340,13 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1303
1340
|
} else if (turn.interruptRequested) {
|
|
1304
1341
|
finalState = "interrupted";
|
|
1305
1342
|
}
|
|
1343
|
+
const published = projection.finalAssistantResult;
|
|
1344
|
+
if (finalState === "completed" && !failure && published &&
|
|
1345
|
+
published.inputMessageId === completion.inputMessageId &&
|
|
1346
|
+
published.itemId === completion.result?.turnId &&
|
|
1347
|
+
text(published.text) === text(completion.result?.text)) {
|
|
1348
|
+
finalAssistantResult = { ...published, threadId: turn.threadId, turnId: turn.id };
|
|
1349
|
+
}
|
|
1306
1350
|
break;
|
|
1307
1351
|
}
|
|
1308
1352
|
} catch (error) {
|
|
@@ -1341,6 +1385,9 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1341
1385
|
}
|
|
1342
1386
|
turn.error = failure;
|
|
1343
1387
|
turn.state = finalState;
|
|
1388
|
+
turn.finalAssistantResult = finalState === "completed" && !failure ? finalAssistantResult : null;
|
|
1389
|
+
turn.finalAssistantResultTarget = turn.finalAssistantResult ? finalAssistantResultTarget : null;
|
|
1390
|
+
turn.finalAssistantResultProcess = turn.finalAssistantResult ? finalAssistantResultProcess : null;
|
|
1344
1391
|
turn.updatedAt = new Date().toISOString();
|
|
1345
1392
|
await writeRun(turn, finalState, failure).catch(() => null);
|
|
1346
1393
|
}
|
|
@@ -1615,7 +1662,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1615
1662
|
ensurePreparedSessionReadiness, inspectSessionReadiness, recoverSessionReadiness, needsObservationRecovery, recoverObservation,
|
|
1616
1663
|
createConversation, selectConversation, existingConversation, runConversationTurn, readConversation, readPersistentConversation, waitForConversationTurn,
|
|
1617
1664
|
stopConversation, deleteConversation, sendMessage, hasActiveTemporaryConversation, reconcileSessions, reconcilePreparedSession, recordStoppedObservation, beginMessageMonitor, beginMonitor, writeBindings,
|
|
1618
|
-
writeConversationProjection,
|
|
1665
|
+
writeConversationProjection, readFinalAssistantResult,
|
|
1619
1666
|
projectConversationMessages: projectOpenCodeConversationMessages,
|
|
1620
1667
|
readSessionState, startPreparedTerminal, attachTerminal, closeTerminal, allowTerminalAttachments,
|
|
1621
1668
|
turnSnapshot: openCodeTurnSnapshot, providerApiFailure: openCodeProviderApiFailure
|
|
@@ -910,7 +910,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
910
910
|
// Calls already in flight retain their actor and turn. Keep application
|
|
911
911
|
// mutations ordered across steering as well as within each request.
|
|
912
912
|
const work = toolWork.then(() => request.tools.execute(input, options));
|
|
913
|
-
toolWork = work.catch(error => {
|
|
913
|
+
toolWork = work.catch(error => {
|
|
914
|
+
// Claude accepts a separate successor query; its predecessor's
|
|
915
|
+
// effect cannot fail that query. Other engines retain the original
|
|
916
|
+
// active-turn failure guard across steering.
|
|
917
|
+
if (engine !== "claude" || current === request) toolFailure ||= error;
|
|
918
|
+
});
|
|
914
919
|
return work;
|
|
915
920
|
}
|
|
916
921
|
};
|