@jskit-ai/assistant-core 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +18 -12
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -1,29 +1,96 @@
|
|
|
1
|
+
import { defaultSettingsMiddleware, extractReasoningMiddleware, wrapLanguageModel } from "ai";
|
|
2
|
+
import {
|
|
3
|
+
LEAKED_MARKUP, REASONING, checkTexts, clipOldResult, compactMessages, createTool, remindOnRepeat, reportsActions, runLoop,
|
|
4
|
+
stoppedShort, stripNotes, toolRecord, unsupportedClaims, windowLimits, withinBudget, withoutClosingWords, withoutMarkup
|
|
5
|
+
} from "@jskit-ai/agent/server";
|
|
1
6
|
import { conversationRequestText } from "../continuity.js";
|
|
2
7
|
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
3
|
-
import { consumeCompletionStream, sanitizeAssistantMessageText } from "../../lib/assistantCompletion.js";
|
|
4
|
-
import { runAssistantToolLoop } from "../../lib/assistantToolLoop.js";
|
|
5
|
-
import { isAssistantProgressOnlyText } from "../../../shared/assistantResponseText.js";
|
|
6
8
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
const
|
|
9
|
+
// The agent engine's account of a reply that stopped early, and its checks, speak for a
|
|
10
|
+
// generic assistant in an app.
|
|
11
|
+
const NAME = "the assistant";
|
|
12
|
+
const APP = "the app";
|
|
13
|
+
const CHECKS = checkTexts(APP);
|
|
14
|
+
// The engine's step limit for work with app tools; a reply without tools is one step anyway.
|
|
15
|
+
const MAX_STEPS = 40;
|
|
16
|
+
// The engine's sizes are for a window of a million tokens (WINDOW_TOKENS). Many catalogue
|
|
17
|
+
// models (DeepSeek's chat models, Groq's, GPT-4o) have 128k: sized for 1M, their requests
|
|
18
|
+
// would fail long before compaction began, while sized for 128k a larger window only
|
|
19
|
+
// compacts sooner. The former character cap was about 64k tokens of history.
|
|
20
|
+
const CONTEXT_TOKENS = 128_000;
|
|
21
|
+
const OUTPUT_LIMIT = "The assistant exceeded the configured response limit.";
|
|
22
|
+
const PROGRESS_LIMIT = "Application tool progress text exceeds the configured API limit.";
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
// The model sees an action's result, or its error. The durable receipt keeps
|
|
25
|
+
// its full ok/result/error envelope in common storage.
|
|
26
|
+
const toolOutput = result => result.ok ? result.result ?? null : { error: result.error };
|
|
27
|
+
|
|
28
|
+
/** A turn whose caller follows no phase events. */
|
|
29
|
+
function ignoreEvent() {}
|
|
30
|
+
|
|
31
|
+
// The engine's state note: what the system knows for certain this turn. It carries
|
|
32
|
+
// the time, so no turn needs an extra action to learn it.
|
|
33
|
+
function stateNote() {
|
|
34
|
+
const now = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "short", timeZone: "UTC" }).format(new Date());
|
|
35
|
+
return ["Current state, from the system. It is the truth; earlier messages may be out of date:",
|
|
36
|
+
`- The time: ${now} UTC. The person may be in another time zone.`].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const content = message => message.content?.length
|
|
40
|
+
? [...(message.text ? [{ type: "text", text: message.text }] : []), ...message.content] : message.text;
|
|
41
|
+
|
|
42
|
+
// A recorded action's receipt id is the provider's call id under its step (`reply-N:`). The
|
|
43
|
+
// model reads the provider's own id, as it does within a reply: providers restrict their ids
|
|
44
|
+
// (Anthropic's take only letters, digits, "_" and "-"; Mistral's are nine characters).
|
|
45
|
+
const providerCallId = id => id.replace(/^reply-\d+:/u, "");
|
|
46
|
+
|
|
47
|
+
// A saved turn as model messages: the authored request, its recorded application
|
|
48
|
+
// actions with their results (long old results cut, as the engine reads them), the answer.
|
|
49
|
+
function turnMessages(turn) {
|
|
50
|
+
return [
|
|
51
|
+
...(turn.user ? [{ role: "user", content: content({ ...turn.user, text: conversationRequestText(turn.user) }) }] : []),
|
|
52
|
+
...(turn.system ? [{ role: "user", content: content({ ...turn.system, text: conversationRequestText({ ...turn.system, origin: "application" }) }) }] : []),
|
|
53
|
+
...(turn.metadata?.applicationTools || []).filter(call => call.result).flatMap(call => [
|
|
54
|
+
{ role: "assistant", content: [{ type: "tool-call", toolCallId: providerCallId(call.id), toolName: call.name,
|
|
55
|
+
input: call.arguments ? JSON.parse(call.arguments) : {} }] },
|
|
56
|
+
clipOldResult({ role: "tool", content: [{ type: "tool-result", toolCallId: providerCallId(call.id), toolName: call.name,
|
|
57
|
+
output: { type: "json", value: toolOutput(call.result) } }] })
|
|
58
|
+
]),
|
|
59
|
+
...(turn.assistant ? [{ role: "assistant", content: turn.assistant.text }] : [])
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Direct API inference runs on the agent engine with the same authorized connection catalogue as other JSKIT services.
|
|
65
|
+
* The application passes the conversation runtime its authorized AI connection resolver, or its own API client factory.
|
|
66
|
+
* @param {object} dependencies
|
|
67
|
+
* @param {{ resolve(request: { context: object, integrationId: string }): Promise<object> }} [dependencies.connections]
|
|
68
|
+
* @param {{ resolveClient(request: { context: object, configuration: object }): Promise<{ enabled: boolean, model: object, defaultModel: string }> }} [dependencies.apiClientFactory]
|
|
69
|
+
* @param {typeof fetch} [dependencies.fetch]
|
|
70
|
+
* @param {Record<string, number>} [dependencies.limits]
|
|
71
|
+
*/
|
|
12
72
|
export function createApiConversationDriver({ connections, apiClientFactory, fetch, limits = {} }) {
|
|
13
|
-
|
|
73
|
+
// readable-check: allow method-probe - input validation: the API client factory an application passes the conversation runtime
|
|
74
|
+
if (apiClientFactory !== undefined && (!apiClientFactory || typeof apiClientFactory.resolveClient !== "function")) {
|
|
14
75
|
throw new TypeError("An API client factory requires resolveClient({ context, configuration }).");
|
|
15
76
|
}
|
|
16
|
-
|
|
77
|
+
// readable-check: allow method-probe - input validation: the AI connection resolver an application passes the conversation runtime
|
|
78
|
+
if (!apiClientFactory && (!connections || typeof connections.resolve !== "function")) {
|
|
79
|
+
throw new TypeError("API conversations require an authorized AI connection resolver.");
|
|
80
|
+
}
|
|
17
81
|
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
18
|
-
const maximumHistory = limits.maxHistoryCharacters ?? 256_000;
|
|
19
82
|
const maximumToolProgress = limits.maxApiToolProgressCharacters;
|
|
20
|
-
|
|
21
|
-
|
|
83
|
+
// The runtime ends a turn with an error past its application action-call limit (32 by
|
|
84
|
+
// default), so the step limit stays within it and a long turn ends with the model's account.
|
|
85
|
+
const maximumSteps = limits.maxSteps ?? Math.min(MAX_STEPS, limits.maxToolCalls ?? 32);
|
|
86
|
+
const contextTokens = limits.contextTokens ?? CONTEXT_TOKENS;
|
|
87
|
+
for (const limit of [maximumOutput, maximumSteps, contextTokens]) {
|
|
22
88
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError("Invalid API conversation limit.");
|
|
23
89
|
}
|
|
24
90
|
if (maximumToolProgress !== undefined && (!Number.isSafeInteger(maximumToolProgress) || maximumToolProgress < 1)) {
|
|
25
91
|
throw new TypeError("Invalid API application tool progress limit.");
|
|
26
92
|
}
|
|
93
|
+
const window = windowLimits(contextTokens);
|
|
27
94
|
const driver = Object.freeze({
|
|
28
95
|
admissionBeforeDispatch: true,
|
|
29
96
|
attachmentTypes: ["text", "image", "file"],
|
|
@@ -34,110 +101,204 @@ export function createApiConversationDriver({ connections, apiClientFactory, fet
|
|
|
34
101
|
validateConversationConfiguration(configuration, { engine: "API", connections, apiClientFactory, connectionRequired: true,
|
|
35
102
|
efforts: ["none", "minimal", "low", "medium", "high", "xhigh"] });
|
|
36
103
|
},
|
|
37
|
-
|
|
104
|
+
// `summary` is the stored summary of the turns before `history`; `saveSummary`
|
|
105
|
+
// stores a new one, so the next turn reads only the turns after it.
|
|
106
|
+
async run({ configuration, context, input, history, requestHistory, continuity, tools, signal, accept, beforeDispatch,
|
|
107
|
+
onMessage, onEvent = ignoreEvent, summary = "", saveSummary }) {
|
|
38
108
|
const { createAiConnectionClient } = await import("../../lib/aiConnectionClient.js");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{ role: "tool", tool_call_id: call.id, content: toolResultContent(call.result) }
|
|
50
|
-
]),
|
|
51
|
-
...(turn.assistant ? [{ role: "assistant", content: turn.assistant.text }] : [])
|
|
52
|
-
])),
|
|
53
|
-
{ role: "user", content: content(input) }
|
|
54
|
-
];
|
|
55
|
-
let accepted = false;
|
|
56
|
-
let round = 0;
|
|
57
|
-
let responseId;
|
|
58
|
-
let responseIsCommentary = false;
|
|
59
|
-
async function prepareClient(inferenceMessages = messages) {
|
|
60
|
-
if (inferenceMessages.reduce((size, message) => size + (typeof message.content === "string" ? message.content.length
|
|
61
|
-
: message.content.reduce((count, part) => count + (part.type === "text" ? part.text.length : 0), 0)) +
|
|
62
|
-
(message.tool_calls ? JSON.stringify(message.tool_calls).length : 0), 0) > maximumHistory) {
|
|
63
|
-
throw new Error("This conversation exceeds the configured API history limit. Start another conversation or increase the limit.");
|
|
64
|
-
}
|
|
65
|
-
let client;
|
|
109
|
+
// A broken receipt, a lost authorization or a limit ends the reply with its
|
|
110
|
+
// own error: the engine ends at once with an aborted signal's reason.
|
|
111
|
+
const stop = new AbortController();
|
|
112
|
+
const stopped = AbortSignal.any([signal, stop.signal]);
|
|
113
|
+
const fail = error => {
|
|
114
|
+
if (!stop.signal.aborted) stop.abort(error);
|
|
115
|
+
return stop.signal.reason;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
async function resolveModel() {
|
|
66
119
|
if (apiClientFactory) {
|
|
67
|
-
client = await apiClientFactory.resolveClient({ context, configuration });
|
|
68
|
-
if (!client?.enabled ||
|
|
69
|
-
throw new Error("Assistant provider is not configured.");
|
|
70
|
-
}
|
|
120
|
+
const client = await apiClientFactory.resolveClient({ context, configuration });
|
|
121
|
+
if (!client?.enabled || !client.model) throw new Error("Assistant provider is not configured.");
|
|
71
122
|
validateConnectionModel(configuration, { model: client.defaultModel });
|
|
72
|
-
|
|
73
|
-
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
74
|
-
validateConnectionModel(configuration, connection);
|
|
75
|
-
client = createAiConnectionClient(connection, { fetch, effort: configuration.effort,
|
|
76
|
-
reportUnavailableToolCalls: Boolean(tools),
|
|
77
|
-
maxOutputTokens: limits.maxOutputTokens, timeoutMs: limits.timeoutMs });
|
|
123
|
+
return client.model;
|
|
78
124
|
}
|
|
79
|
-
|
|
125
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
126
|
+
validateConnectionModel(configuration, connection);
|
|
127
|
+
return createAiConnectionClient(connection, { fetch }).model;
|
|
128
|
+
}
|
|
129
|
+
async function authorize() {
|
|
130
|
+
const model = await resolveModel();
|
|
131
|
+
stopped.throwIfAborted();
|
|
80
132
|
await beforeDispatch();
|
|
81
|
-
|
|
82
|
-
return client;
|
|
133
|
+
return model;
|
|
83
134
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
|
|
136
|
+
// Admission precedes every model call. Each later call (steps, checks,
|
|
137
|
+
// summaries, accounts) authorizes the same connection again. Reasoning a
|
|
138
|
+
// model writes inline in <think> tags is reasoning, never reply text.
|
|
139
|
+
// `stepWords` are the streaming step's words so far, without that reasoning: its
|
|
140
|
+
// actions run only once its model call has ended, so they are all said by then.
|
|
141
|
+
let admitted = false;
|
|
142
|
+
let stepWords = "";
|
|
143
|
+
const model = wrapLanguageModel({ model: await authorize(), middleware: [
|
|
144
|
+
{ specificationVersion: "v4", async transformParams({ params }) {
|
|
145
|
+
if (admitted) await authorize().catch(error => { throw fail(error); });
|
|
146
|
+
admitted = true;
|
|
147
|
+
return params;
|
|
148
|
+
}, async wrapStream({ doStream }) {
|
|
149
|
+
const result = await doStream();
|
|
150
|
+
stepWords = "";
|
|
151
|
+
return { ...result, stream: result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
|
|
152
|
+
if (part.type === "text-delta") stepWords += part.delta;
|
|
153
|
+
controller.enqueue(part);
|
|
154
|
+
} })) };
|
|
155
|
+
} },
|
|
156
|
+
extractReasoningMiddleware({ tagName: "think" }),
|
|
157
|
+
...(configuration.effort ? [defaultSettingsMiddleware({ settings: { reasoning: configuration.effort } })] : [])
|
|
158
|
+
] });
|
|
159
|
+
await accept();
|
|
160
|
+
|
|
161
|
+
let step = 0;
|
|
162
|
+
const repeats = {};
|
|
163
|
+
async function runTool(call) {
|
|
164
|
+
// A step's progress is shown and held to its limits before any of its actions
|
|
165
|
+
// runs. Another action after an unknown outcome could repeat it.
|
|
166
|
+
try {
|
|
167
|
+
await stepProgress();
|
|
168
|
+
return await tools.execute(call);
|
|
169
|
+
} catch (error) { throw fail(error); }
|
|
170
|
+
}
|
|
171
|
+
/** One engine tool call, recorded under its step, with the model-facing result. */
|
|
172
|
+
function runEngineTool(name, value, { toolCallId }) {
|
|
173
|
+
return runTool({
|
|
174
|
+
id: `reply-${step + 1}:${toolCallId}`, name, arguments: JSON.stringify(value ?? {}) }).then(toolOutput);
|
|
175
|
+
}
|
|
176
|
+
const engineTools = Object.fromEntries((tools?.schemas || []).map(({ function: tool }) => {
|
|
177
|
+
// The engine's copy of the frozen definition is the one its repeat reminder wraps.
|
|
178
|
+
const engineTool = createTool(Object.freeze({ id: tool.name, description: tool.description, parameters: tool.parameters,
|
|
179
|
+
execute: (value, options) => runEngineTool(tool.name, value, options) }));
|
|
180
|
+
remindOnRepeat(engineTool, repeats);
|
|
181
|
+
return [tool.name, engineTool];
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const request = { role: "user", content: content(input) };
|
|
185
|
+
let earlier = summary;
|
|
186
|
+
let recorded = requestHistory ?? history.flatMap(turnMessages);
|
|
187
|
+
// The runtime's own history is compacted between turns; the summary and
|
|
188
|
+
// its last turn are stored, so later turns read only what follows.
|
|
189
|
+
if (!requestHistory && saveSummary) {
|
|
190
|
+
let started = false;
|
|
191
|
+
try {
|
|
192
|
+
const compacted = await compactMessages({ model, app: APP, messages: recorded, previous: summary, limits: window.summaries,
|
|
193
|
+
onCompacting: async () => { started = true; await onEvent({ type: "phase", phase: "compacting" }); } });
|
|
194
|
+
if (compacted.saved) {
|
|
195
|
+
// The summary ends where a turn starts.
|
|
196
|
+
let covered = 0;
|
|
197
|
+
let throughTurnId = "";
|
|
198
|
+
for (const turn of history) {
|
|
199
|
+
const size = turnMessages(turn).length;
|
|
200
|
+
if (covered + size > compacted.saved.covers) break;
|
|
201
|
+
covered += size;
|
|
202
|
+
throughTurnId = turn.turnId;
|
|
131
203
|
}
|
|
204
|
+
await saveSummary({ text: compacted.summary, throughTurnId });
|
|
205
|
+
({ summary: earlier, messages: recorded } = compacted);
|
|
132
206
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await onMessage({ id, outputId: id, role: "assistant", text, complete: true });
|
|
138
|
-
return { metadata };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
stopped.throwIfAborted();
|
|
209
|
+
// The engine's last-resort cut below still keeps the reply within its window.
|
|
210
|
+
console.error(`Compacting a conversation: ${error?.message || error}`);
|
|
139
211
|
}
|
|
140
|
-
|
|
212
|
+
if (started) await onEvent({ type: "phase", phase: "working" });
|
|
213
|
+
}
|
|
214
|
+
const messages = withinBudget([...(continuity ? [{ role: "user", content: continuity }] : []), ...recorded, request], window.history);
|
|
215
|
+
|
|
216
|
+
// Each step streams as reply-N. A step that also calls tools ends as
|
|
217
|
+
// commentary; the reply's last words become the answer.
|
|
218
|
+
// Output past its limit stops showing at once and fails the reply before the
|
|
219
|
+
// step's actions run, or when it ends: aborting a stream mid-way leaves Node 26
|
|
220
|
+
// an unhandled rejection.
|
|
221
|
+
let answer = "";
|
|
222
|
+
let overLimit = false;
|
|
223
|
+
// The engine's reply joins every step's words; the answer leaves out those
|
|
224
|
+
// already shown as commentary, unless they are all there is.
|
|
225
|
+
const shown = [];
|
|
226
|
+
const unsaid = text => shown.reduce((rest, said) => rest === said ? ""
|
|
227
|
+
: rest.startsWith(`${said}\n\n`) ? rest.slice(said.length + 2) : rest, text) || text;
|
|
228
|
+
// A step whose actions run completes as commentary before the first of them;
|
|
229
|
+
// one whose calls don't run (an action it wasn't given) does when it ends.
|
|
230
|
+
let progress = { id: "", words: "", shown: null };
|
|
231
|
+
function stepProgress() {
|
|
232
|
+
const id = `reply-${step + 1}`;
|
|
233
|
+
if (progress.id !== id) progress = { id, words: stepWords, shown: showProgress(id, stepWords) };
|
|
234
|
+
return progress.shown;
|
|
235
|
+
}
|
|
236
|
+
async function showProgress(id, words) {
|
|
237
|
+
if (words.length > maximumOutput) throw new Error(OUTPUT_LIMIT);
|
|
238
|
+
const text = withoutMarkup(words);
|
|
239
|
+
if (maximumToolProgress !== undefined && text.length > maximumToolProgress) throw new Error(PROGRESS_LIMIT);
|
|
240
|
+
if (answer === id) answer = "";
|
|
241
|
+
if (text) await onMessage({ id, outputId: id, role: "commentary", text, complete: true });
|
|
242
|
+
}
|
|
243
|
+
async function onText(text) {
|
|
244
|
+
overLimit ||= text.length > maximumOutput;
|
|
245
|
+
const visible = withoutMarkup(text);
|
|
246
|
+
// Words streamed after their step's progress was shown are already in it.
|
|
247
|
+
if (!visible || overLimit || progress.id === `reply-${step + 1}`) return;
|
|
248
|
+
answer = `reply-${step + 1}`;
|
|
249
|
+
await onMessage({ id: answer, outputId: answer, role: "assistant", text: visible, complete: false });
|
|
250
|
+
}
|
|
251
|
+
async function onStep(result) {
|
|
252
|
+
const id = `reply-${++step}`;
|
|
253
|
+
if (overLimit) return fail(new Error(OUTPUT_LIMIT));
|
|
254
|
+
if (result.finishReason === "length") return fail(new Error("The model reached its output token limit before completing the answer. Increase maxOutputTokens in the conversation limits."));
|
|
255
|
+
if (!result.toolCalls.length) return;
|
|
256
|
+
const text = withoutMarkup(result.text);
|
|
257
|
+
if (maximumToolProgress !== undefined && text.length > maximumToolProgress) return fail(new Error(PROGRESS_LIMIT));
|
|
258
|
+
if (answer === id) answer = "";
|
|
259
|
+
if (!text) return;
|
|
260
|
+
shown.push(...result.content.filter(part => part.type === "text").map(part => part.text.trim()).filter(Boolean));
|
|
261
|
+
if (progress.id === id && progress.words === result.text) return;
|
|
262
|
+
await onMessage({ id, outputId: id, role: "commentary", text, complete: true });
|
|
263
|
+
}
|
|
264
|
+
const pass = async history => {
|
|
265
|
+
const result = await runLoop({ model, name: NAME, instructions: configuration.systemPrompt, history, tools: engineTools,
|
|
266
|
+
summary: earlier, state: stateNote(), maxSteps: maximumSteps, onText, onStep, signal: stopped, clean: text => stripNotes(unsaid(text)),
|
|
267
|
+
// With nothing done yet, a failed call is the reply's error, not an account.
|
|
268
|
+
throwIf: (_error, steps) => !step && !steps.length,
|
|
269
|
+
// A configured effort reaches every provider the standard way.
|
|
270
|
+
reasoning: configuration.effort ? {} : REASONING.on,
|
|
271
|
+
...(limits.maxOutputTokens ? { maxOutputTokens: limits.maxOutputTokens } : {}) });
|
|
272
|
+
stopped.throwIfAborted();
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let { text, transcript, unfinished, account } = await pass(messages);
|
|
277
|
+
// A check sends the reply back once, carrying on from its own steps.
|
|
278
|
+
let check = "";
|
|
279
|
+
async function carryOn(name, note) {
|
|
280
|
+
check = name;
|
|
281
|
+
const steps = withoutClosingWords(transcript);
|
|
282
|
+
const next = await pass([...messages, ...steps.map(clipOldResult), { role: "user", content: note }]);
|
|
283
|
+
({ text, unfinished } = next);
|
|
284
|
+
transcript = [...steps, ...next.transcript];
|
|
285
|
+
}
|
|
286
|
+
const working = Object.keys(engineTools).length > 0;
|
|
287
|
+
if (working && LEAKED_MARKUP.test(text)) await carryOn("markup", CHECKS.markup);
|
|
288
|
+
// The engine's own account of a reply that stopped early already says where it got to.
|
|
289
|
+
else if (working && !account && stoppedShort(text.trim())) await carryOn("stoppedShort", CHECKS.stoppedShort);
|
|
290
|
+
if (!check && working && reportsActions(text)) {
|
|
291
|
+
const claims = await unsupportedClaims({ model, reply: text, record: toolRecord(transcript, [], APP) });
|
|
292
|
+
stopped.throwIfAborted();
|
|
293
|
+
if (claims.length) await carryOn("unsupported", CHECKS.unsupported(claims));
|
|
294
|
+
}
|
|
295
|
+
text = withoutMarkup(text);
|
|
296
|
+
if (text.length > maximumOutput) throw new Error(OUTPUT_LIMIT);
|
|
297
|
+
if (text) {
|
|
298
|
+
const id = answer || `reply-${step + 1}`;
|
|
299
|
+
await onMessage({ id, outputId: id, role: "assistant", text, complete: true });
|
|
300
|
+
}
|
|
301
|
+
return { metadata: { ...(unfinished ? { unfinished } : {}), ...(check ? { check } : {}) } };
|
|
141
302
|
}
|
|
142
303
|
});
|
|
143
304
|
return driver;
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { createConversationSystemPrompt } from "../systemPrompt.js";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("../claudeProcess.js").ClaudeCodeProcess} ClaudeCodeProcess
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The conversation owner's control of its one native process.
|
|
9
|
+
* @typedef {object} ClaudeProcessControl
|
|
10
|
+
* @property {() => ClaudeCodeProcess | null} getProcess
|
|
11
|
+
* @property {() => boolean} isActive
|
|
12
|
+
* @property {(input: object) => Promise<ClaudeCodeProcess>} startProcess
|
|
13
|
+
* @property {(reason: string) => Promise<unknown>} stopProcess
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One instance per native Claude conversation; process ownership stays with the host.
|
|
18
|
+
* @param {ClaudeProcessControl} control
|
|
19
|
+
*/
|
|
20
|
+
export function createClaudeConversationAdapter({ getProcess, isActive, startProcess, stopProcess }) {
|
|
8
21
|
const instructions = createConversationSystemPrompt();
|
|
9
22
|
let pending = Promise.resolve();
|
|
10
23
|
let updateIdentity;
|
|
@@ -63,5 +76,15 @@ export function createClaudeConversationAdapter({ getProcess, isActive, startPro
|
|
|
63
76
|
return operation;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
/** The latest queued preparation. */
|
|
80
|
+
function whenIdle() {
|
|
81
|
+
return pending;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
prepare,
|
|
86
|
+
instructionArguments,
|
|
87
|
+
invalidate: instructions.invalidate,
|
|
88
|
+
whenIdle
|
|
89
|
+
});
|
|
67
90
|
}
|