@lamplitisles/dsh-nanocodex 0.1.0-beta.0
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/LICENSE +65 -0
- package/NOTICE +2 -0
- package/README.md +182 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +52 -0
- package/dist/index.d.ts +435 -0
- package/dist/index.js +3405 -0
- package/dist/standard-preset.d.ts +7 -0
- package/dist/standard-preset.js +25 -0
- package/engine-release.json +14 -0
- package/package.json +119 -0
- package/presets/standard/agent.cordis.yml +6 -0
- package/presets/standard/preset.yml +2 -0
- package/vendor/nanocodex/README.md +1344 -0
- package/vendor/nanocodex/actions/events.d.mts +10 -0
- package/vendor/nanocodex/actions/events.mjs +163 -0
- package/vendor/nanocodex/actions/index.d.mts +13 -0
- package/vendor/nanocodex/actions/index.mjs +35 -0
- package/vendor/nanocodex/actions/session.d.mts +66 -0
- package/vendor/nanocodex/actions/session.mjs +59 -0
- package/vendor/nanocodex/actions/turn.d.mts +59 -0
- package/vendor/nanocodex/actions/turn.mjs +37 -0
- package/vendor/nanocodex/actions/voice.d.mts +22 -0
- package/vendor/nanocodex/actions/voice.mjs +50 -0
- package/vendor/nanocodex/browser/Agent.d.mts +55 -0
- package/vendor/nanocodex/browser/Agent.mjs +11 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
- package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
- package/vendor/nanocodex/browser/Transport.d.mts +74 -0
- package/vendor/nanocodex/browser/Transport.mjs +61 -0
- package/vendor/nanocodex/browser/Voice.d.mts +71 -0
- package/vendor/nanocodex/browser/Voice.mjs +291 -0
- package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
- package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
- package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
- package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
- package/vendor/nanocodex/browser/config.d.mts +36 -0
- package/vendor/nanocodex/browser/config.mjs +439 -0
- package/vendor/nanocodex/browser/engine.mjs +13 -0
- package/vendor/nanocodex/browser/harness.mjs +55 -0
- package/vendor/nanocodex/browser/host.d.mts +83 -0
- package/vendor/nanocodex/browser/host.mjs +555 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
- package/vendor/nanocodex/browser/index.d.mts +92 -0
- package/vendor/nanocodex/browser/index.mjs +23 -0
- package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
- package/vendor/nanocodex/browser/workspace.d.mts +16 -0
- package/vendor/nanocodex/browser/workspace.mjs +107 -0
- package/vendor/nanocodex/cloud/Client.d.mts +83 -0
- package/vendor/nanocodex/cloud/Client.mjs +231 -0
- package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
- package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
- package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
- package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
- package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
- package/vendor/nanocodex/cloud/Errors.mjs +39 -0
- package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
- package/vendor/nanocodex/cloud/Principal.mjs +88 -0
- package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
- package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
- package/vendor/nanocodex/cloud/Transport.mjs +454 -0
- package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
- package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
- package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
- package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
- package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
- package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
- package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
- package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
- package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
- package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
- package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
- package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
- package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
- package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
- package/vendor/nanocodex/cloud/index.d.mts +30 -0
- package/vendor/nanocodex/cloud/index.mjs +9 -0
- package/vendor/nanocodex/cloud/internal.mjs +533 -0
- package/vendor/nanocodex/cloud/mercator.mjs +108 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
- package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
- package/vendor/nanocodex/cloud/server/index.mjs +1 -0
- package/vendor/nanocodex/cloud/types.d.mts +211 -0
- package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
- package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
- package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
- package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
- package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
- package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
- package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
- package/vendor/nanocodex/cloudflare/index.mjs +6 -0
- package/vendor/nanocodex/host/Agent.d.mts +55 -0
- package/vendor/nanocodex/host/Agent.mjs +1 -0
- package/vendor/nanocodex/host/index.d.mts +82 -0
- package/vendor/nanocodex/host/index.mjs +10 -0
- package/vendor/nanocodex/index.d.mts +81 -0
- package/vendor/nanocodex/index.mjs +12 -0
- package/vendor/nanocodex/internal.mjs +1275 -0
- package/vendor/nanocodex/managed/Agent.d.mts +380 -0
- package/vendor/nanocodex/managed/Agent.mjs +1694 -0
- package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
- package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
- package/vendor/nanocodex/managed/README.md +241 -0
- package/vendor/nanocodex/managed/Voice.mjs +202 -0
- package/vendor/nanocodex/managed/index.d.mts +20 -0
- package/vendor/nanocodex/managed/index.mjs +2 -0
- package/vendor/nanocodex/managed/internal.mjs +119 -0
- package/vendor/nanocodex/node/Agent.d.mts +50 -0
- package/vendor/nanocodex/node/Agent.mjs +203 -0
- package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
- package/vendor/nanocodex/node/Transport.d.mts +44 -0
- package/vendor/nanocodex/node/Transport.mjs +46 -0
- package/vendor/nanocodex/node/host.mjs +731 -0
- package/vendor/nanocodex/node/index.d.mts +73 -0
- package/vendor/nanocodex/node/index.mjs +17 -0
- package/vendor/nanocodex/node/workspace.d.mts +10 -0
- package/vendor/nanocodex/node/workspace.mjs +198 -0
- package/vendor/nanocodex/package.json +200 -0
- package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
- package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
- package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-node/package.json +1 -0
- package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
- package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
- package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
- package/vendor/nanocodex/pkg-web/package.json +1 -0
- package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
- package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
- package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
- package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
- package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
- package/vendor/nanocodex/runtime/durability.mjs +153 -0
- package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
- package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
- package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
- package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
- package/vendor/nanocodex/runtime/subagents.mjs +54 -0
- package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
- package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
- package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
- package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
- package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
- package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
- package/vendor/nanocodex/runtime/utf8.mjs +1 -0
- package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
- package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
- package/vendor/nanocodex/runtime/workspace.mjs +1 -0
- package/vendor/nanocodex/tools/Tools.d.mts +56 -0
- package/vendor/nanocodex/tools/Tools.mjs +136 -0
- package/vendor/nanocodex/tools/artifact.d.mts +1 -0
- package/vendor/nanocodex/tools/artifact.mjs +1 -0
- package/vendor/nanocodex/tools/attachment.mjs +1 -0
- package/vendor/nanocodex/tools/bash.d.mts +1 -0
- package/vendor/nanocodex/tools/bash.mjs +1 -0
- package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
- package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
- package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
- package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
- package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
- package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
- package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
- package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
- package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
- package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
- package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
- package/vendor/nanocodex/tools/browser/index.mjs +174 -0
- package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
- package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
- package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
- package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
- package/vendor/nanocodex/tools/dataset.d.mts +1 -0
- package/vendor/nanocodex/tools/dataset.mjs +1 -0
- package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
- package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
- package/vendor/nanocodex/tools/index.d.mts +49 -0
- package/vendor/nanocodex/tools/index.mjs +19 -0
- package/vendor/nanocodex/tools/namedTool.mjs +1 -0
- package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
- package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
- package/vendor/nanocodex/tools/ssh.d.mts +1 -0
- package/vendor/nanocodex/tools/ssh.mjs +1 -0
- package/vendor/nanocodex/tools/standard.mjs +1 -0
- package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
- package/vendor/nanocodex/types.d.mts +663 -0
- package/vendor/nanocodex/wasm.d.mts +4 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
- package/vendor/nanocodex/worker/index.d.mts +18 -0
- package/vendor/nanocodex/worker/index.mjs +5 -0
- package/vendor/nanocodex-tools/README.md +65 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
- package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
- package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
- package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
- package/vendor/nanocodex-tools/dist/index.js +14 -0
- package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
- package/vendor/nanocodex-tools/dist/memory.js +411 -0
- package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
- package/vendor/nanocodex-tools/dist/namespace.js +338 -0
- package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
- package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
- package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
- package/vendor/nanocodex-tools/dist/runtime.js +37 -0
- package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
- package/vendor/nanocodex-tools/dist/session.js +286 -0
- package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
- package/vendor/nanocodex-tools/dist/shell.js +553 -0
- package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
- package/vendor/nanocodex-tools/dist/ssh.js +66 -0
- package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
- package/vendor/nanocodex-tools/dist/workspace.js +237 -0
- package/vendor/nanocodex-tools/package.json +122 -0
- package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
- package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
- package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
- package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
- package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
- package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
- package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
- package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
- package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
- package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
- package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
- package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
- package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
- package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
- package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
- package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
- package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
- package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
- package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
- package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
- package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
- package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
- package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
- package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
- package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
- package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
- package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
- package/vendor/nanocodex-tools/tools/x.mjs +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3405 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { CompactionEngine, CompactionId, ManualCompactionError, compactCheckpointSource, isCompactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore } from "@deepseek-ai/dsh-compaction";
|
|
3
|
+
import { LlmAdapter, ReasoningEffortId, ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { SessionPreparation, canonicalHeader, headerEquals } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { joinContextSections, renderContextSections, renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
9
|
+
import { Agent, Transport, createQuickJsEvaluator } from "../vendor/nanocodex/node/index.mjs";
|
|
10
|
+
import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core";
|
|
11
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
12
|
+
import z$1 from "@deepseek-ai/schemastery";
|
|
13
|
+
import { createScope, scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
14
|
+
import { admitPromptContent } from "@deepseek-ai/dsh-attachment";
|
|
15
|
+
import { Inbox, agentEvents, assembleContextFor, emitAgentEvent } from "@deepseek-ai/dsh-agent";
|
|
16
|
+
import { apply as apply$1 } from "@deepseek-ai/dsh-command-compact";
|
|
17
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
18
|
+
import "@deepseek-ai/dsh-tools";
|
|
19
|
+
//#region src/constants.ts
|
|
20
|
+
const INTERRUPTED_TOOL_OUTPUT = "Tool execution was interrupted before a complete result was recorded. It may have had side effects; inspect the current state before retrying.";
|
|
21
|
+
const PLUGIN_NAME = "dsh-nanocodex";
|
|
22
|
+
const APPLY_PATCH_NAME = "apply_patch";
|
|
23
|
+
const APPLY_PATCH_DESCRIPTION = "Apply a raw Codex patch to text files in the active workspace. Call apply_patch(input: string) with a complete Add File/Update File patch. Supports Add File and Update File with contextual hunks; Delete File and Move to are not supported.";
|
|
24
|
+
const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch hunk+ end_patch
|
|
25
|
+
begin_patch: "*** Begin Patch" LF
|
|
26
|
+
end_patch: "*** End Patch" LF?
|
|
27
|
+
hunk: add_hunk | update_hunk
|
|
28
|
+
add_hunk: "*** Add File: " filename LF add_line+
|
|
29
|
+
update_hunk: "*** Update File: " filename LF change+
|
|
30
|
+
filename: /(.+)/
|
|
31
|
+
add_line: "+" /(.*)/ LF
|
|
32
|
+
change: (change_context | change_line)+ eof_line?
|
|
33
|
+
change_context: ("@@" | "@@ " /(.+)/) LF
|
|
34
|
+
change_line: ("+" | "-" | " ") /(.*)/ LF
|
|
35
|
+
eof_line: "*** End of File" LF
|
|
36
|
+
%import common.LF`;
|
|
37
|
+
const DEFAULT_MAX_PATCH_CHARS = 4e6;
|
|
38
|
+
const DEFAULT_MAX_PATCH_FILES = 64;
|
|
39
|
+
const DEFAULT_MAX_PATCH_FILE_BYTES = 4e6;
|
|
40
|
+
const MODEL_CONTEXT_WINDOW = 2e5;
|
|
41
|
+
const SUPPORTED_MODELS = [
|
|
42
|
+
"gpt-5.6-sol",
|
|
43
|
+
"gpt-5.6-terra",
|
|
44
|
+
"gpt-5.6-luna",
|
|
45
|
+
"gpt-6-astra"
|
|
46
|
+
];
|
|
47
|
+
function isSupportedModel(value) {
|
|
48
|
+
return value !== void 0 && SUPPORTED_MODELS.includes(value);
|
|
49
|
+
}
|
|
50
|
+
function isSupportedRoute(provider, api) {
|
|
51
|
+
return (provider === "openai" || provider === "openai-codex-responses") && (api === "openai-responses" || api === "openai");
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/history.ts
|
|
55
|
+
function imageDataUrl(ref, bytes) {
|
|
56
|
+
return `data:${ref.mediaType};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
57
|
+
}
|
|
58
|
+
async function imageItem(ctx, ref, signal) {
|
|
59
|
+
const stored = await ctx.attachments.readImage(ref, signal);
|
|
60
|
+
return {
|
|
61
|
+
type: "input_image",
|
|
62
|
+
image_url: imageDataUrl(stored.ref, stored.data),
|
|
63
|
+
detail: "auto"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function outputText(block) {
|
|
67
|
+
if (block.type !== "text") throw new Error(`Nanocodex cannot hydrate assistant content block ${JSON.stringify(block.type)}`);
|
|
68
|
+
return {
|
|
69
|
+
type: "output_text",
|
|
70
|
+
text: block.text
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function inputText(block) {
|
|
74
|
+
if (block.type !== "text") throw new Error(`Nanocodex cannot hydrate user content block ${JSON.stringify(block.type)}`);
|
|
75
|
+
return {
|
|
76
|
+
type: "input_text",
|
|
77
|
+
text: block.text
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function toolOutput(ctx, blocks, signal) {
|
|
81
|
+
if (blocks.length === 1 && blocks[0]?.type === "text") return blocks[0].text;
|
|
82
|
+
const output = [];
|
|
83
|
+
for (const block of blocks) {
|
|
84
|
+
if (block.type !== "text") {
|
|
85
|
+
if (block.type === "image") {
|
|
86
|
+
output.push(await imageItem(ctx, block.attachment, signal));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Nanocodex cannot hydrate tool-result content block ${JSON.stringify(block.type)}`);
|
|
90
|
+
}
|
|
91
|
+
output.push({
|
|
92
|
+
type: "input_text",
|
|
93
|
+
text: block.text
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return output;
|
|
97
|
+
}
|
|
98
|
+
function textOfCheckpoint(message) {
|
|
99
|
+
return message.content.map((block) => {
|
|
100
|
+
if (block.type !== "text") throw new Error(`Nanocodex cannot hydrate compaction checkpoint block ${JSON.stringify(block.type)}`);
|
|
101
|
+
return block.text;
|
|
102
|
+
}).join("\n");
|
|
103
|
+
}
|
|
104
|
+
/** Pi Responses IDs store call_id|item_id; only the call part pairs results. */
|
|
105
|
+
function historyToolCallId(value) {
|
|
106
|
+
return value.split("|", 1)[0].replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64).replace(/_+$/u, "");
|
|
107
|
+
}
|
|
108
|
+
function rawCustomInput(name, argumentsText) {
|
|
109
|
+
const key = name === "apply_patch" ? "patch" : "code";
|
|
110
|
+
let value;
|
|
111
|
+
try {
|
|
112
|
+
value = JSON.parse(argumentsText);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new Error(`Nanocodex cannot hydrate an invalid ${name} call`, { cause: error });
|
|
115
|
+
}
|
|
116
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value[key] !== "string" || Object.keys(value).length !== 1) throw new Error(`Nanocodex cannot hydrate ${name} without canonical {${key}} arguments`);
|
|
117
|
+
return value[key];
|
|
118
|
+
}
|
|
119
|
+
function validateToolPairs(messages) {
|
|
120
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const message of messages) {
|
|
122
|
+
if (message.role !== "assistant") continue;
|
|
123
|
+
for (const block of message.content) {
|
|
124
|
+
if (block.type !== "tool-call") continue;
|
|
125
|
+
const id = historyToolCallId(block.id);
|
|
126
|
+
if (toolNames.has(id)) throw new Error(`Nanocodex cannot hydrate duplicate tool call ${JSON.stringify(id)}`);
|
|
127
|
+
toolNames.set(id, block.name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const seenCalls = /* @__PURE__ */ new Set();
|
|
131
|
+
const resultIds = /* @__PURE__ */ new Set();
|
|
132
|
+
for (const message of messages) for (const block of message.content) {
|
|
133
|
+
if (message.role === "assistant" && block.type === "tool-call") {
|
|
134
|
+
seenCalls.add(historyToolCallId(block.id));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (block.type !== "tool-result") continue;
|
|
138
|
+
const id = historyToolCallId(block.toolCallId);
|
|
139
|
+
const name = toolNames.get(id);
|
|
140
|
+
if (name === void 0) throw new Error(`Nanocodex cannot hydrate a tool result for foreign call ${JSON.stringify(id)}`);
|
|
141
|
+
if (name !== "apply_patch" && name !== "exec") continue;
|
|
142
|
+
if (!seenCalls.has(id)) throw new Error(`Nanocodex cannot hydrate ${name} output before call ${JSON.stringify(id)}`);
|
|
143
|
+
if (resultIds.has(id)) throw new Error(`Nanocodex cannot hydrate duplicate ${name} output ${JSON.stringify(id)}`);
|
|
144
|
+
resultIds.add(id);
|
|
145
|
+
}
|
|
146
|
+
for (const [id, name] of toolNames) if ((name === "apply_patch" || name === "exec") && !resultIds.has(id)) throw new Error(`Nanocodex cannot hydrate ${name} call without a result ${JSON.stringify(id)}`);
|
|
147
|
+
return toolNames;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Convert the authoritative active DSH surface to Nanocodex's typed history.
|
|
151
|
+
* Tool calls are represented as history items only; no handler is invoked while
|
|
152
|
+
* hydrating a session.
|
|
153
|
+
*/
|
|
154
|
+
async function buildHistoryProjection(messages, ctx, signal) {
|
|
155
|
+
const history = [];
|
|
156
|
+
const items = [];
|
|
157
|
+
let continuitySummary;
|
|
158
|
+
const toolNames = validateToolPairs(messages);
|
|
159
|
+
const push = (message, item) => {
|
|
160
|
+
history.push(item);
|
|
161
|
+
items.push({
|
|
162
|
+
message,
|
|
163
|
+
item
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
for (const message of messages) {
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
if (message.role === "user" && isCompactCheckpointSource(message.source)) {
|
|
169
|
+
continuitySummary = textOfCheckpoint(message);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const toolResult = message.content[0];
|
|
173
|
+
if (message.role === "user" && message.content.length === 1 && toolResult?.type === "tool-result") {
|
|
174
|
+
const id = historyToolCallId(toolResult.toolCallId);
|
|
175
|
+
const name = toolNames.get(id);
|
|
176
|
+
const output = await toolOutput(ctx, toolResult.content, signal);
|
|
177
|
+
push(message, name === "apply_patch" || name === "exec" ? {
|
|
178
|
+
type: "custom_tool_call_output",
|
|
179
|
+
call_id: id,
|
|
180
|
+
output
|
|
181
|
+
} : {
|
|
182
|
+
type: "function_call_output",
|
|
183
|
+
call_id: id,
|
|
184
|
+
output
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (message.role === "assistant") {
|
|
189
|
+
const output = [];
|
|
190
|
+
for (const block of message.content) {
|
|
191
|
+
if (block.type === "reasoning") continue;
|
|
192
|
+
if (block.type === "tool-call") {
|
|
193
|
+
if (output.length > 0) push(message, {
|
|
194
|
+
type: "message",
|
|
195
|
+
role: "assistant",
|
|
196
|
+
content: output.splice(0),
|
|
197
|
+
id: String(message.id),
|
|
198
|
+
status: "completed"
|
|
199
|
+
});
|
|
200
|
+
const id = historyToolCallId(block.id);
|
|
201
|
+
if (block.name === "apply_patch" || block.name === "exec") push(message, {
|
|
202
|
+
type: "custom_tool_call",
|
|
203
|
+
name: block.name,
|
|
204
|
+
input: rawCustomInput(block.name, block.arguments),
|
|
205
|
+
call_id: id
|
|
206
|
+
});
|
|
207
|
+
else push(message, {
|
|
208
|
+
type: "function_call",
|
|
209
|
+
name: block.name,
|
|
210
|
+
arguments: block.arguments,
|
|
211
|
+
call_id: id
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
output.push(outputText(block));
|
|
216
|
+
}
|
|
217
|
+
if (output.length > 0) push(message, {
|
|
218
|
+
type: "message",
|
|
219
|
+
role: "assistant",
|
|
220
|
+
content: output,
|
|
221
|
+
id: String(message.id),
|
|
222
|
+
status: "completed"
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const content = [];
|
|
227
|
+
for (const block of message.content) if (block.type === "image") content.push(await imageItem(ctx, block.attachment, signal));
|
|
228
|
+
else if (block.type === "text") content.push(inputText(block));
|
|
229
|
+
else throw new Error(`Nanocodex cannot hydrate user content block ${JSON.stringify(block.type)}`);
|
|
230
|
+
if (content.length === 0) throw new Error(`Nanocodex cannot hydrate empty DSH message ${String(message.id)}`);
|
|
231
|
+
push(message, {
|
|
232
|
+
type: "message",
|
|
233
|
+
role: message.role === "system" ? "developer" : "user",
|
|
234
|
+
content,
|
|
235
|
+
id: String(message.id),
|
|
236
|
+
status: "completed"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
history,
|
|
241
|
+
items,
|
|
242
|
+
...continuitySummary !== void 0 ? { continuitySummary } : {}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function buildHistorySeed(messages, ctx, signal) {
|
|
246
|
+
const { items: _items, ...seed } = await buildHistoryProjection(messages, ctx, signal);
|
|
247
|
+
return seed;
|
|
248
|
+
}
|
|
249
|
+
/** Convert newly admitted DSH user content into one Nanocodex prompt. */
|
|
250
|
+
async function buildPromptInput(messages, ctx, signal) {
|
|
251
|
+
const items = [];
|
|
252
|
+
for (const message of messages) {
|
|
253
|
+
signal?.throwIfAborted();
|
|
254
|
+
for (const block of message.content) if (block.type === "text") items.push({
|
|
255
|
+
type: "text",
|
|
256
|
+
text: block.text
|
|
257
|
+
});
|
|
258
|
+
else if (block.type === "image") {
|
|
259
|
+
const stored = await ctx.attachments.readImage(block.attachment, signal);
|
|
260
|
+
items.push({
|
|
261
|
+
type: "image",
|
|
262
|
+
image_url: imageDataUrl(stored.ref, stored.data),
|
|
263
|
+
detail: "auto"
|
|
264
|
+
});
|
|
265
|
+
} else if (block.type === "tool-result") continue;
|
|
266
|
+
else throw new Error(`Nanocodex cannot use admitted DSH content block ${JSON.stringify(block.type)} as prompt input`);
|
|
267
|
+
}
|
|
268
|
+
if (items.length === 0) return "Continue from the current conversation.";
|
|
269
|
+
if (items.length === 1 && items[0]?.type === "text") return items[0].text;
|
|
270
|
+
return items;
|
|
271
|
+
}
|
|
272
|
+
function plainText(messages) {
|
|
273
|
+
return messages.flatMap((message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : [])).join("\n");
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/session-id.ts
|
|
277
|
+
const DSH_SESSION_ID = /^session-([0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/iu;
|
|
278
|
+
/** Convert the DSH `session-<UUID>` identity to Nanocodex's UUID identity. */
|
|
279
|
+
function normalizeNanocodexSessionId(value) {
|
|
280
|
+
const match = DSH_SESSION_ID.exec(value);
|
|
281
|
+
if (match?.[1] === void 0) throw new Error(`Nanocodex requires a DSH session ID in the form session-<UUIDv4|UUIDv7>, got ${JSON.stringify(value)}`);
|
|
282
|
+
return match[1].toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/settings.ts
|
|
286
|
+
const SETTINGS_NAMESPACE = "llm-pi-ai";
|
|
287
|
+
const SUPPORTED_PROVIDERS = ["openai", "openai-codex-responses"];
|
|
288
|
+
const DEFAULT_NATIVE_API = "openai-responses";
|
|
289
|
+
const profile = z$1.object({
|
|
290
|
+
apiKeyEnv: z$1.string(),
|
|
291
|
+
api: z$1.string(),
|
|
292
|
+
baseURL: z$1.string(),
|
|
293
|
+
websocketURL: z$1.string(),
|
|
294
|
+
reasoning: z$1.string()
|
|
295
|
+
}).loose(true);
|
|
296
|
+
/**
|
|
297
|
+
* Keep the existing settings namespace and profile shape readable by DSH
|
|
298
|
+
* configuration surfaces while accepting fields owned by older pi-ai
|
|
299
|
+
* profiles. Nanocodex intentionally validates only the route fields it can
|
|
300
|
+
* act on at request admission.
|
|
301
|
+
*/
|
|
302
|
+
const NanocodexSettingsSchema = z$1.object({ providers: z$1.dict(profile).default({}) });
|
|
303
|
+
function normalizedApi(provider, api) {
|
|
304
|
+
if (api !== void 0) return api;
|
|
305
|
+
return SUPPORTED_PROVIDERS.includes(provider) ? DEFAULT_NATIVE_API : void 0;
|
|
306
|
+
}
|
|
307
|
+
function configuredNanocodexProviders(value) {
|
|
308
|
+
return SUPPORTED_PROVIDERS.filter((provider) => {
|
|
309
|
+
const configured = value.providers[provider];
|
|
310
|
+
return configured !== void 0 && isSupportedRoute(provider, normalizedApi(provider, configured.api));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function settingsValue(ctx) {
|
|
314
|
+
const value = ctx.settings.get(SETTINGS_NAMESPACE);
|
|
315
|
+
if (!value || typeof value !== "object") throw new Error(`Nanocodex route settings are unavailable: register ${SETTINGS_NAMESPACE} before creating an agent`);
|
|
316
|
+
const providers = value.providers;
|
|
317
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) throw new Error(`Nanocodex route settings ${SETTINGS_NAMESPACE}.providers is invalid`);
|
|
318
|
+
return { providers };
|
|
319
|
+
}
|
|
320
|
+
function thinking(value) {
|
|
321
|
+
if (value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") return value;
|
|
322
|
+
}
|
|
323
|
+
function supportedThinking(value) {
|
|
324
|
+
if (value === "pro") return value;
|
|
325
|
+
return thinking(value);
|
|
326
|
+
}
|
|
327
|
+
/** Resolve one explicit DSH route without provider or credential fallbacks. */
|
|
328
|
+
async function resolveNanocodexRoute(ctx, options) {
|
|
329
|
+
const provider = options.provider;
|
|
330
|
+
const model = options.model;
|
|
331
|
+
if (!provider || !model) throw new Error("Nanocodex requires an explicit AgentOptions.provider and AgentOptions.model");
|
|
332
|
+
if (!isSupportedModel(model)) throw new Error(`Nanocodex does not support model ${JSON.stringify(model)}`);
|
|
333
|
+
const configured = settingsValue(ctx).providers[provider];
|
|
334
|
+
if (!configured) throw new Error(`Nanocodex route ${JSON.stringify(provider)} is not configured`);
|
|
335
|
+
if (!isSupportedRoute(provider, normalizedApi(provider, configured.api))) throw new Error(`Nanocodex does not support route ${JSON.stringify(provider)} with api ${JSON.stringify(configured.api)}`);
|
|
336
|
+
if (!configured.apiKeyEnv) throw new Error(`Nanocodex route ${JSON.stringify(provider)} must declare apiKeyEnv in ${SETTINGS_NAMESPACE}`);
|
|
337
|
+
let ref;
|
|
338
|
+
try {
|
|
339
|
+
ref = credentialRef(configured.apiKeyEnv);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new Error(`Nanocodex route ${JSON.stringify(provider)} has an invalid credential reference`, { cause: error });
|
|
342
|
+
}
|
|
343
|
+
const resolved = await ctx.credentials.resolve(ref);
|
|
344
|
+
if (!resolved?.value) throw new Error(`Nanocodex credential ${JSON.stringify(configured.apiKeyEnv)} is not configured for route ${JSON.stringify(provider)}`);
|
|
345
|
+
const selectedThinking = thinking(options.reasoningEffort ?? configured.reasoning);
|
|
346
|
+
return {
|
|
347
|
+
provider,
|
|
348
|
+
model,
|
|
349
|
+
apiKey: resolved.value,
|
|
350
|
+
...configured.baseURL ? { apiBaseUrl: configured.baseURL } : {},
|
|
351
|
+
...configured.websocketURL ? { websocketUrl: configured.websocketURL } : {},
|
|
352
|
+
...selectedThinking === void 0 ? {} : { thinking: selectedThinking }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/tool-bridge.ts
|
|
357
|
+
function jsonArguments(input) {
|
|
358
|
+
try {
|
|
359
|
+
return JSON.stringify(input ?? {}) ?? "{}";
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new Error("Nanocodex produced non-serializable tool arguments", { cause: error });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function normalizedArguments(input) {
|
|
365
|
+
const text = jsonArguments(input);
|
|
366
|
+
return {
|
|
367
|
+
text,
|
|
368
|
+
value: JSON.parse(text)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function contentText(content) {
|
|
372
|
+
return content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n") || "The DSH tool returned no text content.";
|
|
373
|
+
}
|
|
374
|
+
function modelValue(result, custom) {
|
|
375
|
+
if (custom) return contentText(result.content);
|
|
376
|
+
if (!result.isError) return result.value;
|
|
377
|
+
return {
|
|
378
|
+
error: result.error.message,
|
|
379
|
+
content: contentText(result.content)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function executionFailure(error) {
|
|
383
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
+
return {
|
|
385
|
+
isError: true,
|
|
386
|
+
error: { message },
|
|
387
|
+
content: [{
|
|
388
|
+
type: "text",
|
|
389
|
+
text: message
|
|
390
|
+
}]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function makeTool(definition, context, customDefinition) {
|
|
394
|
+
const custom = customDefinition?.type === "custom";
|
|
395
|
+
let outputSchema = structuredClone(definition.output.schema);
|
|
396
|
+
let description = definition.description;
|
|
397
|
+
if (definition.name === "read_image") {
|
|
398
|
+
description += "\nIn Code Mode, display the loaded image with image(await tools.read_image({file_path: \"picture.png\"})). The result contains path and image_url (a data URL); image(result) emits the image to the model. A bare return does not emit output.";
|
|
399
|
+
outputSchema = {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
path: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "Resolved image file path."
|
|
405
|
+
},
|
|
406
|
+
image_url: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "Loaded image data URL."
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
required: ["path", "image_url"],
|
|
412
|
+
additionalProperties: false
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
name: definition.name,
|
|
417
|
+
description,
|
|
418
|
+
parameters: structuredClone(definition.parameters),
|
|
419
|
+
outputSchema,
|
|
420
|
+
...customDefinition === void 0 ? {} : { definition: structuredClone(customDefinition) },
|
|
421
|
+
async handler(input, call) {
|
|
422
|
+
const id = call.callId;
|
|
423
|
+
const argumentsValue = normalizedArguments(custom && definition.name === "apply_patch" ? (() => {
|
|
424
|
+
if (typeof input !== "string" || input.length === 0) throw new TypeError("apply_patch expects one non-empty raw patch string");
|
|
425
|
+
return { patch: input };
|
|
426
|
+
})() : input);
|
|
427
|
+
const callSeq = await context.callbacks.onCall({
|
|
428
|
+
id,
|
|
429
|
+
name: definition.name,
|
|
430
|
+
arguments: argumentsValue.text,
|
|
431
|
+
...call.parentCallId ? { parentCallId: call.parentCallId } : {}
|
|
432
|
+
});
|
|
433
|
+
const parentCallId = call.parentCallId;
|
|
434
|
+
if (parentCallId) context.agent.session.append("tool/code-dispatch-start", {
|
|
435
|
+
rootCallId: ToolCallId(parentCallId),
|
|
436
|
+
parentCallId: ToolCallId(parentCallId),
|
|
437
|
+
subCallId: ToolCallId(id),
|
|
438
|
+
name: definition.name,
|
|
439
|
+
arguments: argumentsValue.value
|
|
440
|
+
});
|
|
441
|
+
let result;
|
|
442
|
+
try {
|
|
443
|
+
result = await context.tools.execute({
|
|
444
|
+
callId: ToolCallId(id),
|
|
445
|
+
...call.parentCallId ? { rootCallId: ToolCallId(call.parentCallId) } : {},
|
|
446
|
+
name: definition.name,
|
|
447
|
+
arguments: argumentsValue.value,
|
|
448
|
+
agent: context.agent,
|
|
449
|
+
signal: call.signal
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
result = executionFailure(error);
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await context.callbacks.onResult({
|
|
456
|
+
id,
|
|
457
|
+
callSeq,
|
|
458
|
+
result,
|
|
459
|
+
...parentCallId ? { parentCallId } : {}
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
if (parentCallId) context.agent.session.append("tool/code-dispatch", {
|
|
463
|
+
rootCallId: ToolCallId(parentCallId),
|
|
464
|
+
parentCallId: ToolCallId(parentCallId),
|
|
465
|
+
subCallId: ToolCallId(id),
|
|
466
|
+
name: definition.name,
|
|
467
|
+
arguments: argumentsValue.value,
|
|
468
|
+
isError: result.isError,
|
|
469
|
+
content: result.content
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (!result.isError && definition.name === "read_image") {
|
|
473
|
+
const image = result.content.find((block) => block.type === "image");
|
|
474
|
+
if (image?.type !== "image") throw new Error("read_image returned no image attachment");
|
|
475
|
+
const stored = await context.agent.ctx.attachments.readImage(image.attachment, call.signal);
|
|
476
|
+
return {
|
|
477
|
+
path: result.value.path,
|
|
478
|
+
image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return modelValue(result, custom);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Build one model-facing tool set from the active DSH scope. */
|
|
486
|
+
function createToolBridge(context) {
|
|
487
|
+
const scope = scopeOf(context.agent.ctx);
|
|
488
|
+
return context.tools.schemas(scope).map((schema) => context.tools.get(schema.name, scope)).filter((definition) => definition !== void 0).map((definition) => makeTool(definition, context, context.customDefinitions?.get(definition.name)));
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/interrupted-tools.ts
|
|
492
|
+
/** Close admitted calls at a stopped driver boundary without replaying them. */
|
|
493
|
+
function closeInterruptedToolCalls(session, failure) {
|
|
494
|
+
const calls = /* @__PURE__ */ new Map();
|
|
495
|
+
const results = /* @__PURE__ */ new Set();
|
|
496
|
+
for (const seq of session.surface.nodes) {
|
|
497
|
+
const event = session.eventAt(seq);
|
|
498
|
+
if (event?.type === "assistant/message") for (const block of event.data.message.content) {
|
|
499
|
+
if (block.type !== "tool-call") continue;
|
|
500
|
+
const id = String(block.id);
|
|
501
|
+
if (calls.has(id)) throw new Error(`Cannot recover duplicate tool call ${JSON.stringify(id)}`);
|
|
502
|
+
calls.set(id, {
|
|
503
|
+
block,
|
|
504
|
+
turn: event.data.turn,
|
|
505
|
+
step: event.data.step
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else if (event?.type === "tool/result") results.add(String(event.data.message.source.callId));
|
|
509
|
+
}
|
|
510
|
+
for (const id of results) calls.delete(id);
|
|
511
|
+
if (calls.size === 0) return;
|
|
512
|
+
const callSeqs = /* @__PURE__ */ new Map();
|
|
513
|
+
for (const event of session.snapshotEvents()) {
|
|
514
|
+
if (event.type !== "tool/call") continue;
|
|
515
|
+
const pending = calls.get(String(event.data.callId));
|
|
516
|
+
if (pending?.turn === event.data.turn && pending.step === event.data.step) callSeqs.set(String(event.data.callId), event.seq);
|
|
517
|
+
}
|
|
518
|
+
const cause = failure instanceof Error ? failure.message : String(failure);
|
|
519
|
+
const text = failure === void 0 ? INTERRUPTED_TOOL_OUTPUT : `${INTERRUPTED_TOOL_OUTPUT}\nCause: ${cause.slice(0, 1024)}`;
|
|
520
|
+
for (const [id, { block, turn, step }] of calls) {
|
|
521
|
+
const callSeq = callSeqs.get(id) ?? session.append("tool/call", {
|
|
522
|
+
turn,
|
|
523
|
+
step,
|
|
524
|
+
callId: block.id,
|
|
525
|
+
name: block.name,
|
|
526
|
+
arguments: block.arguments
|
|
527
|
+
}).seq;
|
|
528
|
+
session.append("tool/result", {
|
|
529
|
+
turn,
|
|
530
|
+
step,
|
|
531
|
+
message: createToolResultMessage({
|
|
532
|
+
callId: block.id,
|
|
533
|
+
content: [{
|
|
534
|
+
type: "text",
|
|
535
|
+
text
|
|
536
|
+
}],
|
|
537
|
+
isError: true
|
|
538
|
+
})
|
|
539
|
+
}, {
|
|
540
|
+
surfaceOp: "append",
|
|
541
|
+
sourceEventSeqs: [callSeq]
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/output.ts
|
|
547
|
+
function record$1(value) {
|
|
548
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
549
|
+
}
|
|
550
|
+
function count(value) {
|
|
551
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
552
|
+
}
|
|
553
|
+
/** Responses input includes cache buckets; DSH input excludes them. */
|
|
554
|
+
function modelCallUsage(event) {
|
|
555
|
+
if (event.type !== "model.call.completed") return void 0;
|
|
556
|
+
const value = record$1(event.payload.usage);
|
|
557
|
+
const input = count(value?.input_tokens);
|
|
558
|
+
const output = count(value?.output_tokens);
|
|
559
|
+
if (value === void 0 || input === void 0 || output === void 0) return void 0;
|
|
560
|
+
const details = record$1(value.input_tokens_details);
|
|
561
|
+
const cacheReadTokens = count(details?.cached_tokens);
|
|
562
|
+
const cacheWriteTokens = count(details?.cache_write_tokens);
|
|
563
|
+
const reasoningTokens = count(record$1(value.output_tokens_details)?.reasoning_tokens);
|
|
564
|
+
const totalTokens = count(value.total_tokens);
|
|
565
|
+
return {
|
|
566
|
+
inputTokens: Math.max(0, input - (cacheReadTokens ?? 0) - (cacheWriteTokens ?? 0)),
|
|
567
|
+
outputTokens: output,
|
|
568
|
+
...totalTokens === void 0 ? {} : { totalTokens },
|
|
569
|
+
...cacheReadTokens === void 0 ? {} : { cacheReadTokens },
|
|
570
|
+
...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
|
|
571
|
+
...reasoningTokens === void 0 ? {} : { reasoningTokens }
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function outputBlock(item) {
|
|
575
|
+
if (item.type === "message" && item.role === "assistant" && Array.isArray(item.content)) {
|
|
576
|
+
const text = item.content.map(record$1).flatMap((part) => part?.type === "output_text" && typeof part.text === "string" ? [part.text] : []).join("");
|
|
577
|
+
return text ? {
|
|
578
|
+
type: "text",
|
|
579
|
+
text
|
|
580
|
+
} : void 0;
|
|
581
|
+
}
|
|
582
|
+
if ((item.type === "custom_tool_call" || item.type === "function_call") && typeof item.call_id === "string" && typeof item.name === "string") {
|
|
583
|
+
let args;
|
|
584
|
+
if (item.type === "custom_tool_call") {
|
|
585
|
+
if (typeof item.input !== "string") throw new Error("Nanocodex custom call has no raw input");
|
|
586
|
+
if (item.name === "exec") args = JSON.stringify({ code: item.input });
|
|
587
|
+
else if (item.name === "apply_patch") args = JSON.stringify({ patch: item.input });
|
|
588
|
+
else throw new Error(`Nanocodex cannot project custom tool ${JSON.stringify(item.name)}`);
|
|
589
|
+
} else {
|
|
590
|
+
if (typeof item.arguments !== "string") throw new Error("Nanocodex function call has no arguments");
|
|
591
|
+
args = item.arguments;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
type: "tool-call",
|
|
595
|
+
id: ToolCallId(item.call_id),
|
|
596
|
+
name: item.name,
|
|
597
|
+
arguments: args
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async function resultContent(agent, value) {
|
|
602
|
+
if (typeof value === "string") return [{
|
|
603
|
+
type: "text",
|
|
604
|
+
text: value
|
|
605
|
+
}];
|
|
606
|
+
if (!Array.isArray(value)) throw new Error("Nanocodex tool result has no supported output body");
|
|
607
|
+
const content = value.map((value) => {
|
|
608
|
+
const part = record$1(value);
|
|
609
|
+
if (part?.type === "input_text" && typeof part.text === "string") return {
|
|
610
|
+
type: "text",
|
|
611
|
+
text: part.text
|
|
612
|
+
};
|
|
613
|
+
if (part?.type === "input_image" && typeof part.image_url === "string") {
|
|
614
|
+
const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=]+)$/u.exec(part.image_url);
|
|
615
|
+
if (match) return {
|
|
616
|
+
type: "image",
|
|
617
|
+
mediaType: match[1],
|
|
618
|
+
data: match[2]
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
throw new Error("Nanocodex tool result contains an unsupported content part");
|
|
622
|
+
});
|
|
623
|
+
if (content.every((part) => part.type === "text")) return content;
|
|
624
|
+
return admitPromptContent(agent.ctx.attachments, content);
|
|
625
|
+
}
|
|
626
|
+
/** One DSH step and one durable Assistant per Nanocodex model request. */
|
|
627
|
+
var NanocodexOutput = class {
|
|
628
|
+
agent;
|
|
629
|
+
provider;
|
|
630
|
+
model;
|
|
631
|
+
turn;
|
|
632
|
+
advanceStep;
|
|
633
|
+
step;
|
|
634
|
+
started = false;
|
|
635
|
+
committed = false;
|
|
636
|
+
blocks = [];
|
|
637
|
+
chunkSeqs = [];
|
|
638
|
+
textIndices = /* @__PURE__ */ new Map();
|
|
639
|
+
openText;
|
|
640
|
+
outputItems = /* @__PURE__ */ new Map();
|
|
641
|
+
parents = /* @__PURE__ */ new Map();
|
|
642
|
+
constructor(agent, provider, model, turn, step, advanceStep) {
|
|
643
|
+
this.agent = agent;
|
|
644
|
+
this.provider = provider;
|
|
645
|
+
this.model = model;
|
|
646
|
+
this.turn = turn;
|
|
647
|
+
this.advanceStep = advanceStep;
|
|
648
|
+
this.step = step;
|
|
649
|
+
}
|
|
650
|
+
get coordinates() {
|
|
651
|
+
return {
|
|
652
|
+
turn: this.turn,
|
|
653
|
+
step: this.step
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
chunk(chunk) {
|
|
657
|
+
this.chunkSeqs.push(this.agent.session.append("assistant/chunk", {
|
|
658
|
+
...this.coordinates,
|
|
659
|
+
chunk
|
|
660
|
+
}).seq);
|
|
661
|
+
}
|
|
662
|
+
text(text, id, complete) {
|
|
663
|
+
if (!text) return;
|
|
664
|
+
let index = typeof id === "string" ? this.textIndices.get(id) : this.openText;
|
|
665
|
+
if (index === void 0) {
|
|
666
|
+
index = this.blocks.length;
|
|
667
|
+
this.blocks.push({
|
|
668
|
+
type: "text",
|
|
669
|
+
text: ""
|
|
670
|
+
});
|
|
671
|
+
if (typeof id === "string") this.textIndices.set(id, index);
|
|
672
|
+
this.chunk({
|
|
673
|
+
type: "block-start",
|
|
674
|
+
index,
|
|
675
|
+
blockType: "text"
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const previous = this.blocks[index];
|
|
679
|
+
if (previous?.type !== "text") throw new Error("Nanocodex text stream lost its block");
|
|
680
|
+
if (complete) {
|
|
681
|
+
this.blocks[index] = {
|
|
682
|
+
type: "text",
|
|
683
|
+
text
|
|
684
|
+
};
|
|
685
|
+
this.chunk({
|
|
686
|
+
type: "block-end",
|
|
687
|
+
index,
|
|
688
|
+
block: this.blocks[index]
|
|
689
|
+
});
|
|
690
|
+
this.openText = void 0;
|
|
691
|
+
} else {
|
|
692
|
+
this.blocks[index] = {
|
|
693
|
+
type: "text",
|
|
694
|
+
text: previous.text + text
|
|
695
|
+
};
|
|
696
|
+
this.openText = index;
|
|
697
|
+
this.chunk({
|
|
698
|
+
type: "text-delta",
|
|
699
|
+
index,
|
|
700
|
+
text
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
installOutput(items) {
|
|
705
|
+
this.blocks = items.flatMap((item) => {
|
|
706
|
+
const block = outputBlock(item);
|
|
707
|
+
return block === void 0 ? [] : [block];
|
|
708
|
+
});
|
|
709
|
+
for (const [index, block] of this.blocks.entries()) this.chunk({
|
|
710
|
+
type: "block-end",
|
|
711
|
+
index,
|
|
712
|
+
block
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
commit(usage, interrupted = false) {
|
|
716
|
+
if (this.committed) return;
|
|
717
|
+
if (usage !== void 0) this.chunk({
|
|
718
|
+
type: "usage",
|
|
719
|
+
usage
|
|
720
|
+
});
|
|
721
|
+
for (const block of this.blocks) if (block.type === "tool-call") this.parents.set(String(block.id), {
|
|
722
|
+
block,
|
|
723
|
+
step: this.step
|
|
724
|
+
});
|
|
725
|
+
this.agent.session.append("assistant/message", {
|
|
726
|
+
...this.coordinates,
|
|
727
|
+
message: createAssistantMessage({
|
|
728
|
+
content: this.blocks,
|
|
729
|
+
source: {
|
|
730
|
+
provider: this.provider,
|
|
731
|
+
model: this.model
|
|
732
|
+
}
|
|
733
|
+
}),
|
|
734
|
+
...usage === void 0 ? {} : { usage },
|
|
735
|
+
...interrupted ? { interrupted: true } : {}
|
|
736
|
+
}, {
|
|
737
|
+
surfaceOp: "append",
|
|
738
|
+
sourceEventSeqs: this.chunkSeqs
|
|
739
|
+
});
|
|
740
|
+
this.committed = true;
|
|
741
|
+
}
|
|
742
|
+
async accept(event) {
|
|
743
|
+
const payload = event.payload;
|
|
744
|
+
if (event.type === "model.call.started") {
|
|
745
|
+
if (this.started) {
|
|
746
|
+
if (!this.committed && this.blocks.length > 0) this.commit(void 0, true);
|
|
747
|
+
this.step = this.advanceStep();
|
|
748
|
+
}
|
|
749
|
+
this.started = true;
|
|
750
|
+
this.committed = false;
|
|
751
|
+
this.blocks = [];
|
|
752
|
+
this.chunkSeqs = [];
|
|
753
|
+
this.textIndices.clear();
|
|
754
|
+
this.outputItems.clear();
|
|
755
|
+
this.openText = void 0;
|
|
756
|
+
} else if (event.type === "assistant.delta" && typeof payload.text === "string") this.text(payload.text, payload.item_id, false);
|
|
757
|
+
else if (event.type === "assistant.message" && typeof payload.text === "string") this.text(payload.text, payload.item_id, true);
|
|
758
|
+
else if (event.type === "api.event" && payload.phase === "generation") {
|
|
759
|
+
const api = record$1(payload.event);
|
|
760
|
+
if (api?.type === "response.output_item.done") {
|
|
761
|
+
const item = record$1(api.item);
|
|
762
|
+
if (item !== void 0) this.outputItems.set(String(item.id ?? api.output_index), item);
|
|
763
|
+
} else if (api?.type === "response.completed") {
|
|
764
|
+
const output = record$1(api.response)?.output;
|
|
765
|
+
if (Array.isArray(output) && output.length > 0) this.installOutput(output.flatMap((value) => {
|
|
766
|
+
const item = record$1(value);
|
|
767
|
+
return item === void 0 ? [] : [item];
|
|
768
|
+
}));
|
|
769
|
+
else if (this.outputItems.size > 0) this.installOutput([...this.outputItems.values()]);
|
|
770
|
+
}
|
|
771
|
+
} else if (event.type === "model.call.completed") this.commit(modelCallUsage(event));
|
|
772
|
+
else if (event.type === "tool.call" || event.type === "tool.result") {
|
|
773
|
+
if (typeof payload.call_id !== "string") return;
|
|
774
|
+
const parent = this.parents.get(payload.call_id);
|
|
775
|
+
if (parent === void 0 || parent.block.name !== "exec" && parent.block.name !== "wait") return;
|
|
776
|
+
if (event.type === "tool.call") parent.callSeq = this.agent.session.append("tool/call", {
|
|
777
|
+
turn: this.turn,
|
|
778
|
+
step: parent.step,
|
|
779
|
+
callId: parent.block.id,
|
|
780
|
+
name: parent.block.name,
|
|
781
|
+
arguments: parent.block.arguments
|
|
782
|
+
}).seq;
|
|
783
|
+
else {
|
|
784
|
+
if (parent.callSeq === void 0) throw new Error("Nanocodex parent result has no durable call");
|
|
785
|
+
this.agent.session.append("tool/result", {
|
|
786
|
+
turn: this.turn,
|
|
787
|
+
step: parent.step,
|
|
788
|
+
message: createToolResultMessage({
|
|
789
|
+
callId: parent.block.id,
|
|
790
|
+
content: await resultContent(this.agent, payload.result),
|
|
791
|
+
isError: payload.status !== "completed"
|
|
792
|
+
})
|
|
793
|
+
}, {
|
|
794
|
+
surfaceOp: "append",
|
|
795
|
+
sourceEventSeqs: [parent.callSeq]
|
|
796
|
+
});
|
|
797
|
+
this.parents.delete(payload.call_id);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
finish(finalText) {
|
|
802
|
+
if (this.committed) return;
|
|
803
|
+
if (this.blocks.length === 0 && finalText) this.text(finalText, void 0, true);
|
|
804
|
+
if (this.blocks.length > 0) this.commit();
|
|
805
|
+
}
|
|
806
|
+
interrupt(error) {
|
|
807
|
+
if (!this.committed && this.blocks.length > 0) this.commit(void 0, true);
|
|
808
|
+
closeInterruptedToolCalls(this.agent.session, error);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/transport-diagnostic.ts
|
|
813
|
+
const FALLBACK_EVENT = "model.attempt.retrying";
|
|
814
|
+
const FALLBACK_ERROR_CLASS = "websocket_fallback";
|
|
815
|
+
const PREVIOUS_TRANSPORT = "responses_websocket_v2";
|
|
816
|
+
const NEXT_TRANSPORT = "responses_https_sse";
|
|
817
|
+
const REASONS = /* @__PURE__ */ new Set([
|
|
818
|
+
"upgrade_required",
|
|
819
|
+
"transport_unavailable",
|
|
820
|
+
"retry_exhausted"
|
|
821
|
+
]);
|
|
822
|
+
const MAX_CORRELATION_LENGTH = 128;
|
|
823
|
+
function boundedCorrelation(value) {
|
|
824
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
825
|
+
return value.slice(0, MAX_CORRELATION_LENGTH);
|
|
826
|
+
}
|
|
827
|
+
function transportFallbackDiagnostic(sessionId, event) {
|
|
828
|
+
if (event.type !== FALLBACK_EVENT) return void 0;
|
|
829
|
+
const payload = event.payload;
|
|
830
|
+
if (payload.error_class !== FALLBACK_ERROR_CLASS || payload.previous_transport !== PREVIOUS_TRANSPORT || payload.next_transport !== NEXT_TRANSPORT || typeof payload.reason !== "string" || !REASONS.has(payload.reason)) return;
|
|
831
|
+
const reason = payload.reason;
|
|
832
|
+
const requestId = boundedCorrelation(event.request_id);
|
|
833
|
+
return {
|
|
834
|
+
kind: "nanocodex.transport_fallback",
|
|
835
|
+
session_id: boundedCorrelation(sessionId) ?? "unknown",
|
|
836
|
+
...requestId === void 0 ? {} : { request_id: requestId },
|
|
837
|
+
error_class: FALLBACK_ERROR_CLASS,
|
|
838
|
+
previous_transport: PREVIOUS_TRANSPORT,
|
|
839
|
+
next_transport: NEXT_TRANSPORT,
|
|
840
|
+
reason
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/** Observe public Nanocodex fallback events and project only safe fields. */
|
|
844
|
+
function observeTransportFallback(ctx, sessionId, source) {
|
|
845
|
+
const watcher = source.events.watch();
|
|
846
|
+
const removeListener = watcher.onEvent((event) => {
|
|
847
|
+
const diagnostic = transportFallbackDiagnostic(sessionId, event);
|
|
848
|
+
if (diagnostic === void 0) return;
|
|
849
|
+
try {
|
|
850
|
+
ctx.logger("dsh-nanocodex.transport").info(diagnostic);
|
|
851
|
+
} catch {}
|
|
852
|
+
});
|
|
853
|
+
return () => {
|
|
854
|
+
removeListener();
|
|
855
|
+
watcher.off();
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/engine.ts
|
|
860
|
+
const nanocodexCheckpointDomain = defineDomain({
|
|
861
|
+
name: "nanocodex_checkpoints",
|
|
862
|
+
version: 1,
|
|
863
|
+
layout: "per-record",
|
|
864
|
+
tables: { sessions: domainTable(z.custom((value) => checkpoint(value) !== void 0)) }
|
|
865
|
+
});
|
|
866
|
+
const RAW_APPLICATION_DEFINITIONS = /* @__PURE__ */ new Map([[APPLY_PATCH_NAME, {
|
|
867
|
+
type: "custom",
|
|
868
|
+
description: APPLY_PATCH_DESCRIPTION,
|
|
869
|
+
format: {
|
|
870
|
+
type: "grammar",
|
|
871
|
+
syntax: "lark",
|
|
872
|
+
definition: APPLY_PATCH_GRAMMAR
|
|
873
|
+
}
|
|
874
|
+
}]]);
|
|
875
|
+
function runtimeKey(agent) {
|
|
876
|
+
return String(agent.session.id);
|
|
877
|
+
}
|
|
878
|
+
function messageText(message) {
|
|
879
|
+
return plainText([message]);
|
|
880
|
+
}
|
|
881
|
+
function supplementaryContext(messages) {
|
|
882
|
+
return messages.filter((message) => message.source.kind === "plugin").map(messageText).filter(Boolean).join("\n\n") || void 0;
|
|
883
|
+
}
|
|
884
|
+
function routeKey(route, agent) {
|
|
885
|
+
return JSON.stringify({
|
|
886
|
+
provider: route.provider,
|
|
887
|
+
model: route.model,
|
|
888
|
+
apiKeyDigest: createHash("sha256").update(route.apiKey).digest("hex"),
|
|
889
|
+
apiBaseUrl: route.apiBaseUrl,
|
|
890
|
+
websocketUrl: route.websocketUrl,
|
|
891
|
+
thinking: route.thinking,
|
|
892
|
+
reasoningEffort: agent.options.reasoningEffort
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function fingerprint(messages) {
|
|
896
|
+
return createHash("sha256").update(JSON.stringify(messages)).digest("hex");
|
|
897
|
+
}
|
|
898
|
+
function makeBoundary(session, messages) {
|
|
899
|
+
return {
|
|
900
|
+
replaceGeneration: session.surface.replaceGeneration,
|
|
901
|
+
surfaceSeqs: [...session.surface.nodes],
|
|
902
|
+
messageCount: messages.length,
|
|
903
|
+
fingerprint: fingerprint(messages)
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function boundaryMatches(boundary, session, messages) {
|
|
907
|
+
const nodes = session.surface.nodes;
|
|
908
|
+
return boundary.replaceGeneration === session.surface.replaceGeneration && boundary.messageCount === messages.length && boundary.surfaceSeqs.length <= nodes.length && boundary.surfaceSeqs.every((seq, index) => nodes[index] === seq) && boundary.fingerprint === fingerprint(messages);
|
|
909
|
+
}
|
|
910
|
+
function surfacePrefix(session, messageCount) {
|
|
911
|
+
const nodes = [...session.surface.nodes];
|
|
912
|
+
if (messageCount > nodes.length) throw new Error("Nanocodex compaction cannot capture a DSH admission boundary");
|
|
913
|
+
return nodes.slice(0, messageCount);
|
|
914
|
+
}
|
|
915
|
+
function record(value) {
|
|
916
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
917
|
+
}
|
|
918
|
+
function supportedModel(value) {
|
|
919
|
+
return typeof value === "string" && isSupportedModel(value) ? value : void 0;
|
|
920
|
+
}
|
|
921
|
+
function sessionSnapshot(value) {
|
|
922
|
+
const candidate = record(value);
|
|
923
|
+
if (!candidate || !Array.isArray(candidate.history)) return void 0;
|
|
924
|
+
if (supportedModel(candidate.model) === void 0) return void 0;
|
|
925
|
+
return candidate;
|
|
926
|
+
}
|
|
927
|
+
function checkpoint(value) {
|
|
928
|
+
const candidate = record(value);
|
|
929
|
+
const boundary = record(candidate?.boundary);
|
|
930
|
+
const snapshot = sessionSnapshot(candidate?.snapshot);
|
|
931
|
+
if (candidate?.version !== 1 || typeof candidate.provider !== "string" || supportedModel(candidate.model) === void 0 || !boundary || snapshot === void 0 || snapshot.model !== candidate.model || !Array.isArray(boundary.surfaceSeqs) || !boundary.surfaceSeqs.every((seq) => typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 0) || typeof boundary.replaceGeneration !== "number" || !Number.isSafeInteger(boundary.replaceGeneration) || boundary.replaceGeneration < 0 || typeof boundary.messageCount !== "number" || !Number.isSafeInteger(boundary.messageCount) || boundary.messageCount < 0 || typeof boundary.fingerprint !== "string") return;
|
|
932
|
+
return {
|
|
933
|
+
version: 1,
|
|
934
|
+
provider: candidate.provider,
|
|
935
|
+
model: supportedModel(candidate.model),
|
|
936
|
+
boundary: {
|
|
937
|
+
replaceGeneration: boundary.replaceGeneration,
|
|
938
|
+
surfaceSeqs: boundary.surfaceSeqs,
|
|
939
|
+
messageCount: boundary.messageCount,
|
|
940
|
+
fingerprint: boundary.fingerprint
|
|
941
|
+
},
|
|
942
|
+
snapshot
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function findMatchingCheckpoint(store, session, messages, provider, model) {
|
|
946
|
+
const candidate = checkpoint(store.get(session.id));
|
|
947
|
+
if (candidate !== void 0 && candidate.provider === provider && candidate.model === model && boundaryMatches(candidate.boundary, session, messages)) return candidate;
|
|
948
|
+
}
|
|
949
|
+
const DEFAULT_COMPACTION_INSTRUCTION = [
|
|
950
|
+
"Create a concise private continuity summary of the conversation above.",
|
|
951
|
+
"Preserve important facts, preferences, commitments, unresolved questions, and the immediate context.",
|
|
952
|
+
"Return summary text only. Do not mention this instruction or compaction."
|
|
953
|
+
].join("\n");
|
|
954
|
+
async function* emptyLlmStream() {}
|
|
955
|
+
function integer(value) {
|
|
956
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
957
|
+
}
|
|
958
|
+
function nullableString(value) {
|
|
959
|
+
return value === null || typeof value === "string";
|
|
960
|
+
}
|
|
961
|
+
function parseCompactionIdentity(value) {
|
|
962
|
+
const candidate = record(value);
|
|
963
|
+
if (!candidate || !integer(candidate.index) || typeof candidate.kind !== "string" || candidate.kind.length === 0 || !nullableString(candidate.id) || !nullableString(candidate.call_id)) return;
|
|
964
|
+
return candidate;
|
|
965
|
+
}
|
|
966
|
+
function parseCompactionOutcome(value) {
|
|
967
|
+
const candidate = record(value);
|
|
968
|
+
const range = record(candidate?.replaced_history);
|
|
969
|
+
const context = record(candidate?.context);
|
|
970
|
+
const retained = Array.isArray(candidate?.retained_tail) ? candidate.retained_tail.map(parseCompactionIdentity) : void 0;
|
|
971
|
+
if (!candidate || typeof candidate.revision !== "string" || candidate.revision.length === 0 || candidate.trigger !== "manual" && candidate.trigger !== "automatic" || !nullableString(candidate.summary) || !range || !integer(range.start) || !integer(range.end) || range.start > range.end || !retained || retained.some((item) => item === void 0) || !context || typeof context.workspace !== "string" || !Array.isArray(context.history) || !context.history.every((item) => record(item) !== void 0)) return;
|
|
972
|
+
return candidate;
|
|
973
|
+
}
|
|
974
|
+
function parseCompactionReplacedEvent(event) {
|
|
975
|
+
if (event.type !== "model.compaction.replaced") return void 0;
|
|
976
|
+
if (parseCompactionOutcome(event.payload) === void 0 || !integer(event.payload.after_model_call_index) || event.payload.phase !== "pre_turn" && event.payload.phase !== "mid_turn") return;
|
|
977
|
+
return event.payload;
|
|
978
|
+
}
|
|
979
|
+
function projectedIdentityMatches(identity, item) {
|
|
980
|
+
const identityKind = identity.kind === "custom_tool_call" ? "function_call" : identity.kind === "custom_tool_call_output" ? "function_call_output" : identity.kind;
|
|
981
|
+
if (identityKind !== (item.type === "custom_tool_call" ? "function_call" : item.type === "custom_tool_call_output" ? "function_call_output" : item.type)) return false;
|
|
982
|
+
const callId = "call_id" in item ? item.call_id : null;
|
|
983
|
+
if (identity.call_id !== callId) return false;
|
|
984
|
+
return identityKind === "message" || identityKind === "function_call" || identityKind === "function_call_output" || identity.id === (item.id ?? null);
|
|
985
|
+
}
|
|
986
|
+
function historyImageDetail(value) {
|
|
987
|
+
return value === void 0 || value === "auto" || value === "low" || value === "high" || value === "original";
|
|
988
|
+
}
|
|
989
|
+
function historyInputItemMatches(left, right) {
|
|
990
|
+
const leftObject = record(left);
|
|
991
|
+
const rightObject = record(right);
|
|
992
|
+
if (leftObject === void 0 || rightObject === void 0) return false;
|
|
993
|
+
if (leftObject.type !== rightObject.type) return false;
|
|
994
|
+
switch (leftObject.type) {
|
|
995
|
+
case "input_text": return typeof leftObject.text === "string" && leftObject.text === rightObject.text;
|
|
996
|
+
case "input_image": return typeof leftObject.image_url === "string" && leftObject.image_url === rightObject.image_url && historyImageDetail(leftObject.detail) && historyImageDetail(rightObject.detail) && leftObject.detail === rightObject.detail;
|
|
997
|
+
case "input_audio": return typeof leftObject.audio_url === "string" && leftObject.audio_url === rightObject.audio_url;
|
|
998
|
+
case "encrypted_content": return typeof leftObject.encrypted_content === "string" && leftObject.encrypted_content === rightObject.encrypted_content;
|
|
999
|
+
default: return false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function historyContentItemMatches(left, right) {
|
|
1003
|
+
const leftObject = record(left);
|
|
1004
|
+
const rightObject = record(right);
|
|
1005
|
+
if (leftObject === void 0 || rightObject === void 0) return false;
|
|
1006
|
+
if (leftObject.type !== rightObject.type) return false;
|
|
1007
|
+
if (leftObject.type === "output_text") return typeof leftObject.text === "string" && leftObject.text === rightObject.text;
|
|
1008
|
+
if (leftObject.type === "input_text" || leftObject.type === "input_image" || leftObject.type === "input_audio") return historyInputItemMatches(left, right);
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
function historyContentMatches(left, right) {
|
|
1012
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => historyContentItemMatches(item, right[index]));
|
|
1013
|
+
}
|
|
1014
|
+
function historyToolOutputMatches(left, right) {
|
|
1015
|
+
if (typeof left === "string" || typeof right === "string") return typeof left === "string" && left === right;
|
|
1016
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => historyInputItemMatches(item, right[index]));
|
|
1017
|
+
}
|
|
1018
|
+
function historyMessageMatches(left, right) {
|
|
1019
|
+
return left?.type === "message" && right.type === "message" && left.role === right.role && historyContentMatches(left.content, right.content);
|
|
1020
|
+
}
|
|
1021
|
+
function historyItemsMatch(left, right) {
|
|
1022
|
+
if (left === void 0) return false;
|
|
1023
|
+
const leftType = historyKind(left);
|
|
1024
|
+
if (leftType !== historyKind(right)) return false;
|
|
1025
|
+
switch (leftType) {
|
|
1026
|
+
case "message": return left.type === "message" && right.type === "message" && left.role === right.role && historyContentMatches(left.content, right.content);
|
|
1027
|
+
case "function_call": {
|
|
1028
|
+
const leftCall = functionCallFields(left);
|
|
1029
|
+
const rightCall = functionCallFields(right);
|
|
1030
|
+
return leftCall !== void 0 && rightCall !== void 0 && leftCall.name === rightCall.name && leftCall.input === rightCall.input && leftCall.call_id === rightCall.call_id;
|
|
1031
|
+
}
|
|
1032
|
+
case "function_call_output": {
|
|
1033
|
+
const leftOutput = functionCallOutputFields(left);
|
|
1034
|
+
const rightOutput = functionCallOutputFields(right);
|
|
1035
|
+
return leftOutput !== void 0 && rightOutput !== void 0 && leftOutput.call_id === rightOutput.call_id && historyToolOutputMatches(leftOutput.output, rightOutput.output);
|
|
1036
|
+
}
|
|
1037
|
+
case "compaction": return left.type === "compaction" && right.type === "compaction" && left.encrypted_content === right.encrypted_content;
|
|
1038
|
+
default: return false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function historyKind(item) {
|
|
1042
|
+
if (item.type === "custom_tool_call") return "function_call";
|
|
1043
|
+
if (item.type === "custom_tool_call_output") return "function_call_output";
|
|
1044
|
+
return item.type;
|
|
1045
|
+
}
|
|
1046
|
+
function functionCallFields(item) {
|
|
1047
|
+
if (item.type === "function_call") return {
|
|
1048
|
+
name: item.name,
|
|
1049
|
+
input: item.arguments,
|
|
1050
|
+
call_id: item.call_id
|
|
1051
|
+
};
|
|
1052
|
+
if (item.type === "custom_tool_call") return {
|
|
1053
|
+
name: item.name,
|
|
1054
|
+
input: item.input,
|
|
1055
|
+
call_id: item.call_id
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function functionCallOutputFields(item) {
|
|
1059
|
+
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") return {
|
|
1060
|
+
call_id: item.call_id,
|
|
1061
|
+
output: item.output
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function validateRetainedApplyPatchPairs(items) {
|
|
1065
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
1066
|
+
const resultIds = /* @__PURE__ */ new Set();
|
|
1067
|
+
for (const item of items) {
|
|
1068
|
+
if (item.type === "custom_tool_call" && item.name === "apply_patch") {
|
|
1069
|
+
if (callIds.has(item.call_id)) throw new Error(`Nanocodex compaction retained duplicate apply_patch call ${JSON.stringify(item.call_id)}`);
|
|
1070
|
+
callIds.add(item.call_id);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if (item.type !== "custom_tool_call_output") continue;
|
|
1074
|
+
const namedPatch = item.name === APPLY_PATCH_NAME;
|
|
1075
|
+
const matchesPatchCall = callIds.has(item.call_id);
|
|
1076
|
+
if (!namedPatch && !matchesPatchCall) continue;
|
|
1077
|
+
if (!matchesPatchCall) throw new Error(`Nanocodex compaction retained apply_patch output has no matching call ${JSON.stringify(item.call_id)}`);
|
|
1078
|
+
if (item.name !== void 0 && item.name !== "apply_patch") throw new Error(`Nanocodex compaction retained apply_patch output has a mismatched name ${JSON.stringify(item.call_id)}`);
|
|
1079
|
+
if (resultIds.has(item.call_id)) throw new Error(`Nanocodex compaction retained duplicate apply_patch output ${JSON.stringify(item.call_id)}`);
|
|
1080
|
+
resultIds.add(item.call_id);
|
|
1081
|
+
}
|
|
1082
|
+
for (const callId of callIds) if (!resultIds.has(callId)) throw new Error(`Nanocodex compaction retained apply_patch call without a result ${JSON.stringify(callId)}`);
|
|
1083
|
+
return callIds;
|
|
1084
|
+
}
|
|
1085
|
+
function codeDispatchAssociations(session) {
|
|
1086
|
+
const bySubCallId = /* @__PURE__ */ new Map();
|
|
1087
|
+
for (const event of session.snapshotEvents()) {
|
|
1088
|
+
if (event.type !== "tool/code-dispatch-start" && event.type !== "tool/code-dispatch") continue;
|
|
1089
|
+
const subCallId = String(event.data.subCallId);
|
|
1090
|
+
const state = bySubCallId.get(subCallId) ?? {
|
|
1091
|
+
starts: [],
|
|
1092
|
+
settles: []
|
|
1093
|
+
};
|
|
1094
|
+
if (event.type === "tool/code-dispatch-start") state.starts.push({
|
|
1095
|
+
seq: event.seq,
|
|
1096
|
+
data: event.data
|
|
1097
|
+
});
|
|
1098
|
+
else state.settles.push({
|
|
1099
|
+
seq: event.seq,
|
|
1100
|
+
data: event.data
|
|
1101
|
+
});
|
|
1102
|
+
bySubCallId.set(subCallId, state);
|
|
1103
|
+
}
|
|
1104
|
+
const associations = /* @__PURE__ */ new Map();
|
|
1105
|
+
const associationFor = (parentCallId) => {
|
|
1106
|
+
const existing = associations.get(parentCallId);
|
|
1107
|
+
if (existing !== void 0) return existing;
|
|
1108
|
+
const created = {
|
|
1109
|
+
children: [],
|
|
1110
|
+
invalid: false
|
|
1111
|
+
};
|
|
1112
|
+
associations.set(parentCallId, created);
|
|
1113
|
+
return created;
|
|
1114
|
+
};
|
|
1115
|
+
for (const state of bySubCallId.values()) {
|
|
1116
|
+
const parentCallIds = /* @__PURE__ */ new Set([...state.starts.map((event) => String(event.data.parentCallId)), ...state.settles.map((event) => String(event.data.parentCallId))]);
|
|
1117
|
+
for (const parentCallId of parentCallIds) {
|
|
1118
|
+
const association = associationFor(parentCallId);
|
|
1119
|
+
const start = state.starts[0];
|
|
1120
|
+
const settle = state.settles[0];
|
|
1121
|
+
if (state.starts.length !== 1 || state.settles.length !== 1 || start === void 0 || settle === void 0 || String(start.data.parentCallId) !== parentCallId || String(settle.data.parentCallId) !== parentCallId || String(start.data.rootCallId) !== String(settle.data.rootCallId) || settle.seq <= start.seq || start.data.name !== settle.data.name || !isDeepStrictEqual(start.data.arguments, settle.data.arguments)) {
|
|
1122
|
+
association.invalid = true;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
association.children.push({
|
|
1126
|
+
startSeq: start.seq,
|
|
1127
|
+
settleSeq: settle.seq,
|
|
1128
|
+
subCallId: String(settle.data.subCallId),
|
|
1129
|
+
name: settle.data.name,
|
|
1130
|
+
arguments: settle.data.arguments,
|
|
1131
|
+
isError: settle.data.isError,
|
|
1132
|
+
content: settle.data.content
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
for (const association of associations.values()) association.children.sort((left, right) => left.startSeq - right.startSeq);
|
|
1137
|
+
return associations;
|
|
1138
|
+
}
|
|
1139
|
+
function dshToolEvidence(session) {
|
|
1140
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
1141
|
+
const forCall = (callId) => {
|
|
1142
|
+
const existing = evidence.get(callId);
|
|
1143
|
+
if (existing !== void 0) return existing;
|
|
1144
|
+
const created = {
|
|
1145
|
+
calls: [],
|
|
1146
|
+
results: []
|
|
1147
|
+
};
|
|
1148
|
+
evidence.set(callId, created);
|
|
1149
|
+
return created;
|
|
1150
|
+
};
|
|
1151
|
+
for (const event of session.snapshotEvents()) if (event.type === "tool/call") forCall(String(event.data.callId)).calls.push({
|
|
1152
|
+
name: event.data.name,
|
|
1153
|
+
arguments: event.data.arguments
|
|
1154
|
+
});
|
|
1155
|
+
else if (event.type === "tool/result") {
|
|
1156
|
+
const block = event.data.message.content[0];
|
|
1157
|
+
forCall(String(event.data.message.source.callId)).results.push({
|
|
1158
|
+
isError: block?.isError === true,
|
|
1159
|
+
content: block?.content ?? []
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
return evidence;
|
|
1163
|
+
}
|
|
1164
|
+
function serializedToolArguments(value) {
|
|
1165
|
+
try {
|
|
1166
|
+
return JSON.stringify(value ?? {}) ?? "{}";
|
|
1167
|
+
} catch {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function codeDispatchCallMatches(projected, projectedIndex, child, evidence) {
|
|
1172
|
+
const call = projected[projectedIndex]?.item;
|
|
1173
|
+
const callFields = call === void 0 ? void 0 : functionCallFields(call);
|
|
1174
|
+
const tool = evidence.get(child.subCallId);
|
|
1175
|
+
const expectedArguments = serializedToolArguments(child.arguments);
|
|
1176
|
+
const expectedInput = child.name === "apply_patch" ? (() => {
|
|
1177
|
+
try {
|
|
1178
|
+
const value = JSON.parse(serializedToolArguments(child.arguments));
|
|
1179
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && typeof value.patch === "string" && Object.keys(value).length === 1 ? value.patch : void 0;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
})() : serializedToolArguments(child.arguments);
|
|
1184
|
+
return (call?.type === "function_call" || call?.type === "custom_tool_call") && callFields !== void 0 && callFields.call_id === historyToolCallId(child.subCallId) && callFields.name === child.name && expectedInput !== void 0 && callFields.input === expectedInput && expectedArguments !== void 0 && tool !== void 0 && tool.calls.length === 1 && tool.results.length === 1 && tool.calls[0].name === child.name && tool.calls[0].arguments === expectedArguments;
|
|
1185
|
+
}
|
|
1186
|
+
function codeDispatchResultMatches(projected, projectedIndex, child, evidence) {
|
|
1187
|
+
const output = projected[projectedIndex]?.item;
|
|
1188
|
+
const outputFields = output === void 0 ? void 0 : functionCallOutputFields(output);
|
|
1189
|
+
const tool = evidence.get(child.subCallId);
|
|
1190
|
+
return (output?.type === "function_call_output" || output?.type === "custom_tool_call_output") && outputFields !== void 0 && outputFields.call_id === historyToolCallId(child.subCallId) && tool !== void 0 && tool.calls.length === 1 && tool.results.length === 1 && tool.results[0].isError === child.isError && isDeepStrictEqual(tool.results[0].content, child.content);
|
|
1191
|
+
}
|
|
1192
|
+
function codeDispatchProjectedCount(projected, projectedIndex, association, evidence) {
|
|
1193
|
+
if (association.invalid || association.children.length === 0) throw new Error("Nanocodex compaction retained Code Mode child tool association is incomplete");
|
|
1194
|
+
const expectations = association.children.flatMap((child) => [{
|
|
1195
|
+
seq: child.startSeq,
|
|
1196
|
+
kind: "call",
|
|
1197
|
+
child
|
|
1198
|
+
}, {
|
|
1199
|
+
seq: child.settleSeq,
|
|
1200
|
+
kind: "result",
|
|
1201
|
+
child
|
|
1202
|
+
}]).sort((left, right) => left.seq - right.seq);
|
|
1203
|
+
let index = projectedIndex;
|
|
1204
|
+
for (const expectation of expectations) {
|
|
1205
|
+
if (!(expectation.kind === "call" ? codeDispatchCallMatches(projected, index, expectation.child, evidence) : codeDispatchResultMatches(projected, index, expectation.child, evidence))) throw new Error("Nanocodex compaction retained Code Mode child tool does not match the DSH pair");
|
|
1206
|
+
index += 1;
|
|
1207
|
+
}
|
|
1208
|
+
return index - projectedIndex;
|
|
1209
|
+
}
|
|
1210
|
+
function mergeSupplementaryContext(entries) {
|
|
1211
|
+
const first = entries[0];
|
|
1212
|
+
if (first === void 0 || first.item.type !== "message" || first.item.role !== "user" || first.message.role !== "user" || first.message.source.kind !== "user") return;
|
|
1213
|
+
const pluginMessages = entries.slice(1).map((entry) => entry.message);
|
|
1214
|
+
if (pluginMessages.some((message) => message.role !== "user" || message.source.kind !== "plugin")) return;
|
|
1215
|
+
const text = pluginMessages.map((message) => plainText([message])).filter(Boolean).join("\n\n");
|
|
1216
|
+
if (!text) return void 0;
|
|
1217
|
+
return {
|
|
1218
|
+
...first.item,
|
|
1219
|
+
content: [...first.item.content, {
|
|
1220
|
+
type: "input_text",
|
|
1221
|
+
text
|
|
1222
|
+
}]
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function isRealUserHistoryItem(entry) {
|
|
1226
|
+
return entry !== void 0 && entry.item.type === "message" && entry.item.role === "user" && entry.message.role === "user" && entry.message.source.kind === "user";
|
|
1227
|
+
}
|
|
1228
|
+
function isPluginUserHistoryItem(entry) {
|
|
1229
|
+
return entry !== void 0 && entry.item.type === "message" && entry.item.role === "user" && entry.message.role === "user" && entry.message.source.kind === "plugin";
|
|
1230
|
+
}
|
|
1231
|
+
function supplementaryGroupEnd(projected, start) {
|
|
1232
|
+
let end = start + 1;
|
|
1233
|
+
while (isPluginUserHistoryItem(projected[end])) end += 1;
|
|
1234
|
+
return end;
|
|
1235
|
+
}
|
|
1236
|
+
function projectedCountForContext(projected, projectedIndex, contextItem) {
|
|
1237
|
+
const candidate = projected[projectedIndex];
|
|
1238
|
+
if (candidate === void 0 || contextItem === void 0) return void 0;
|
|
1239
|
+
if (!isRealUserHistoryItem(candidate)) return historyItemsMatch(contextItem, candidate.item) ? 1 : void 0;
|
|
1240
|
+
const groupEnd = supplementaryGroupEnd(projected, projectedIndex);
|
|
1241
|
+
const merged = mergeSupplementaryContext(projected.slice(projectedIndex, groupEnd));
|
|
1242
|
+
if (merged !== void 0 && historyMessageMatches(contextItem, merged)) return groupEnd - projectedIndex;
|
|
1243
|
+
return historyItemsMatch(contextItem, candidate.item) ? 1 : void 0;
|
|
1244
|
+
}
|
|
1245
|
+
/** The only provider-facing owner in the DSH Nanocodex package. */
|
|
1246
|
+
var NanocodexEngine = class {
|
|
1247
|
+
ctx;
|
|
1248
|
+
checkpoints;
|
|
1249
|
+
quickJsPromise;
|
|
1250
|
+
requestSurfaceGeneration = /* @__PURE__ */ new WeakMap();
|
|
1251
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
1252
|
+
constructor(ctx, checkpoints) {
|
|
1253
|
+
this.ctx = ctx;
|
|
1254
|
+
this.checkpoints = checkpoints;
|
|
1255
|
+
}
|
|
1256
|
+
quickJs() {
|
|
1257
|
+
this.quickJsPromise ??= (async () => {
|
|
1258
|
+
const asyncQuickJsVariant = (await import("@jitl/quickjs-wasmfile-release-asyncify")).default;
|
|
1259
|
+
const quickJs = await newQuickJSAsyncWASMModuleFromVariant(asyncQuickJsVariant);
|
|
1260
|
+
return createQuickJsEvaluator(quickJs);
|
|
1261
|
+
})();
|
|
1262
|
+
return this.quickJsPromise;
|
|
1263
|
+
}
|
|
1264
|
+
async run(agent, previousMessages, messages, assembly, turn, step, signal, advanceStep) {
|
|
1265
|
+
signal.throwIfAborted();
|
|
1266
|
+
const route = await resolveNanocodexRoute(this.ctx, agent.options);
|
|
1267
|
+
signal.throwIfAborted();
|
|
1268
|
+
const historySeed = await buildHistorySeed(previousMessages, this.ctx, signal);
|
|
1269
|
+
const preTurnSurfaceSeqs = surfacePrefix(agent.session, previousMessages.length);
|
|
1270
|
+
const input = await buildPromptInput(messages.filter((message) => message.source.kind !== "plugin"), this.ctx, signal);
|
|
1271
|
+
const system = renderPrompt(assembly);
|
|
1272
|
+
const quickJs = await this.quickJs();
|
|
1273
|
+
signal.throwIfAborted();
|
|
1274
|
+
const output = new NanocodexOutput(agent, route.provider, route.model, turn, step, advanceStep);
|
|
1275
|
+
let projection = Promise.resolve();
|
|
1276
|
+
const active = {
|
|
1277
|
+
output,
|
|
1278
|
+
drain: () => projection
|
|
1279
|
+
};
|
|
1280
|
+
const visibleToolNames = new Set(assembly.tools.map((tool) => tool.name));
|
|
1281
|
+
const toolKey = JSON.stringify(assembly.tools);
|
|
1282
|
+
const key = runtimeKey(agent);
|
|
1283
|
+
let runtime = this.runtimes.get(key);
|
|
1284
|
+
if (runtime !== void 0 && (runtime.agent !== agent || runtime.routeKey !== routeKey(route, agent) || runtime.system !== system || runtime.toolKey !== toolKey || runtime.boundary === void 0 || !boundaryMatches(runtime.boundary, agent.session, previousMessages))) {
|
|
1285
|
+
await this.closeRuntime(runtime);
|
|
1286
|
+
this.runtimes.delete(key);
|
|
1287
|
+
runtime = void 0;
|
|
1288
|
+
}
|
|
1289
|
+
if (runtime === void 0) {
|
|
1290
|
+
const bridge = createToolBridge({
|
|
1291
|
+
tools: this.ctx.tools,
|
|
1292
|
+
agent,
|
|
1293
|
+
signal,
|
|
1294
|
+
customDefinitions: RAW_APPLICATION_DEFINITIONS,
|
|
1295
|
+
callbacks: {
|
|
1296
|
+
onCall: async ({ id, name, arguments: argumentsText, parentCallId }) => {
|
|
1297
|
+
const current = runtime?.active;
|
|
1298
|
+
if (current === void 0) throw new Error("Nanocodex tool call has no active DSH step");
|
|
1299
|
+
await current.drain();
|
|
1300
|
+
if (parentCallId !== void 0) return void 0;
|
|
1301
|
+
return agent.session.append("tool/call", {
|
|
1302
|
+
...current.output.coordinates,
|
|
1303
|
+
callId: ToolCallId(id),
|
|
1304
|
+
name,
|
|
1305
|
+
arguments: argumentsText
|
|
1306
|
+
}).seq;
|
|
1307
|
+
},
|
|
1308
|
+
onResult: async ({ id, callSeq, result }) => {
|
|
1309
|
+
const current = runtime?.active;
|
|
1310
|
+
if (current !== void 0) await current.drain();
|
|
1311
|
+
if (callSeq !== void 0) {
|
|
1312
|
+
const call = agent.session.eventAt(callSeq);
|
|
1313
|
+
if (call?.type !== "tool/call") throw new Error("Nanocodex tool result has no durable call");
|
|
1314
|
+
this.appendToolResult(agent, call.data.turn, call.data.step, id, callSeq, result);
|
|
1315
|
+
}
|
|
1316
|
+
for (const context of result.additionalContexts ?? []) agent.inject(context);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}).filter((tool) => visibleToolNames.has(tool.name));
|
|
1320
|
+
const checkpoint = findMatchingCheckpoint(this.checkpoints, agent.session, previousMessages, route.provider, route.model);
|
|
1321
|
+
runtime = {
|
|
1322
|
+
agent,
|
|
1323
|
+
nodeAgent: await this.createNodeAgent(agent, route, historySeed, system, bridge, quickJs, checkpoint?.snapshot),
|
|
1324
|
+
provider: route.provider,
|
|
1325
|
+
model: route.model,
|
|
1326
|
+
routeKey: routeKey(route, agent),
|
|
1327
|
+
system,
|
|
1328
|
+
toolKey,
|
|
1329
|
+
bridge,
|
|
1330
|
+
consumedCompactionRevisions: /* @__PURE__ */ new Set()
|
|
1331
|
+
};
|
|
1332
|
+
this.runtimes.set(key, runtime);
|
|
1333
|
+
}
|
|
1334
|
+
runtime.active = active;
|
|
1335
|
+
const automaticCompactions = [];
|
|
1336
|
+
this.appendRequestFacts(agent, route.provider, route.model, agent.options.reasoningEffort, system, runtime.bridge);
|
|
1337
|
+
const nodeAgent = runtime.nodeAgent;
|
|
1338
|
+
const watcher = nodeAgent.events.watch();
|
|
1339
|
+
const removeListener = watcher.onEvent((event) => {
|
|
1340
|
+
projection = projection.then(async () => {
|
|
1341
|
+
await output.accept(event);
|
|
1342
|
+
const replaced = parseCompactionReplacedEvent(event);
|
|
1343
|
+
if (replaced !== void 0 && replaced.trigger === "automatic" && this.consumeCompactionOutcome(runtime, replaced)) automaticCompactions.push({
|
|
1344
|
+
outcome: replaced,
|
|
1345
|
+
phase: replaced.phase,
|
|
1346
|
+
afterModelCallIndex: replaced.after_model_call_index,
|
|
1347
|
+
admittedSurfaceSeqs: replaced.phase === "pre_turn" ? preTurnSurfaceSeqs : [...agent.session.surface.nodes]
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
projection.catch(() => modelTurn.cancel().catch(() => void 0));
|
|
1351
|
+
});
|
|
1352
|
+
const removeTransportFallback = observeTransportFallback(this.ctx, String(agent.id), nodeAgent);
|
|
1353
|
+
const modelTurn = nodeAgent.turn.prompt({
|
|
1354
|
+
input,
|
|
1355
|
+
...supplementaryContext(messages) ? { supplementaryContext: supplementaryContext(messages) } : {}
|
|
1356
|
+
});
|
|
1357
|
+
const abort = () => {
|
|
1358
|
+
modelTurn.cancel().catch(() => void 0);
|
|
1359
|
+
};
|
|
1360
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1361
|
+
let discardRuntime = false;
|
|
1362
|
+
try {
|
|
1363
|
+
await modelTurn.accepted();
|
|
1364
|
+
const result = await modelTurn.result();
|
|
1365
|
+
try {
|
|
1366
|
+
await projection;
|
|
1367
|
+
signal.throwIfAborted();
|
|
1368
|
+
output.finish(result.finalMessage);
|
|
1369
|
+
const snapshot = await result.snapshot();
|
|
1370
|
+
return {
|
|
1371
|
+
provider: route.provider,
|
|
1372
|
+
model: route.model,
|
|
1373
|
+
snapshot,
|
|
1374
|
+
automaticCompactions
|
|
1375
|
+
};
|
|
1376
|
+
} finally {
|
|
1377
|
+
result.dispose();
|
|
1378
|
+
}
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
discardRuntime = true;
|
|
1381
|
+
let failure = error;
|
|
1382
|
+
try {
|
|
1383
|
+
await projection;
|
|
1384
|
+
} catch (projectionError) {
|
|
1385
|
+
failure = projectionError;
|
|
1386
|
+
}
|
|
1387
|
+
output.interrupt(failure);
|
|
1388
|
+
throw failure;
|
|
1389
|
+
} finally {
|
|
1390
|
+
signal.removeEventListener("abort", abort);
|
|
1391
|
+
removeListener();
|
|
1392
|
+
watcher.off();
|
|
1393
|
+
removeTransportFallback();
|
|
1394
|
+
modelTurn.dispose();
|
|
1395
|
+
runtime.active = void 0;
|
|
1396
|
+
if (discardRuntime) {
|
|
1397
|
+
this.runtimes.delete(key);
|
|
1398
|
+
await this.closeRuntime(runtime).catch(() => void 0);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
/** Release the retained Nanocodex host after DSH has drained the Agent. */
|
|
1403
|
+
async dispose(agent) {
|
|
1404
|
+
const key = runtimeKey(agent);
|
|
1405
|
+
const runtime = this.runtimes.get(key);
|
|
1406
|
+
if (runtime === void 0) return;
|
|
1407
|
+
this.runtimes.delete(key);
|
|
1408
|
+
await this.closeRuntime(runtime);
|
|
1409
|
+
}
|
|
1410
|
+
/** Invalidate a runtime whose private context no longer matches DSH. */
|
|
1411
|
+
async invalidate(agent) {
|
|
1412
|
+
await this.dispose(agent);
|
|
1413
|
+
}
|
|
1414
|
+
/** Mark the retained runtime at the current authoritative DSH surface. */
|
|
1415
|
+
markSurfaceBoundary(agent) {
|
|
1416
|
+
const runtime = this.runtimes.get(runtimeKey(agent));
|
|
1417
|
+
if (runtime !== void 0) runtime.boundary = makeBoundary(agent.session, agent.session.deriveMessages());
|
|
1418
|
+
}
|
|
1419
|
+
consumeCompactionOutcome(runtime, outcome) {
|
|
1420
|
+
if (runtime.consumedCompactionRevisions.has(outcome.revision)) return false;
|
|
1421
|
+
runtime.consumedCompactionRevisions.add(outcome.revision);
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
runtimeFor(agent) {
|
|
1425
|
+
const runtime = this.runtimes.get(runtimeKey(agent));
|
|
1426
|
+
if (runtime === void 0 || runtime.agent !== agent) throw new Error("Nanocodex compaction requires the agent's current live runtime");
|
|
1427
|
+
return runtime;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Map Nanocodex's exact retained-tail identities to the current DSH surface.
|
|
1431
|
+
* The engine owns the selection; DSH never guesses from a fixed message count.
|
|
1432
|
+
*/
|
|
1433
|
+
async mapCompactionOutcome(agent, outcome, signal, mapping) {
|
|
1434
|
+
signal.throwIfAborted();
|
|
1435
|
+
const range = outcome.replaced_history;
|
|
1436
|
+
const retained = outcome.retained_tail;
|
|
1437
|
+
if (range.start !== 0 || range.end <= 0 || retained.length === 0 || retained[0]?.index !== range.end || retained.some((item, index) => item.index !== range.end + index)) throw new Error("Nanocodex compaction returned a non-contiguous retained history boundary");
|
|
1438
|
+
const nodes = [...agent.session.surface.nodes];
|
|
1439
|
+
const messages = agent.session.deriveMessages();
|
|
1440
|
+
if (nodes.length !== messages.length) throw new Error("Nanocodex compaction cannot map a DSH surface with mismatched messages");
|
|
1441
|
+
const admittedSurfaceSeqs = mapping === void 0 ? void 0 : new Set(mapping.admittedSurfaceSeqs);
|
|
1442
|
+
const historyProjection = await buildHistoryProjection(messages, this.ctx, signal);
|
|
1443
|
+
const surfaceIndexByMessageId = new Map(messages.map((message, index) => [String(message.id), index]));
|
|
1444
|
+
const projected = [];
|
|
1445
|
+
for (const projectedItem of historyProjection.items) {
|
|
1446
|
+
signal.throwIfAborted();
|
|
1447
|
+
const surfaceIndex = surfaceIndexByMessageId.get(String(projectedItem.message.id));
|
|
1448
|
+
if (surfaceIndex === void 0) continue;
|
|
1449
|
+
const surfaceSeq = nodes[surfaceIndex];
|
|
1450
|
+
if (surfaceSeq === void 0 || admittedSurfaceSeqs !== void 0 && !admittedSurfaceSeqs.has(surfaceSeq)) continue;
|
|
1451
|
+
projected.push({
|
|
1452
|
+
surfaceIndex,
|
|
1453
|
+
message: projectedItem.message,
|
|
1454
|
+
item: projectedItem.item
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
const contextHistory = outcome.context.history;
|
|
1458
|
+
const retainedContextStart = contextHistory.length - retained.length;
|
|
1459
|
+
if (retainedContextStart < 0) throw new Error("Nanocodex compaction context does not contain the retained tail");
|
|
1460
|
+
const retainedContext = contextHistory.slice(retainedContextStart);
|
|
1461
|
+
if (retainedContext.length !== retained.length) throw new Error("Nanocodex compaction context does not contain the retained tail");
|
|
1462
|
+
const directPatchCallIds = validateRetainedApplyPatchPairs(retainedContext);
|
|
1463
|
+
const surfaceCustomCallIds = new Set(projected.flatMap(({ item }) => item.type === "custom_tool_call" ? [item.call_id] : []));
|
|
1464
|
+
let firstIndex = -1;
|
|
1465
|
+
const firstIdentityPosition = retained.findIndex((identity, index) => {
|
|
1466
|
+
const contextItem = retainedContext[index];
|
|
1467
|
+
return identity.kind === "message" && contextItem?.type === "message" && contextItem.role === "user";
|
|
1468
|
+
});
|
|
1469
|
+
const firstIdentity = firstIdentityPosition < 0 ? void 0 : retained[firstIdentityPosition];
|
|
1470
|
+
const firstContext = firstIdentityPosition < 0 ? void 0 : retainedContext[firstIdentityPosition];
|
|
1471
|
+
if (firstIdentity === void 0 || firstContext === void 0 || !projectedIdentityMatches(firstIdentity, firstContext)) throw new Error("Nanocodex compaction retained tail does not begin at a real DSH user message");
|
|
1472
|
+
for (let index = projected.length - 1; index >= 0; index -= 1) {
|
|
1473
|
+
const candidate = projected[index];
|
|
1474
|
+
if (!isRealUserHistoryItem(candidate) || !projectedIdentityMatches(firstIdentity, candidate.item)) continue;
|
|
1475
|
+
if (projectedCountForContext(projected, index, firstContext) !== void 0) {
|
|
1476
|
+
firstIndex = index;
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
const first = firstIndex < 0 ? void 0 : projected[firstIndex];
|
|
1481
|
+
if (first === void 0 || !projectedIdentityMatches(firstIdentity, first.item) || first.surfaceIndex === 0 || !isRealUserHistoryItem(first)) throw new Error("Nanocodex compaction retained tail does not begin at a real DSH user message");
|
|
1482
|
+
const codeDispatches = codeDispatchAssociations(agent.session);
|
|
1483
|
+
const toolEvidence = dshToolEvidence(agent.session);
|
|
1484
|
+
let projectedIndex = firstIndex;
|
|
1485
|
+
let pendingCustomCallId;
|
|
1486
|
+
for (let retainedPosition = 0; retainedPosition < retained.length; retainedPosition += 1) {
|
|
1487
|
+
const identity = retained[retainedPosition];
|
|
1488
|
+
const contextItem = retainedContext[retainedPosition];
|
|
1489
|
+
const contextCallId = "call_id" in contextItem ? contextItem.call_id : void 0;
|
|
1490
|
+
const surfaceCustom = contextCallId !== void 0 && (directPatchCallIds.has(contextCallId) || surfaceCustomCallIds.has(contextCallId));
|
|
1491
|
+
const dshBacked = identity.kind === "message" || identity.kind === "function_call" || identity.kind === "function_call_output" || surfaceCustom;
|
|
1492
|
+
const engineTool = !surfaceCustom && (identity.kind === "custom_tool_call" || identity.kind === "custom_tool_call_output");
|
|
1493
|
+
if (!dshBacked && !engineTool) {
|
|
1494
|
+
if (pendingCustomCallId !== void 0) throw new Error("Nanocodex compaction retained Code Mode tool pair is incomplete");
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (!projectedIdentityMatches(identity, retainedContext[retainedPosition])) throw new Error("Nanocodex compaction retained history does not match its public context");
|
|
1498
|
+
if (engineTool) {
|
|
1499
|
+
if (identity.kind === "custom_tool_call") {
|
|
1500
|
+
if (pendingCustomCallId !== void 0) throw new Error("Nanocodex compaction retained Code Mode tool pair is incomplete");
|
|
1501
|
+
if (typeof identity.call_id === "string") {
|
|
1502
|
+
const association = codeDispatches.get(identity.call_id);
|
|
1503
|
+
if (association !== void 0) projectedIndex += codeDispatchProjectedCount(projected, projectedIndex, association, toolEvidence);
|
|
1504
|
+
}
|
|
1505
|
+
pendingCustomCallId = identity.call_id;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
if (pendingCustomCallId !== identity.call_id) throw new Error("Nanocodex compaction retained Code Mode tool pair is incomplete");
|
|
1509
|
+
pendingCustomCallId = void 0;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (pendingCustomCallId !== void 0) throw new Error("Nanocodex compaction retained Code Mode tool pair is incomplete");
|
|
1513
|
+
const candidate = projected[projectedIndex];
|
|
1514
|
+
const projectedCount = candidate === void 0 ? void 0 : projectedCountForContext(projected, projectedIndex, retainedContext[retainedPosition]);
|
|
1515
|
+
if (candidate === void 0 || !projectedIdentityMatches(identity, candidate.item)) throw new Error("Nanocodex compaction retained history order does not match the active DSH surface");
|
|
1516
|
+
if (candidate.surfaceIndex < first.surfaceIndex) throw new Error("Nanocodex compaction retained tail reaches before its DSH user boundary");
|
|
1517
|
+
if (projectedCount === void 0) throw new Error("Nanocodex compaction retained history order does not match the active DSH surface");
|
|
1518
|
+
projectedIndex += projectedCount;
|
|
1519
|
+
}
|
|
1520
|
+
if (pendingCustomCallId !== void 0) throw new Error("Nanocodex compaction retained Code Mode tool pair is incomplete");
|
|
1521
|
+
if (projectedIndex !== projected.length) throw new Error("Nanocodex compaction retained tail omits an active DSH history item");
|
|
1522
|
+
const start = nodes[0];
|
|
1523
|
+
const end = nodes[first.surfaceIndex - 1];
|
|
1524
|
+
if (start === void 0 || end === void 0 || !toolPairingBalancedBefore(agent.session, start) || !toolPairingBalancedAfter(agent.session, end)) throw new Error("Nanocodex compaction retained boundary splits a DSH tool exchange");
|
|
1525
|
+
return {
|
|
1526
|
+
shadowedRange: {
|
|
1527
|
+
start,
|
|
1528
|
+
end
|
|
1529
|
+
},
|
|
1530
|
+
shadowedSeqs: nodes.slice(0, first.surfaceIndex)
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
async resolveCompactionInstruction(agent, route, _context, signal) {
|
|
1534
|
+
signal.throwIfAborted();
|
|
1535
|
+
const sessionId = String(agent.session.id);
|
|
1536
|
+
const options = {
|
|
1537
|
+
provider: route.provider,
|
|
1538
|
+
model: route.model,
|
|
1539
|
+
messages: [createUserMessage({
|
|
1540
|
+
content: [{
|
|
1541
|
+
type: "text",
|
|
1542
|
+
text: DEFAULT_COMPACTION_INSTRUCTION
|
|
1543
|
+
}],
|
|
1544
|
+
source: {
|
|
1545
|
+
kind: "plugin",
|
|
1546
|
+
plugin: "dsh-compaction-basic"
|
|
1547
|
+
}
|
|
1548
|
+
})],
|
|
1549
|
+
...agent.options.reasoningEffort === void 0 ? {} : { reasoningEffort: agent.options.reasoningEffort },
|
|
1550
|
+
sessionId,
|
|
1551
|
+
purpose: "compaction",
|
|
1552
|
+
signal
|
|
1553
|
+
};
|
|
1554
|
+
const stream = agent.ctx.waterfall(agent.ctx.llm, "llm/stream", options, emptyLlmStream);
|
|
1555
|
+
for await (const _chunk of stream) signal.throwIfAborted();
|
|
1556
|
+
signal.throwIfAborted();
|
|
1557
|
+
const instruction = plainText(options.messages).trim();
|
|
1558
|
+
if (!instruction) throw new Error("Nanocodex compaction instruction is empty");
|
|
1559
|
+
return instruction;
|
|
1560
|
+
}
|
|
1561
|
+
/** Persist the exact successful engine snapshot at the current DSH surface. */
|
|
1562
|
+
async persistCheckpoint(agent, provider, model, snapshot, signal) {
|
|
1563
|
+
signal.throwIfAborted();
|
|
1564
|
+
const session = agent.session;
|
|
1565
|
+
const boundary = makeBoundary(session, session.deriveMessages());
|
|
1566
|
+
const checkpoint = {
|
|
1567
|
+
version: 1,
|
|
1568
|
+
provider,
|
|
1569
|
+
model,
|
|
1570
|
+
boundary,
|
|
1571
|
+
snapshot
|
|
1572
|
+
};
|
|
1573
|
+
await this.checkpoints.put(session.id, checkpoint);
|
|
1574
|
+
const context = {
|
|
1575
|
+
provider,
|
|
1576
|
+
model,
|
|
1577
|
+
contextWindow: MODEL_CONTEXT_WINDOW
|
|
1578
|
+
};
|
|
1579
|
+
session.append("request/context", context);
|
|
1580
|
+
await this.ctx.sessions.flush(session);
|
|
1581
|
+
const runtime = this.runtimes.get(runtimeKey(agent));
|
|
1582
|
+
if (runtime !== void 0) runtime.boundary = boundary;
|
|
1583
|
+
}
|
|
1584
|
+
async createNodeAgent(agent, route, historySeed, system, bridge, quickJs, resume) {
|
|
1585
|
+
const base = {
|
|
1586
|
+
transport: Transport.openAi({
|
|
1587
|
+
apiKey: route.apiKey,
|
|
1588
|
+
...route.apiBaseUrl ? { apiBaseUrl: route.apiBaseUrl } : {},
|
|
1589
|
+
...route.websocketUrl ? { websocketUrl: route.websocketUrl } : {},
|
|
1590
|
+
websocketWarmup: true
|
|
1591
|
+
}),
|
|
1592
|
+
model: route.model,
|
|
1593
|
+
...route.thinking ? { thinking: route.thinking } : {},
|
|
1594
|
+
...agent.options.reasoningEffort === "pro" ? { reasoningMode: "pro" } : {},
|
|
1595
|
+
sessionId: normalizeNanocodexSessionId(String(agent.id)),
|
|
1596
|
+
...system ? { additionalInstructions: system } : {},
|
|
1597
|
+
resolveCompactionInstruction: (context, signal) => this.resolveCompactionInstruction(agent, route, context, signal),
|
|
1598
|
+
codeEvaluator: quickJs,
|
|
1599
|
+
tools: bridge,
|
|
1600
|
+
subagents: false,
|
|
1601
|
+
toolMode: "code"
|
|
1602
|
+
};
|
|
1603
|
+
if (resume !== void 0) try {
|
|
1604
|
+
return await Agent.create({
|
|
1605
|
+
...base,
|
|
1606
|
+
resume
|
|
1607
|
+
});
|
|
1608
|
+
} catch {}
|
|
1609
|
+
return Agent.create({
|
|
1610
|
+
...base,
|
|
1611
|
+
...historySeed.history.length > 0 || historySeed.continuitySummary !== void 0 ? { historySeed } : {}
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
async closeRuntime(runtime) {
|
|
1615
|
+
await runtime.nodeAgent.session.shutdown().catch(() => void 0);
|
|
1616
|
+
runtime.nodeAgent.dispose();
|
|
1617
|
+
}
|
|
1618
|
+
/** Run custom compaction on the already-live Nanocodex runtime. */
|
|
1619
|
+
async compact(agent, signal) {
|
|
1620
|
+
signal.throwIfAborted();
|
|
1621
|
+
const runtime = this.runtimeFor(agent);
|
|
1622
|
+
const watcher = runtime.nodeAgent.events.watch();
|
|
1623
|
+
let eventOutcome;
|
|
1624
|
+
let installed = false;
|
|
1625
|
+
let succeeded = false;
|
|
1626
|
+
const removeListener = watcher.onEvent((event) => {
|
|
1627
|
+
const replaced = parseCompactionReplacedEvent(event);
|
|
1628
|
+
if (replaced === void 0 || replaced.trigger !== "manual") return;
|
|
1629
|
+
installed = true;
|
|
1630
|
+
if (this.consumeCompactionOutcome(runtime, replaced)) eventOutcome = replaced;
|
|
1631
|
+
});
|
|
1632
|
+
const removeTransportFallback = observeTransportFallback(this.ctx, String(agent.id), runtime.nodeAgent);
|
|
1633
|
+
let shutdownRequested = false;
|
|
1634
|
+
const abort = () => {
|
|
1635
|
+
shutdownRequested = true;
|
|
1636
|
+
runtime.nodeAgent.session.shutdown().catch(() => void 0);
|
|
1637
|
+
};
|
|
1638
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1639
|
+
try {
|
|
1640
|
+
signal.throwIfAborted();
|
|
1641
|
+
const returned = await runtime.nodeAgent.session.compact();
|
|
1642
|
+
signal.throwIfAborted();
|
|
1643
|
+
if (returned === null) throw new Error("Nanocodex manual compaction did not produce one matching replacement event");
|
|
1644
|
+
const snapshot = await runtime.nodeAgent.session.snapshot();
|
|
1645
|
+
signal.throwIfAborted();
|
|
1646
|
+
if (eventOutcome === void 0 || returned.revision !== eventOutcome.revision) throw new Error("Nanocodex manual compaction did not produce one matching replacement event");
|
|
1647
|
+
const summary = returned.summary?.trim();
|
|
1648
|
+
if (!summary) throw new Error("Nanocodex compaction returned no summary text");
|
|
1649
|
+
const selection = await this.mapCompactionOutcome(agent, returned, signal);
|
|
1650
|
+
succeeded = true;
|
|
1651
|
+
return {
|
|
1652
|
+
summary,
|
|
1653
|
+
outcome: returned,
|
|
1654
|
+
selection,
|
|
1655
|
+
snapshot,
|
|
1656
|
+
provider: runtime.provider,
|
|
1657
|
+
model: runtime.model
|
|
1658
|
+
};
|
|
1659
|
+
} finally {
|
|
1660
|
+
signal.removeEventListener("abort", abort);
|
|
1661
|
+
removeListener();
|
|
1662
|
+
watcher.off();
|
|
1663
|
+
removeTransportFallback();
|
|
1664
|
+
if (shutdownRequested || installed && !succeeded) await this.invalidate(agent).catch(() => void 0);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
appendRequestFacts(agent, provider, model, reasoningEffort, system, bridge) {
|
|
1668
|
+
const tools = bridge.map((tool) => ({
|
|
1669
|
+
name: tool.name,
|
|
1670
|
+
description: tool.description,
|
|
1671
|
+
parameters: tool.parameters ?? {
|
|
1672
|
+
type: "object",
|
|
1673
|
+
properties: {},
|
|
1674
|
+
additionalProperties: false
|
|
1675
|
+
}
|
|
1676
|
+
}));
|
|
1677
|
+
const header = canonicalHeader({
|
|
1678
|
+
config: {
|
|
1679
|
+
provider,
|
|
1680
|
+
model,
|
|
1681
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
1682
|
+
},
|
|
1683
|
+
...system ? { system } : {},
|
|
1684
|
+
...tools.length > 0 ? { tools } : {}
|
|
1685
|
+
});
|
|
1686
|
+
const session = agent.session;
|
|
1687
|
+
const baseline = session.requestHeader();
|
|
1688
|
+
const hasLiveHeader = session.snapshotEvents(session.firstLiveSeq).some((event) => event.type === "request/header");
|
|
1689
|
+
const previousGeneration = this.requestSurfaceGeneration.get(session);
|
|
1690
|
+
const startsSeries = previousGeneration !== void 0 && previousGeneration !== session.surface.replaceGeneration;
|
|
1691
|
+
if (!hasLiveHeader) session.append("request/header", {
|
|
1692
|
+
header,
|
|
1693
|
+
reason: baseline === void 0 ? "initial" : "resume"
|
|
1694
|
+
});
|
|
1695
|
+
else if (baseline === void 0 || !headerEquals(baseline, header)) session.append("request/header", {
|
|
1696
|
+
header,
|
|
1697
|
+
reason: "change",
|
|
1698
|
+
...startsSeries ? { startsSeries: true } : {}
|
|
1699
|
+
});
|
|
1700
|
+
else if (startsSeries) session.append("request/header", {
|
|
1701
|
+
header,
|
|
1702
|
+
reason: "series"
|
|
1703
|
+
});
|
|
1704
|
+
this.requestSurfaceGeneration.set(session, session.surface.replaceGeneration);
|
|
1705
|
+
const requestContext = {
|
|
1706
|
+
provider,
|
|
1707
|
+
model,
|
|
1708
|
+
contextWindow: MODEL_CONTEXT_WINDOW
|
|
1709
|
+
};
|
|
1710
|
+
const previousContext = session.requestContext();
|
|
1711
|
+
if (previousContext?.provider !== requestContext.provider || previousContext.model !== requestContext.model || previousContext.contextWindow !== requestContext.contextWindow) session.append("request/context", requestContext);
|
|
1712
|
+
}
|
|
1713
|
+
appendToolResult(agent, turn, step, id, callSeq, result) {
|
|
1714
|
+
const message = createToolResultMessage({
|
|
1715
|
+
callId: ToolCallId(id),
|
|
1716
|
+
content: result.content,
|
|
1717
|
+
isError: result.isError
|
|
1718
|
+
});
|
|
1719
|
+
agent.session.append("tool/result", {
|
|
1720
|
+
turn,
|
|
1721
|
+
step,
|
|
1722
|
+
message,
|
|
1723
|
+
...result.error?.info ? { error: result.error.info } : {},
|
|
1724
|
+
...result.meta !== void 0 ? { meta: result.meta } : {}
|
|
1725
|
+
}, {
|
|
1726
|
+
surfaceOp: "append",
|
|
1727
|
+
sourceEventSeqs: [callSeq]
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
//#endregion
|
|
1732
|
+
//#region src/compaction-engine.ts
|
|
1733
|
+
function errorText(error) {
|
|
1734
|
+
return error instanceof Error ? error.message : String(error);
|
|
1735
|
+
}
|
|
1736
|
+
function supportedPrefix(session) {
|
|
1737
|
+
const nodes = [...session.surface.nodes];
|
|
1738
|
+
const messages = session.deriveMessages();
|
|
1739
|
+
if (nodes.length !== messages.length) throw new Error("Nanocodex compaction cannot select a mismatched DSH surface");
|
|
1740
|
+
let retainedIndex = -1;
|
|
1741
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1742
|
+
const message = messages[index];
|
|
1743
|
+
if (message?.role === "user" && message.source.kind === "user") {
|
|
1744
|
+
retainedIndex = index;
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (retainedIndex <= 0) return null;
|
|
1749
|
+
const start = nodes[0];
|
|
1750
|
+
const end = nodes[retainedIndex - 1];
|
|
1751
|
+
if (start === void 0 || end === void 0 || !toolPairingBalancedBefore(session, start) || !toolPairingBalancedAfter(session, end)) throw new Error("Nanocodex compaction prefix would split a DSH tool exchange");
|
|
1752
|
+
return {
|
|
1753
|
+
start,
|
|
1754
|
+
end,
|
|
1755
|
+
shadowedSeqs: nodes.slice(0, retainedIndex)
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function sameSelection(left, right) {
|
|
1759
|
+
return left.start === right.shadowedRange.start && left.end === right.shadowedRange.end && left.shadowedSeqs.length === right.shadowedSeqs.length && left.shadowedSeqs.every((seq, index) => right.shadowedSeqs[index] === seq);
|
|
1760
|
+
}
|
|
1761
|
+
function openTurn(session) {
|
|
1762
|
+
let turn = null;
|
|
1763
|
+
for (const event of session.snapshotEvents()) {
|
|
1764
|
+
if (event.type === "turn/start") turn = event.data.turn;
|
|
1765
|
+
if (event.type === "turn/end" && turn === event.data.turn) turn = null;
|
|
1766
|
+
}
|
|
1767
|
+
return turn;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Minimal DSH compaction owner. The checkpoint is model-private and lands only
|
|
1771
|
+
* as the normal non-expandable compact checkpoint message. The selected range
|
|
1772
|
+
* and all transaction markers remain fully durable in DSH's event log.
|
|
1773
|
+
*/
|
|
1774
|
+
var NanocodexCompactionEngine = class extends CompactionEngine {
|
|
1775
|
+
engine;
|
|
1776
|
+
constructor(ctx, engine) {
|
|
1777
|
+
super(ctx);
|
|
1778
|
+
this.engine = engine;
|
|
1779
|
+
}
|
|
1780
|
+
async compactIfNeeded(agent, _trigger, signal) {
|
|
1781
|
+
signal.throwIfAborted();
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
async compactNow(agent, signal, sourceCommandId) {
|
|
1785
|
+
signal.throwIfAborted();
|
|
1786
|
+
let cancelled = false;
|
|
1787
|
+
let cancellationReason;
|
|
1788
|
+
try {
|
|
1789
|
+
return await agent.runMaintenance(async (agentSignal) => {
|
|
1790
|
+
const operationSignal = AbortSignal.any([agentSignal, signal]);
|
|
1791
|
+
operationSignal.throwIfAborted();
|
|
1792
|
+
const selection = supportedPrefix(agent.session);
|
|
1793
|
+
if (selection === null) return null;
|
|
1794
|
+
let committed = false;
|
|
1795
|
+
try {
|
|
1796
|
+
const result = await this.compactSelection(agent, selection, operationSignal, sourceCommandId);
|
|
1797
|
+
committed = true;
|
|
1798
|
+
await this.ctx.sessions.flush(agent.session);
|
|
1799
|
+
return result;
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
if (committed) await this.engine.invalidate(agent).catch(() => void 0);
|
|
1802
|
+
if (operationSignal.aborted) {
|
|
1803
|
+
cancelled = true;
|
|
1804
|
+
cancellationReason = operationSignal.reason;
|
|
1805
|
+
throw operationSignal.reason;
|
|
1806
|
+
}
|
|
1807
|
+
throw new ManualCompactionError("summary", "Nanocodex could not create a continuity checkpoint", { cause: error });
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
if (cancelled || signal.aborted) throw cancellationReason ?? signal.reason;
|
|
1812
|
+
if (error instanceof ManualCompactionError) throw error;
|
|
1813
|
+
throw new ManualCompactionError("busy", "manual compaction requires an idle agent", { cause: error });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
async compactRegion(start, end, agent, signal = new AbortController().signal) {
|
|
1817
|
+
const maintenanceAgent = agent;
|
|
1818
|
+
if (typeof maintenanceAgent.runMaintenance !== "function") throw new ManualCompactionError("busy", "region compaction requires an idle agent maintenance boundary");
|
|
1819
|
+
const initialNodes = [...agent.session.surface.nodes];
|
|
1820
|
+
const initialStartIndex = initialNodes.indexOf(start);
|
|
1821
|
+
const initialEndIndex = initialNodes.indexOf(end);
|
|
1822
|
+
const initialSupported = supportedPrefix(agent.session);
|
|
1823
|
+
if (initialStartIndex < 0 || initialEndIndex < initialStartIndex || initialSupported === null || initialSupported.start !== start || initialSupported.end !== end || initialSupported.shadowedSeqs.length !== initialEndIndex - initialStartIndex + 1 || initialSupported.shadowedSeqs.some((seq, index) => initialNodes[initialStartIndex + index] !== seq)) throw new ManualCompactionError("changed", "Nanocodex compactRegion only supports the current prefix before the latest real user tail");
|
|
1824
|
+
let cancelled = false;
|
|
1825
|
+
let cancellationReason;
|
|
1826
|
+
try {
|
|
1827
|
+
return await maintenanceAgent.runMaintenance(async (agentSignal) => {
|
|
1828
|
+
const operationSignal = AbortSignal.any([agentSignal, signal]);
|
|
1829
|
+
operationSignal.throwIfAborted();
|
|
1830
|
+
const nodes = [...agent.session.surface.nodes];
|
|
1831
|
+
const startIndex = nodes.indexOf(start);
|
|
1832
|
+
const endIndex = nodes.indexOf(end);
|
|
1833
|
+
if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) throw new Error("Nanocodex compaction range is not a current surface span");
|
|
1834
|
+
const supported = supportedPrefix(agent.session);
|
|
1835
|
+
if (supported === null || supported.start !== start || supported.end !== end || supported.shadowedSeqs.length !== endIndex - startIndex + 1 || supported.shadowedSeqs.some((seq, index) => nodes[startIndex + index] !== seq)) throw new ManualCompactionError("changed", "Nanocodex compactRegion only supports the current prefix before the latest real user tail");
|
|
1836
|
+
let committed = false;
|
|
1837
|
+
try {
|
|
1838
|
+
const result = await this.compactSelection(agent, {
|
|
1839
|
+
start,
|
|
1840
|
+
end,
|
|
1841
|
+
shadowedSeqs: nodes.slice(startIndex, endIndex + 1)
|
|
1842
|
+
}, operationSignal);
|
|
1843
|
+
committed = true;
|
|
1844
|
+
await this.ctx.sessions.flush(agent.session);
|
|
1845
|
+
return result;
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
if (committed) await this.engine.invalidate(agent).catch(() => void 0);
|
|
1848
|
+
if (operationSignal.aborted) {
|
|
1849
|
+
cancelled = true;
|
|
1850
|
+
cancellationReason = operationSignal.reason;
|
|
1851
|
+
throw operationSignal.reason;
|
|
1852
|
+
}
|
|
1853
|
+
throw error;
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (cancelled || signal.aborted) throw cancellationReason ?? signal.reason;
|
|
1858
|
+
if (error instanceof ManualCompactionError) throw error;
|
|
1859
|
+
throw new ManualCompactionError("summary", "Nanocodex could not create a continuity checkpoint", { cause: error });
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Commit a summary produced by Nanocodex's automatic private compaction.
|
|
1864
|
+
* Nanocodex owns the model call; DSH still owns the durable surface and the
|
|
1865
|
+
* non-expandable checkpoint visible to Companion.
|
|
1866
|
+
*/
|
|
1867
|
+
async commitAutomaticSummary(agent, automatic, signal) {
|
|
1868
|
+
signal.throwIfAborted();
|
|
1869
|
+
const summary = automatic.outcome.summary?.trim() ?? "";
|
|
1870
|
+
if (!summary) {
|
|
1871
|
+
await this.engine.invalidate(agent).catch(() => void 0);
|
|
1872
|
+
throw new Error("Nanocodex automatic compaction returned an empty summary");
|
|
1873
|
+
}
|
|
1874
|
+
let selection;
|
|
1875
|
+
try {
|
|
1876
|
+
selection = await this.engine.mapCompactionOutcome(agent, automatic.outcome, signal, automatic);
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
await this.engine.invalidate(agent).catch(() => void 0);
|
|
1879
|
+
throw error;
|
|
1880
|
+
}
|
|
1881
|
+
const attempt = this.beginCompaction(agent);
|
|
1882
|
+
let committed = false;
|
|
1883
|
+
try {
|
|
1884
|
+
const result = this.commitSummary(agent, {
|
|
1885
|
+
start: selection.shadowedRange.start,
|
|
1886
|
+
end: selection.shadowedRange.end,
|
|
1887
|
+
shadowedSeqs: [...selection.shadowedSeqs]
|
|
1888
|
+
}, [{
|
|
1889
|
+
type: "text",
|
|
1890
|
+
text: summary
|
|
1891
|
+
}], signal, attempt);
|
|
1892
|
+
committed = true;
|
|
1893
|
+
return result;
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
this.endFailedCompaction(agent, attempt, error);
|
|
1896
|
+
await this.engine.invalidate(agent).catch(() => void 0);
|
|
1897
|
+
throw error;
|
|
1898
|
+
} finally {
|
|
1899
|
+
if (committed) this.engine.markSurfaceBoundary(agent);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
async compactSelection(agent, selection, signal, sourceCommandId) {
|
|
1903
|
+
signal.throwIfAborted();
|
|
1904
|
+
const attempt = this.beginCompaction(agent, sourceCommandId);
|
|
1905
|
+
let generated = false;
|
|
1906
|
+
try {
|
|
1907
|
+
const result = await this.engine.compact(agent, signal);
|
|
1908
|
+
generated = true;
|
|
1909
|
+
if (!sameSelection(selection, result.selection)) throw new ManualCompactionError("changed", "Nanocodex compacted a different DSH prefix than the requested range");
|
|
1910
|
+
const committed = this.commitSummary(agent, selection, [{
|
|
1911
|
+
type: "text",
|
|
1912
|
+
text: result.summary
|
|
1913
|
+
}], signal, attempt);
|
|
1914
|
+
this.engine.markSurfaceBoundary(agent);
|
|
1915
|
+
await this.engine.persistCheckpoint(agent, result.provider, result.model, result.snapshot, signal);
|
|
1916
|
+
return committed;
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
this.endFailedCompaction(agent, attempt, error);
|
|
1919
|
+
if (generated) await this.engine.invalidate(agent).catch(() => void 0);
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
beginCompaction(agent, sourceCommandId) {
|
|
1924
|
+
const lifecycle = {
|
|
1925
|
+
compactionId: CompactionId(randomUUID()),
|
|
1926
|
+
...sourceCommandId === void 0 ? {} : { sourceCommandId },
|
|
1927
|
+
turn: openTurn(agent.session)
|
|
1928
|
+
};
|
|
1929
|
+
const startEvent = agent.session.append("compaction/start", lifecycle);
|
|
1930
|
+
return {
|
|
1931
|
+
...lifecycle,
|
|
1932
|
+
startSeq: startEvent.seq,
|
|
1933
|
+
ended: false
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
endFailedCompaction(agent, attempt, error) {
|
|
1937
|
+
if (attempt.ended) return;
|
|
1938
|
+
try {
|
|
1939
|
+
agent.session.append("compaction/end", {
|
|
1940
|
+
compactionId: attempt.compactionId,
|
|
1941
|
+
...attempt.sourceCommandId === void 0 ? {} : { sourceCommandId: attempt.sourceCommandId },
|
|
1942
|
+
turn: attempt.turn,
|
|
1943
|
+
error: errorText(error)
|
|
1944
|
+
});
|
|
1945
|
+
} catch {} finally {
|
|
1946
|
+
attempt.ended = true;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
commitSummary(agent, selection, summary, signal, attempt) {
|
|
1950
|
+
signal.throwIfAborted();
|
|
1951
|
+
const provider = agent.options.provider;
|
|
1952
|
+
const model = agent.options.model;
|
|
1953
|
+
if (!provider || !model) throw new Error("Nanocodex compaction requires an explicit provider/model");
|
|
1954
|
+
signal.throwIfAborted();
|
|
1955
|
+
const summaryEvent = agent.session.append("compaction/summary", {
|
|
1956
|
+
compactionId: attempt.compactionId,
|
|
1957
|
+
...attempt.sourceCommandId === void 0 ? {} : { sourceCommandId: attempt.sourceCommandId },
|
|
1958
|
+
summary,
|
|
1959
|
+
rawOutput: summary,
|
|
1960
|
+
shadowedRange: {
|
|
1961
|
+
start: selection.start,
|
|
1962
|
+
end: selection.end
|
|
1963
|
+
},
|
|
1964
|
+
shadowedSeqs: selection.shadowedSeqs,
|
|
1965
|
+
shadowedTokenCount: Math.max(1, Math.ceil(textOfBlocks(summary).length / 4)),
|
|
1966
|
+
provider,
|
|
1967
|
+
model
|
|
1968
|
+
});
|
|
1969
|
+
const checkpoint = createUserMessage({
|
|
1970
|
+
content: summary,
|
|
1971
|
+
source: compactCheckpointSource(attempt.compactionId, attempt.sourceCommandId)
|
|
1972
|
+
});
|
|
1973
|
+
agent.session.append("user/message", checkpoint, {
|
|
1974
|
+
surfaceOp: {
|
|
1975
|
+
op: "replace",
|
|
1976
|
+
start: selection.start,
|
|
1977
|
+
end: selection.end
|
|
1978
|
+
},
|
|
1979
|
+
sourceEventSeqs: [
|
|
1980
|
+
attempt.startSeq,
|
|
1981
|
+
summaryEvent.seq,
|
|
1982
|
+
...selection.shadowedSeqs
|
|
1983
|
+
]
|
|
1984
|
+
});
|
|
1985
|
+
const endEvent = agent.session.append("compaction/end", {
|
|
1986
|
+
compactionId: attempt.compactionId,
|
|
1987
|
+
...attempt.sourceCommandId === void 0 ? {} : { sourceCommandId: attempt.sourceCommandId },
|
|
1988
|
+
turn: attempt.turn
|
|
1989
|
+
});
|
|
1990
|
+
attempt.ended = true;
|
|
1991
|
+
const shadowedTokenCount = Math.max(1, Math.ceil(textOfBlocks(summary).length / 4));
|
|
1992
|
+
return {
|
|
1993
|
+
compactionId: attempt.compactionId,
|
|
1994
|
+
...attempt.sourceCommandId === void 0 ? {} : { sourceCommandId: attempt.sourceCommandId },
|
|
1995
|
+
startSeq: attempt.startSeq,
|
|
1996
|
+
summarySeq: summaryEvent.seq,
|
|
1997
|
+
endSeq: endEvent.seq,
|
|
1998
|
+
summary,
|
|
1999
|
+
shadowedRange: {
|
|
2000
|
+
start: selection.start,
|
|
2001
|
+
end: selection.end
|
|
2002
|
+
},
|
|
2003
|
+
shadowedSeqs: [...selection.shadowedSeqs],
|
|
2004
|
+
shadowedTokenCount
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
function textOfBlocks(blocks) {
|
|
2009
|
+
return blocks.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
|
|
2010
|
+
}
|
|
2011
|
+
//#endregion
|
|
2012
|
+
//#region src/agent.ts
|
|
2013
|
+
const RUNTIME_CONTEXT_SOURCE = "@deepseek-ai/dsh-system-prompt";
|
|
2014
|
+
const CLEARED_RUNTIME_CONTEXT = "Current runtime context: none. Earlier runtime-context snapshots no longer apply.";
|
|
2015
|
+
function deferred() {
|
|
2016
|
+
let resolve;
|
|
2017
|
+
let reject;
|
|
2018
|
+
return {
|
|
2019
|
+
promise: new Promise((resolvePromise, rejectPromise) => {
|
|
2020
|
+
resolve = resolvePromise;
|
|
2021
|
+
reject = rejectPromise;
|
|
2022
|
+
}),
|
|
2023
|
+
resolve,
|
|
2024
|
+
reject
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function lastTurn(session) {
|
|
2028
|
+
let result = 0;
|
|
2029
|
+
for (const event of session.snapshotEvents()) if (event.type === "turn/start") result = Math.max(result, event.data.turn);
|
|
2030
|
+
return result;
|
|
2031
|
+
}
|
|
2032
|
+
function cancelCause(signal) {
|
|
2033
|
+
const reason = signal.reason;
|
|
2034
|
+
if (reason && typeof reason === "object" && "kind" in reason && (reason.kind === "user" || reason.kind === "parent" || reason.kind === "disposed" || reason.kind === "hook")) return reason;
|
|
2035
|
+
return {
|
|
2036
|
+
kind: "hook",
|
|
2037
|
+
reason: reason instanceof Error ? reason.message : String(reason ?? "aborted")
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function errorInfo(error) {
|
|
2041
|
+
return {
|
|
2042
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2043
|
+
code: "UNKNOWN"
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* DSH's public Agent handle backed by Nanocodex turns. DSH owns the queue and
|
|
2048
|
+
* durable log; each model step is hydrated from the active surface.
|
|
2049
|
+
*/
|
|
2050
|
+
var NanocodexAgent = class {
|
|
2051
|
+
id;
|
|
2052
|
+
options;
|
|
2053
|
+
session;
|
|
2054
|
+
inbox;
|
|
2055
|
+
ctx;
|
|
2056
|
+
loopCtx;
|
|
2057
|
+
engine;
|
|
2058
|
+
scope;
|
|
2059
|
+
dispatch;
|
|
2060
|
+
phase;
|
|
2061
|
+
activityDone = Promise.resolve();
|
|
2062
|
+
runtimeContextText;
|
|
2063
|
+
constructor(loopCtx, id, options, session, engine) {
|
|
2064
|
+
this.loopCtx = loopCtx;
|
|
2065
|
+
this.id = id;
|
|
2066
|
+
this.options = options;
|
|
2067
|
+
this.session = session;
|
|
2068
|
+
closeInterruptedToolCalls(session);
|
|
2069
|
+
this.engine = engine;
|
|
2070
|
+
this.dispatch = agentEvents(loopCtx, this);
|
|
2071
|
+
this.inbox = new Inbox(session, {
|
|
2072
|
+
inserted: (message) => this.dispatch.emit("agent/inbox/inserted", { message }),
|
|
2073
|
+
discarded: (message) => this.dispatch.emit("agent/inbox/discarded", { message }),
|
|
2074
|
+
claimed: (message, turn) => this.dispatch.emit("agent/inbox/claimed", {
|
|
2075
|
+
message,
|
|
2076
|
+
turn
|
|
2077
|
+
})
|
|
2078
|
+
});
|
|
2079
|
+
this.scope = createScope(loopCtx, this);
|
|
2080
|
+
this.ctx = this.scope.ctx.extend({ agent: this });
|
|
2081
|
+
this.phase = {
|
|
2082
|
+
kind: "idle",
|
|
2083
|
+
lastTurn: lastTurn(session)
|
|
2084
|
+
};
|
|
2085
|
+
this.runtimeContextText = this.findRuntimeContext();
|
|
2086
|
+
}
|
|
2087
|
+
get status() {
|
|
2088
|
+
return this.phase.kind === "running" ? "running" : "idle";
|
|
2089
|
+
}
|
|
2090
|
+
send(message, target, wakeup) {
|
|
2091
|
+
const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
|
|
2092
|
+
this.inbox.splice(wakingAfterAbort ? "next-turn" : target, Infinity, 0, [message]);
|
|
2093
|
+
if (wakeup) this.wakeDriver(wakingAfterAbort);
|
|
2094
|
+
}
|
|
2095
|
+
followup(message) {
|
|
2096
|
+
this.send(message, "next-turn", true);
|
|
2097
|
+
}
|
|
2098
|
+
steer(message) {
|
|
2099
|
+
this.send(message, "next-step", true);
|
|
2100
|
+
}
|
|
2101
|
+
inject(message) {
|
|
2102
|
+
this.send(message, "next-step", false);
|
|
2103
|
+
}
|
|
2104
|
+
cancel(cause, options = {}) {
|
|
2105
|
+
if (!options.keepInbox) {
|
|
2106
|
+
this.inbox.clear();
|
|
2107
|
+
if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
|
|
2108
|
+
} else if (this.phase.kind !== "idle" && this.inbox.hasPending) this.phase.wakeRequested = true;
|
|
2109
|
+
if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
|
|
2110
|
+
}
|
|
2111
|
+
whenIdle() {
|
|
2112
|
+
return this.waitForIdle();
|
|
2113
|
+
}
|
|
2114
|
+
async waitForIdle() {
|
|
2115
|
+
let observed;
|
|
2116
|
+
do {
|
|
2117
|
+
observed = this.activityDone;
|
|
2118
|
+
await observed;
|
|
2119
|
+
} while (observed !== this.activityDone);
|
|
2120
|
+
}
|
|
2121
|
+
runMaintenance(task) {
|
|
2122
|
+
if (this.phase.kind !== "idle") throw new Error(`agent "${String(this.id)}" already has active work`);
|
|
2123
|
+
const done = deferred();
|
|
2124
|
+
const maintenance = {
|
|
2125
|
+
kind: "maintenance",
|
|
2126
|
+
abort: new AbortController(),
|
|
2127
|
+
lastTurn: this.phase.lastTurn,
|
|
2128
|
+
wakeRequested: false
|
|
2129
|
+
};
|
|
2130
|
+
this.phase = maintenance;
|
|
2131
|
+
this.activityDone = done.promise;
|
|
2132
|
+
return (async () => {
|
|
2133
|
+
try {
|
|
2134
|
+
return await task(maintenance.abort.signal);
|
|
2135
|
+
} finally {
|
|
2136
|
+
this.phase = {
|
|
2137
|
+
kind: "idle",
|
|
2138
|
+
lastTurn: maintenance.lastTurn
|
|
2139
|
+
};
|
|
2140
|
+
if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
|
|
2141
|
+
done.resolve();
|
|
2142
|
+
}
|
|
2143
|
+
})();
|
|
2144
|
+
}
|
|
2145
|
+
wakeDriver(wakeAfterAbort = false) {
|
|
2146
|
+
if (this.phase.kind !== "idle") {
|
|
2147
|
+
if (this.phase.abort.signal.reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) this.phase.wakeRequested = true;
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const done = deferred();
|
|
2151
|
+
const running = {
|
|
2152
|
+
kind: "running",
|
|
2153
|
+
abort: new AbortController(),
|
|
2154
|
+
turn: this.phase.lastTurn,
|
|
2155
|
+
step: 0,
|
|
2156
|
+
wakeRequested: false
|
|
2157
|
+
};
|
|
2158
|
+
this.phase = running;
|
|
2159
|
+
this.activityDone = done.promise;
|
|
2160
|
+
this.dispatch.emit("agent/status", { status: "running" });
|
|
2161
|
+
this.loopCtx.agents.withInitiator(this, () => this.kick()).then(() => done.resolve(), (error) => {
|
|
2162
|
+
this.dispatch.emit("agent/error", {
|
|
2163
|
+
turn: running.turn,
|
|
2164
|
+
step: running.step,
|
|
2165
|
+
error
|
|
2166
|
+
});
|
|
2167
|
+
done.resolve();
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
async dispose() {
|
|
2171
|
+
this.cancel({ kind: "disposed" });
|
|
2172
|
+
await this.waitForIdle();
|
|
2173
|
+
await this.engine.dispose(this);
|
|
2174
|
+
await this.scope.dispose();
|
|
2175
|
+
}
|
|
2176
|
+
async kick() {
|
|
2177
|
+
try {
|
|
2178
|
+
while (await this.turn());
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
const phase = this.phase;
|
|
2181
|
+
this.dispatch.emit("agent/error", {
|
|
2182
|
+
turn: phase.kind === "running" ? phase.turn : phase.lastTurn,
|
|
2183
|
+
step: phase.kind === "running" ? phase.step : 0,
|
|
2184
|
+
error
|
|
2185
|
+
});
|
|
2186
|
+
} finally {
|
|
2187
|
+
if (this.phase.kind === "running") {
|
|
2188
|
+
const { turn, wakeRequested } = this.phase;
|
|
2189
|
+
this.phase = {
|
|
2190
|
+
kind: "idle",
|
|
2191
|
+
lastTurn: turn
|
|
2192
|
+
};
|
|
2193
|
+
this.dispatch.emit("agent/status", { status: "idle" });
|
|
2194
|
+
if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
async preStep(target, turn, step) {
|
|
2199
|
+
if (this.phase.kind !== "running") throw new Error("pre-step outside running phase");
|
|
2200
|
+
const signal = this.phase.abort.signal;
|
|
2201
|
+
const claimed = this.inbox.claim(target, turn);
|
|
2202
|
+
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal));
|
|
2203
|
+
signal.throwIfAborted();
|
|
2204
|
+
const sections = renderContextSections(assembly);
|
|
2205
|
+
this.runtimeContextText = this.findRuntimeContext();
|
|
2206
|
+
const runtime = this.projectRuntimeContext(joinContextSections(sections), sections);
|
|
2207
|
+
const decision = await this.dispatch.waterfall("agent/pre-step", {
|
|
2208
|
+
messages: claimed,
|
|
2209
|
+
turn,
|
|
2210
|
+
step,
|
|
2211
|
+
signal
|
|
2212
|
+
}, () => Promise.resolve({
|
|
2213
|
+
kind: "enter",
|
|
2214
|
+
messages: runtime === void 0 ? claimed : [...claimed, runtime]
|
|
2215
|
+
}));
|
|
2216
|
+
signal.throwIfAborted();
|
|
2217
|
+
return decision.kind === "reject" ? { decision } : {
|
|
2218
|
+
decision,
|
|
2219
|
+
assembly
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
async turn() {
|
|
2223
|
+
if (this.phase.kind !== "running") throw new Error("turn without driver reservation");
|
|
2224
|
+
const phase = this.phase;
|
|
2225
|
+
const signal = phase.abort.signal;
|
|
2226
|
+
signal.throwIfAborted();
|
|
2227
|
+
const turn = phase.turn + 1;
|
|
2228
|
+
phase.turn = turn;
|
|
2229
|
+
this.session.append("turn/start", { turn });
|
|
2230
|
+
let reason = { kind: "completed" };
|
|
2231
|
+
let target = "next-turn";
|
|
2232
|
+
try {
|
|
2233
|
+
while (true) {
|
|
2234
|
+
signal.throwIfAborted();
|
|
2235
|
+
const step = phase.step + 1;
|
|
2236
|
+
const prepared = await this.preStep(target, turn, step);
|
|
2237
|
+
if (prepared.decision.kind === "reject") {
|
|
2238
|
+
reason = { kind: "blocked" };
|
|
2239
|
+
break;
|
|
2240
|
+
}
|
|
2241
|
+
const messages = prepared.decision.messages;
|
|
2242
|
+
if (phase.step === 0 && messages.length === 0) break;
|
|
2243
|
+
signal.throwIfAborted();
|
|
2244
|
+
const previousMessages = this.session.deriveMessages();
|
|
2245
|
+
this.session.append("step/start", {
|
|
2246
|
+
turn,
|
|
2247
|
+
step
|
|
2248
|
+
});
|
|
2249
|
+
phase.step = step;
|
|
2250
|
+
try {
|
|
2251
|
+
for (const message of messages) this.session.append("user/message", message, { surfaceOp: "append" });
|
|
2252
|
+
const engineResult = await this.engine.run(this, previousMessages, messages, prepared.assembly, turn, step, signal, () => {
|
|
2253
|
+
this.session.append("step/end", {
|
|
2254
|
+
turn,
|
|
2255
|
+
step: phase.step
|
|
2256
|
+
});
|
|
2257
|
+
phase.step += 1;
|
|
2258
|
+
this.session.append("step/start", {
|
|
2259
|
+
turn,
|
|
2260
|
+
step: phase.step
|
|
2261
|
+
});
|
|
2262
|
+
return phase.step;
|
|
2263
|
+
});
|
|
2264
|
+
if (engineResult.automaticCompactions.length > 0) {
|
|
2265
|
+
const compaction = this.loopCtx.compaction;
|
|
2266
|
+
if (compaction === void 0 || typeof compaction.commitAutomaticSummary !== "function") throw new Error("Nanocodex automatic compaction has no DSH compaction owner");
|
|
2267
|
+
for (const automatic of engineResult.automaticCompactions) await compaction.commitAutomaticSummary(this, automatic, signal);
|
|
2268
|
+
}
|
|
2269
|
+
try {
|
|
2270
|
+
await this.engine.persistCheckpoint(this, engineResult.provider, engineResult.model, engineResult.snapshot, signal);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
await this.engine.invalidate(this).catch(() => void 0);
|
|
2273
|
+
throw error;
|
|
2274
|
+
}
|
|
2275
|
+
} finally {
|
|
2276
|
+
this.session.append("step/end", {
|
|
2277
|
+
turn,
|
|
2278
|
+
step: phase.step
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
reason = { kind: "completed" };
|
|
2282
|
+
signal.throwIfAborted();
|
|
2283
|
+
if (this.inbox.nextStep.length === 0) {
|
|
2284
|
+
await this.dispatch.serial("agent/turn-stopping", {
|
|
2285
|
+
turn,
|
|
2286
|
+
signal
|
|
2287
|
+
});
|
|
2288
|
+
signal.throwIfAborted();
|
|
2289
|
+
}
|
|
2290
|
+
if (this.inbox.nextStep.length === 0) break;
|
|
2291
|
+
target = "next-step";
|
|
2292
|
+
}
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
if (signal.aborted) reason = {
|
|
2295
|
+
kind: "aborted",
|
|
2296
|
+
reason: cancelCause(signal)
|
|
2297
|
+
};
|
|
2298
|
+
else {
|
|
2299
|
+
reason = {
|
|
2300
|
+
kind: "error",
|
|
2301
|
+
error: errorInfo(error)
|
|
2302
|
+
};
|
|
2303
|
+
this.dispatch.emit("agent/error", {
|
|
2304
|
+
turn,
|
|
2305
|
+
step: phase.step,
|
|
2306
|
+
error
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
} finally {
|
|
2310
|
+
this.session.append("turn/end", {
|
|
2311
|
+
turn,
|
|
2312
|
+
reason
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
if (!signal.aborted && reason.kind === "completed" && this.inbox.hasPending) {
|
|
2316
|
+
phase.abort = new AbortController();
|
|
2317
|
+
phase.step = 0;
|
|
2318
|
+
phase.wakeRequested = false;
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
findRuntimeContext() {
|
|
2324
|
+
for (const message of this.session.deriveMessages().reverse()) if (message.source.kind === "plugin" && message.source.plugin === RUNTIME_CONTEXT_SOURCE && message.content.length === 1 && message.content[0]?.type === "text") return message.content[0].text;
|
|
2325
|
+
}
|
|
2326
|
+
projectRuntimeContext(current, sections) {
|
|
2327
|
+
if (this.runtimeContextText === void 0 && current.length === 0) return void 0;
|
|
2328
|
+
const next = current.length === 0 ? CLEARED_RUNTIME_CONTEXT : current;
|
|
2329
|
+
if (this.runtimeContextText === next) return void 0;
|
|
2330
|
+
this.runtimeContextText = next;
|
|
2331
|
+
return createUserMessage({
|
|
2332
|
+
content: [{
|
|
2333
|
+
type: "text",
|
|
2334
|
+
text: next
|
|
2335
|
+
}],
|
|
2336
|
+
source: sections.length === 0 ? {
|
|
2337
|
+
kind: "plugin",
|
|
2338
|
+
plugin: RUNTIME_CONTEXT_SOURCE
|
|
2339
|
+
} : {
|
|
2340
|
+
kind: "plugin",
|
|
2341
|
+
plugin: RUNTIME_CONTEXT_SOURCE,
|
|
2342
|
+
form: "snapshot",
|
|
2343
|
+
sections
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
//#endregion
|
|
2349
|
+
//#region src/factory.ts
|
|
2350
|
+
/** AgentFactory implementation installed by the selected Cordis row. */
|
|
2351
|
+
var NanocodexFactory = class {
|
|
2352
|
+
ctx;
|
|
2353
|
+
engine;
|
|
2354
|
+
live = /* @__PURE__ */ new Set();
|
|
2355
|
+
closed = false;
|
|
2356
|
+
constructor(ctx, engine) {
|
|
2357
|
+
this.ctx = ctx;
|
|
2358
|
+
this.engine = engine;
|
|
2359
|
+
}
|
|
2360
|
+
async createAgent(ownerCtx, options) {
|
|
2361
|
+
const preparation = SessionPreparation.create(this.ctx.sessions.prepare(options.sessionId, {
|
|
2362
|
+
...options.seed === void 0 ? {} : { seed: options.seed },
|
|
2363
|
+
...options.meta === void 0 ? {} : { meta: options.meta },
|
|
2364
|
+
...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
|
|
2365
|
+
}));
|
|
2366
|
+
try {
|
|
2367
|
+
return await this.publish(ownerCtx, preparation, options.agentOptions ?? {}, options.setup, options.signal, "startup");
|
|
2368
|
+
} finally {
|
|
2369
|
+
preparation[Symbol.dispose]();
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async resume(ownerCtx, options) {
|
|
2373
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
2374
|
+
if (persistence === void 0) throw new Error("cannot resume Nanocodex: dsh-session-persistence is not configured");
|
|
2375
|
+
const preparation = await persistence.prepare(options.resumeSessionId, options.signal);
|
|
2376
|
+
try {
|
|
2377
|
+
return await this.publish(ownerCtx, preparation, options.agentOptions ?? {}, options.setup, options.signal, "resume");
|
|
2378
|
+
} finally {
|
|
2379
|
+
preparation[Symbol.dispose]();
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async disposeAll() {
|
|
2383
|
+
this.closed = true;
|
|
2384
|
+
await Promise.all([...this.live].map((entry) => entry.dispose()));
|
|
2385
|
+
}
|
|
2386
|
+
async publish(ownerCtx, preparation, options, setup, callerSignal, source) {
|
|
2387
|
+
if (this.closed) throw new Error("Nanocodex factory is no longer active");
|
|
2388
|
+
const agent = new NanocodexAgent(this.ctx, preparation.session.id, options ?? {}, preparation.session, this.engine);
|
|
2389
|
+
let detachSession;
|
|
2390
|
+
let detachAgent;
|
|
2391
|
+
let disposed;
|
|
2392
|
+
const dispose = () => {
|
|
2393
|
+
disposed ??= (async () => {
|
|
2394
|
+
agent.cancel({ kind: "disposed" });
|
|
2395
|
+
await agent.whenIdle();
|
|
2396
|
+
detachAgent?.();
|
|
2397
|
+
detachSession?.();
|
|
2398
|
+
await agent.dispose();
|
|
2399
|
+
this.live.delete(entry);
|
|
2400
|
+
})();
|
|
2401
|
+
return disposed;
|
|
2402
|
+
};
|
|
2403
|
+
const entry = {
|
|
2404
|
+
agent,
|
|
2405
|
+
dispose
|
|
2406
|
+
};
|
|
2407
|
+
this.live.add(entry);
|
|
2408
|
+
ownerCtx.effect(() => () => dispose(), `dsh-nanocodex.lifecycle(${String(agent.id)})`);
|
|
2409
|
+
try {
|
|
2410
|
+
if (callerSignal?.aborted) throw callerSignal.reason;
|
|
2411
|
+
const commit = await setup?.(agent.ctx);
|
|
2412
|
+
apply$1(agent.ctx);
|
|
2413
|
+
if (commit) commit.commit();
|
|
2414
|
+
if (callerSignal?.aborted) throw callerSignal.reason;
|
|
2415
|
+
detachSession = agent.ctx.sessions.enter(preparation.session);
|
|
2416
|
+
detachAgent = this.ctx.agents.enter(agent, ownerCtx.agent);
|
|
2417
|
+
agent.ctx.sessions.announce(preparation.session);
|
|
2418
|
+
this.ctx.agents.announce(agent);
|
|
2419
|
+
emitAgentEvent(this.ctx, agent, "agent/session-start", { source });
|
|
2420
|
+
return {
|
|
2421
|
+
agent,
|
|
2422
|
+
dispose
|
|
2423
|
+
};
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
await dispose();
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
//#endregion
|
|
2431
|
+
//#region src/llm-adapter.ts
|
|
2432
|
+
const REASONING_EFFORTS = [
|
|
2433
|
+
["none", "None"],
|
|
2434
|
+
["low", "Low"],
|
|
2435
|
+
["medium", "Medium"],
|
|
2436
|
+
["high", "High"],
|
|
2437
|
+
["xhigh", "Extra high"],
|
|
2438
|
+
["max", "Maximum"],
|
|
2439
|
+
["pro", "Pro"]
|
|
2440
|
+
];
|
|
2441
|
+
function usage(value) {
|
|
2442
|
+
return {
|
|
2443
|
+
inputTokens: value.input_tokens,
|
|
2444
|
+
outputTokens: value.output_tokens,
|
|
2445
|
+
...Number.isFinite(value.total_tokens) ? { totalTokens: value.total_tokens } : {},
|
|
2446
|
+
...Number.isFinite(value.cached_input_tokens) && value.cached_input_tokens > 0 ? { cacheReadTokens: value.cached_input_tokens } : {},
|
|
2447
|
+
...Number.isFinite(value.cache_write_input_tokens) && value.cache_write_input_tokens > 0 ? { cacheWriteTokens: value.cache_write_input_tokens } : {},
|
|
2448
|
+
...Number.isFinite(value.reasoning_output_tokens) && value.reasoning_output_tokens > 0 ? { reasoningTokens: value.reasoning_output_tokens } : {}
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function providerName(provider) {
|
|
2452
|
+
return provider === "openai-codex-responses" ? "OpenAI Codex Responses" : "OpenAI Responses";
|
|
2453
|
+
}
|
|
2454
|
+
function modelName(model) {
|
|
2455
|
+
return model.replaceAll("-", " ").replace(/(^| )([a-z])/gu, (_, p, c) => `${p}${c.toUpperCase()}`);
|
|
2456
|
+
}
|
|
2457
|
+
function isProvider(value) {
|
|
2458
|
+
return value === "openai" || value === "openai-codex-responses";
|
|
2459
|
+
}
|
|
2460
|
+
function defaultReasoning(ctx, provider) {
|
|
2461
|
+
const settings = ctx.settings.get(SETTINGS_NAMESPACE);
|
|
2462
|
+
if (!settings || typeof settings !== "object") return void 0;
|
|
2463
|
+
const providers = settings.providers;
|
|
2464
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) return void 0;
|
|
2465
|
+
const profile = providers[provider];
|
|
2466
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) return void 0;
|
|
2467
|
+
return supportedThinking(profile.reasoning);
|
|
2468
|
+
}
|
|
2469
|
+
/** Native DSH model-catalog adapter for the Nanocodex-owned routes. */
|
|
2470
|
+
var NanocodexLlmAdapter = class extends LlmAdapter {
|
|
2471
|
+
ctx;
|
|
2472
|
+
constructor(ctx) {
|
|
2473
|
+
super();
|
|
2474
|
+
this.ctx = ctx;
|
|
2475
|
+
}
|
|
2476
|
+
providerInfo(provider) {
|
|
2477
|
+
if (!isProvider(provider)) throw new Error(`Nanocodex does not own provider ${JSON.stringify(provider)}`);
|
|
2478
|
+
return {
|
|
2479
|
+
id: provider,
|
|
2480
|
+
name: providerName(provider)
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
async listModels(provider) {
|
|
2484
|
+
this.providerInfo(provider);
|
|
2485
|
+
return SUPPORTED_MODELS.map((id) => ({
|
|
2486
|
+
provider,
|
|
2487
|
+
id,
|
|
2488
|
+
name: modelName(id),
|
|
2489
|
+
inputModalities: ["text", "image"]
|
|
2490
|
+
}));
|
|
2491
|
+
}
|
|
2492
|
+
async resolveModel(provider, model, signal) {
|
|
2493
|
+
signal?.throwIfAborted();
|
|
2494
|
+
this.providerInfo(provider);
|
|
2495
|
+
if (!isSupportedModel(model)) throw new Error(`Nanocodex does not support model ${JSON.stringify(model)}`);
|
|
2496
|
+
const configured = defaultReasoning(this.ctx, provider);
|
|
2497
|
+
const efforts = REASONING_EFFORTS.map(([id, name]) => ({
|
|
2498
|
+
id: ReasoningEffortId(id),
|
|
2499
|
+
name
|
|
2500
|
+
}));
|
|
2501
|
+
return {
|
|
2502
|
+
provider,
|
|
2503
|
+
id: model,
|
|
2504
|
+
name: modelName(model),
|
|
2505
|
+
inputModalities: ["text", "image"],
|
|
2506
|
+
context: { contextWindow: MODEL_CONTEXT_WINDOW },
|
|
2507
|
+
reasoning: {
|
|
2508
|
+
efforts,
|
|
2509
|
+
...configured === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured) }
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* The retained session-title consumer uses this one-shot path. Ordinary
|
|
2515
|
+
* conversation turns remain owned by NanocodexAgent/NanocodexEngine and do
|
|
2516
|
+
* not recurse through DSH's LLM runtime.
|
|
2517
|
+
*/
|
|
2518
|
+
stream(options) {
|
|
2519
|
+
return this.streamOneShot(options);
|
|
2520
|
+
}
|
|
2521
|
+
async *streamOneShot(options) {
|
|
2522
|
+
const signal = options.signal;
|
|
2523
|
+
signal?.throwIfAborted();
|
|
2524
|
+
if (!isSupportedModel(options.model)) throw new Error(`Nanocodex does not support model ${JSON.stringify(options.model)}`);
|
|
2525
|
+
const route = await resolveNanocodexRoute(this.ctx, {
|
|
2526
|
+
provider: options.provider,
|
|
2527
|
+
model: options.model,
|
|
2528
|
+
...options.reasoningEffort === void 0 ? {} : { reasoningEffort: options.reasoningEffort }
|
|
2529
|
+
});
|
|
2530
|
+
const last = options.messages.at(-1);
|
|
2531
|
+
const historySeed = await buildHistorySeed(last?.role === "user" ? options.messages.slice(0, -1) : options.messages, this.ctx, signal);
|
|
2532
|
+
const input = last?.role === "user" ? await buildPromptInput([last], this.ctx, signal) : "Continue from the current conversation.";
|
|
2533
|
+
const runtimeSessionId = randomUUID();
|
|
2534
|
+
const nodeAgent = await Agent.create({
|
|
2535
|
+
transport: Transport.openAi({
|
|
2536
|
+
apiKey: route.apiKey,
|
|
2537
|
+
...route.apiBaseUrl ? { apiBaseUrl: route.apiBaseUrl } : {},
|
|
2538
|
+
...route.websocketUrl ? { websocketUrl: route.websocketUrl } : {},
|
|
2539
|
+
websocketWarmup: false
|
|
2540
|
+
}),
|
|
2541
|
+
model: options.model,
|
|
2542
|
+
...route.thinking ? { thinking: route.thinking } : {},
|
|
2543
|
+
...options.reasoningEffort === "pro" ? { reasoningMode: "pro" } : {},
|
|
2544
|
+
sessionId: runtimeSessionId,
|
|
2545
|
+
subagents: false,
|
|
2546
|
+
historySeed,
|
|
2547
|
+
...options.system ? { additionalInstructions: options.system } : {}
|
|
2548
|
+
});
|
|
2549
|
+
const removeTransportFallback = observeTransportFallback(this.ctx, options.sessionId === void 0 ? nodeAgent.sessionId : String(options.sessionId), nodeAgent);
|
|
2550
|
+
const turn = nodeAgent.turn.prompt({ input });
|
|
2551
|
+
const abort = () => void turn.cancel().catch(() => void 0);
|
|
2552
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2553
|
+
try {
|
|
2554
|
+
await turn.accepted();
|
|
2555
|
+
const result = await turn.result();
|
|
2556
|
+
try {
|
|
2557
|
+
signal?.throwIfAborted();
|
|
2558
|
+
if (result.finalMessage) {
|
|
2559
|
+
yield {
|
|
2560
|
+
type: "block-start",
|
|
2561
|
+
index: 0,
|
|
2562
|
+
blockType: "text"
|
|
2563
|
+
};
|
|
2564
|
+
yield {
|
|
2565
|
+
type: "text-delta",
|
|
2566
|
+
index: 0,
|
|
2567
|
+
text: result.finalMessage
|
|
2568
|
+
};
|
|
2569
|
+
yield {
|
|
2570
|
+
type: "block-end",
|
|
2571
|
+
index: 0,
|
|
2572
|
+
block: {
|
|
2573
|
+
type: "text",
|
|
2574
|
+
text: result.finalMessage
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
yield {
|
|
2579
|
+
type: "usage",
|
|
2580
|
+
usage: usage(await result.usage())
|
|
2581
|
+
};
|
|
2582
|
+
yield {
|
|
2583
|
+
type: "finish",
|
|
2584
|
+
reason: { kind: "stop" }
|
|
2585
|
+
};
|
|
2586
|
+
} finally {
|
|
2587
|
+
result.dispose();
|
|
2588
|
+
}
|
|
2589
|
+
} finally {
|
|
2590
|
+
signal?.removeEventListener("abort", abort);
|
|
2591
|
+
removeTransportFallback();
|
|
2592
|
+
turn.dispose();
|
|
2593
|
+
nodeAgent.dispose();
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
//#endregion
|
|
2598
|
+
//#region src/apply-patch/codex-whitespace.ts
|
|
2599
|
+
function isRustWhitespace(character) {
|
|
2600
|
+
const codePoint = character?.codePointAt(0);
|
|
2601
|
+
return codePoint !== void 0 && (codePoint >= 9 && codePoint <= 13 || codePoint === 32 || codePoint === 133 || codePoint === 160 || codePoint === 5760 || codePoint >= 8192 && codePoint <= 8202 || codePoint === 8232 || codePoint === 8233 || codePoint === 8239 || codePoint === 8287 || codePoint === 12288);
|
|
2602
|
+
}
|
|
2603
|
+
function rustTrimStart(value) {
|
|
2604
|
+
let start = 0;
|
|
2605
|
+
while (start < value.length && isRustWhitespace(value[start])) start += 1;
|
|
2606
|
+
return value.slice(start);
|
|
2607
|
+
}
|
|
2608
|
+
function rustTrimEnd(value) {
|
|
2609
|
+
let end = value.length;
|
|
2610
|
+
while (end > 0 && isRustWhitespace(value[end - 1])) end -= 1;
|
|
2611
|
+
return value.slice(0, end);
|
|
2612
|
+
}
|
|
2613
|
+
function rustTrim(value) {
|
|
2614
|
+
return rustTrimEnd(rustTrimStart(value));
|
|
2615
|
+
}
|
|
2616
|
+
//#endregion
|
|
2617
|
+
//#region src/apply-patch/errors.ts
|
|
2618
|
+
var PatchError = class extends Error {
|
|
2619
|
+
code;
|
|
2620
|
+
line;
|
|
2621
|
+
constructor(message, code, options = {}) {
|
|
2622
|
+
super(message, options);
|
|
2623
|
+
this.name = "PatchError";
|
|
2624
|
+
this.code = code;
|
|
2625
|
+
if (options.line !== void 0) this.line = options.line;
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
function patchErrorMessage(error) {
|
|
2629
|
+
return error instanceof Error ? error.message : String(error);
|
|
2630
|
+
}
|
|
2631
|
+
//#endregion
|
|
2632
|
+
//#region src/apply-patch/source-file.ts
|
|
2633
|
+
/** Preserve the first observed line ending, as the Codex patcher does. */
|
|
2634
|
+
var SourceFile = class SourceFile {
|
|
2635
|
+
lines;
|
|
2636
|
+
preferredEnding;
|
|
2637
|
+
constructor(lines, preferredEnding) {
|
|
2638
|
+
this.lines = lines;
|
|
2639
|
+
this.preferredEnding = preferredEnding;
|
|
2640
|
+
}
|
|
2641
|
+
static parse(content) {
|
|
2642
|
+
const lines = [];
|
|
2643
|
+
let preferredEnding;
|
|
2644
|
+
let lineStart = 0;
|
|
2645
|
+
let cursor = 0;
|
|
2646
|
+
while (cursor < content.length) {
|
|
2647
|
+
const current = content[cursor];
|
|
2648
|
+
let ending;
|
|
2649
|
+
let endingLength = 0;
|
|
2650
|
+
if (current === "\r" && content[cursor + 1] === "\n") {
|
|
2651
|
+
ending = "\r\n";
|
|
2652
|
+
endingLength = 2;
|
|
2653
|
+
} else if (current === "\r" || current === "\n") {
|
|
2654
|
+
ending = current;
|
|
2655
|
+
endingLength = 1;
|
|
2656
|
+
}
|
|
2657
|
+
if (ending === void 0) {
|
|
2658
|
+
cursor += 1;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
preferredEnding ??= ending;
|
|
2662
|
+
lines.push({
|
|
2663
|
+
text: content.slice(lineStart, cursor),
|
|
2664
|
+
ending
|
|
2665
|
+
});
|
|
2666
|
+
cursor += endingLength;
|
|
2667
|
+
lineStart = cursor;
|
|
2668
|
+
}
|
|
2669
|
+
if (lineStart < content.length) lines.push({ text: content.slice(lineStart) });
|
|
2670
|
+
return new SourceFile(lines, preferredEnding ?? "\n");
|
|
2671
|
+
}
|
|
2672
|
+
lineTexts() {
|
|
2673
|
+
return this.lines.map((line) => line.text);
|
|
2674
|
+
}
|
|
2675
|
+
applyReplacements(replacements) {
|
|
2676
|
+
const result = [];
|
|
2677
|
+
let sourceIndex = 0;
|
|
2678
|
+
for (const [start, oldLength, newLines] of replacements) {
|
|
2679
|
+
if (start < sourceIndex) throw new Error("overlapping replacements");
|
|
2680
|
+
result.push(...this.lines.slice(sourceIndex, start));
|
|
2681
|
+
result.push(...newLines.map((text) => ({
|
|
2682
|
+
text,
|
|
2683
|
+
ending: this.preferredEnding
|
|
2684
|
+
})));
|
|
2685
|
+
sourceIndex = start + oldLength;
|
|
2686
|
+
}
|
|
2687
|
+
result.push(...this.lines.slice(sourceIndex));
|
|
2688
|
+
for (const line of result) line.ending ??= this.preferredEnding;
|
|
2689
|
+
this.lines = result;
|
|
2690
|
+
}
|
|
2691
|
+
contents() {
|
|
2692
|
+
return this.lines.map((line) => `${line.text}${line.ending ?? ""}`).join("");
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
//#endregion
|
|
2696
|
+
//#region src/apply-patch/matcher.ts
|
|
2697
|
+
const UNICODE_ASCII = /* @__PURE__ */ new Map([
|
|
2698
|
+
["‐", "-"],
|
|
2699
|
+
["‑", "-"],
|
|
2700
|
+
["‒", "-"],
|
|
2701
|
+
["–", "-"],
|
|
2702
|
+
["—", "-"],
|
|
2703
|
+
["―", "-"],
|
|
2704
|
+
["−", "-"],
|
|
2705
|
+
["‘", "'"],
|
|
2706
|
+
["’", "'"],
|
|
2707
|
+
["‚", "'"],
|
|
2708
|
+
["‛", "'"],
|
|
2709
|
+
["“", "\""],
|
|
2710
|
+
["”", "\""],
|
|
2711
|
+
["„", "\""],
|
|
2712
|
+
["‟", "\""],
|
|
2713
|
+
["\xA0", " "],
|
|
2714
|
+
[" ", " "],
|
|
2715
|
+
[" ", " "],
|
|
2716
|
+
[" ", " "],
|
|
2717
|
+
[" ", " "],
|
|
2718
|
+
[" ", " "],
|
|
2719
|
+
[" ", " "],
|
|
2720
|
+
[" ", " "],
|
|
2721
|
+
[" ", " "],
|
|
2722
|
+
[" ", " "],
|
|
2723
|
+
[" ", " "],
|
|
2724
|
+
[" ", " "],
|
|
2725
|
+
[" ", " "]
|
|
2726
|
+
]);
|
|
2727
|
+
function normalizeUnicode(line) {
|
|
2728
|
+
return Array.from(rustTrim(line)).map((character) => UNICODE_ASCII.get(character) ?? character).join("");
|
|
2729
|
+
}
|
|
2730
|
+
function findWith(lines, pattern, start, compare) {
|
|
2731
|
+
const last = lines.length - pattern.length;
|
|
2732
|
+
for (let index = start; index <= last; index += 1) if (pattern.every((line, offset) => compare(lines[index + offset], line))) return index;
|
|
2733
|
+
}
|
|
2734
|
+
/** Codex's exact, trimmed, and Unicode-normalized context matching order. */
|
|
2735
|
+
function seekSequence(lines, pattern, start, atEnd) {
|
|
2736
|
+
if (pattern.length === 0) return start;
|
|
2737
|
+
if (pattern.length > lines.length) return void 0;
|
|
2738
|
+
const searchStart = atEnd ? Math.max(start, lines.length - pattern.length) : start;
|
|
2739
|
+
return findWith(lines, pattern, searchStart, (left, right) => left === right) ?? findWith(lines, pattern, searchStart, (left, right) => rustTrimEnd(left) === rustTrimEnd(right)) ?? findWith(lines, pattern, searchStart, (left, right) => rustTrim(left) === rustTrim(right)) ?? findWith(lines, pattern, searchStart, (left, right) => normalizeUnicode(left) === normalizeUnicode(right));
|
|
2740
|
+
}
|
|
2741
|
+
function computeReplacements(lines, path, chunks) {
|
|
2742
|
+
const replacements = [];
|
|
2743
|
+
let lineIndex = 0;
|
|
2744
|
+
for (const chunk of chunks) {
|
|
2745
|
+
if (chunk.changeContext !== void 0) {
|
|
2746
|
+
const contextIndex = seekSequence(lines, [chunk.changeContext], lineIndex, false);
|
|
2747
|
+
if (contextIndex === void 0) throw new PatchError(`apply_patch: Failed to find context '${chunk.changeContext}' in ${path}`, "PATCH_CONTEXT_NOT_FOUND", { line: chunk.line });
|
|
2748
|
+
lineIndex = contextIndex + 1;
|
|
2749
|
+
}
|
|
2750
|
+
if (chunk.oldLines.length === 0) {
|
|
2751
|
+
replacements.push([
|
|
2752
|
+
lines.length,
|
|
2753
|
+
0,
|
|
2754
|
+
chunk.newLines
|
|
2755
|
+
]);
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
let pattern = chunk.oldLines;
|
|
2759
|
+
let newLines = chunk.newLines;
|
|
2760
|
+
let matchIndex = seekSequence(lines, pattern, lineIndex, chunk.isEndOfFile);
|
|
2761
|
+
if (matchIndex === void 0 && pattern.at(-1) === "") {
|
|
2762
|
+
pattern = pattern.slice(0, -1);
|
|
2763
|
+
if (newLines.at(-1) === "") newLines = newLines.slice(0, -1);
|
|
2764
|
+
matchIndex = seekSequence(lines, pattern, lineIndex, chunk.isEndOfFile);
|
|
2765
|
+
}
|
|
2766
|
+
if (matchIndex === void 0) throw new PatchError(`apply_patch: Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`, "PATCH_CONTEXT_NOT_FOUND", { line: chunk.line });
|
|
2767
|
+
let oldStart = 0;
|
|
2768
|
+
let newStart = 0;
|
|
2769
|
+
for (const [oldContext, newContext] of chunk.contextLineIndices) {
|
|
2770
|
+
if (oldContext >= pattern.length || newContext >= newLines.length) break;
|
|
2771
|
+
if (oldStart !== oldContext || newStart !== newContext) replacements.push([
|
|
2772
|
+
matchIndex + oldStart,
|
|
2773
|
+
oldContext - oldStart,
|
|
2774
|
+
newLines.slice(newStart, newContext)
|
|
2775
|
+
]);
|
|
2776
|
+
oldStart = oldContext + 1;
|
|
2777
|
+
newStart = newContext + 1;
|
|
2778
|
+
}
|
|
2779
|
+
if (oldStart !== pattern.length || newStart !== newLines.length) replacements.push([
|
|
2780
|
+
matchIndex + oldStart,
|
|
2781
|
+
pattern.length - oldStart,
|
|
2782
|
+
newLines.slice(newStart)
|
|
2783
|
+
]);
|
|
2784
|
+
lineIndex = matchIndex + pattern.length;
|
|
2785
|
+
}
|
|
2786
|
+
replacements.sort((left, right) => left[0] - right[0]);
|
|
2787
|
+
return replacements;
|
|
2788
|
+
}
|
|
2789
|
+
/** Apply update chunks entirely in memory before a filesystem write. */
|
|
2790
|
+
function applyChunks(content, path, chunks) {
|
|
2791
|
+
const source = SourceFile.parse(content);
|
|
2792
|
+
source.applyReplacements(computeReplacements(source.lineTexts(), path, chunks));
|
|
2793
|
+
return source.contents();
|
|
2794
|
+
}
|
|
2795
|
+
//#endregion
|
|
2796
|
+
//#region src/apply-patch/parser.ts
|
|
2797
|
+
const BEGIN_PATCH = "*** Begin Patch";
|
|
2798
|
+
const END_PATCH = "*** End Patch";
|
|
2799
|
+
const ADD_FILE = "*** Add File: ";
|
|
2800
|
+
const UPDATE_FILE = "*** Update File: ";
|
|
2801
|
+
const DELETE_FILE = "*** Delete File: ";
|
|
2802
|
+
const MOVE_TO = "*** Move to: ";
|
|
2803
|
+
const END_OF_FILE = "*** End of File";
|
|
2804
|
+
const CHANGE_CONTEXT = "@@ ";
|
|
2805
|
+
const EMPTY_CHANGE_CONTEXT = "@@";
|
|
2806
|
+
function invalidPatch(message) {
|
|
2807
|
+
throw new PatchError(`apply_patch: invalid patch: ${message}`, "PATCH_INVALID");
|
|
2808
|
+
}
|
|
2809
|
+
function invalidHunk(message, line) {
|
|
2810
|
+
throw new PatchError(`apply_patch: invalid hunk at line ${line}: ${message}`, "PATCH_INVALID", { line });
|
|
2811
|
+
}
|
|
2812
|
+
function unsupported(message, line) {
|
|
2813
|
+
throw new PatchError(`apply_patch: unsupported operation: ${message}`, "PATCH_UNSUPPORTED", line === void 0 ? {} : { line });
|
|
2814
|
+
}
|
|
2815
|
+
function splitPatchLines(input) {
|
|
2816
|
+
const trimmed = rustTrim(input);
|
|
2817
|
+
const original = trimmed.length === 0 ? [] : trimmed.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
2818
|
+
const first = original[0] === void 0 ? void 0 : rustTrim(original[0]);
|
|
2819
|
+
const finalLine = original.at(-1);
|
|
2820
|
+
const last = finalLine === void 0 ? void 0 : rustTrim(finalLine);
|
|
2821
|
+
if (first === "*** Begin Patch" && last === "*** End Patch") return original;
|
|
2822
|
+
if (original.length >= 4 && (original[0] === "<<EOF" || original[0] === "<<'EOF'" || original[0] === "<<\"EOF\"") && original.at(-1)?.endsWith("EOF")) {
|
|
2823
|
+
const inner = original.slice(1, -1);
|
|
2824
|
+
if (rustTrim(inner[0]) === "*** Begin Patch" && rustTrim(inner.at(-1)) === "*** End Patch") return inner;
|
|
2825
|
+
if (rustTrim(inner[0]) !== "*** Begin Patch") invalidPatch(`The first line of the patch must be '${BEGIN_PATCH}'`);
|
|
2826
|
+
invalidPatch(`The last line of the patch must be '${END_PATCH}'`);
|
|
2827
|
+
}
|
|
2828
|
+
if (first !== void 0 && first !== "*** Begin Patch") invalidPatch(`The first line of the patch must be '${BEGIN_PATCH}'`);
|
|
2829
|
+
invalidPatch(`The last line of the patch must be '${END_PATCH}'`);
|
|
2830
|
+
}
|
|
2831
|
+
function newChunk(line, changeContext) {
|
|
2832
|
+
return {
|
|
2833
|
+
...changeContext === void 0 ? {} : { changeContext },
|
|
2834
|
+
oldLines: [],
|
|
2835
|
+
newLines: [],
|
|
2836
|
+
contextLineIndices: [],
|
|
2837
|
+
isEndOfFile: false,
|
|
2838
|
+
line
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
function ensureUpdateHunkIsNotEmpty(mode, nextLine, lineNumber) {
|
|
2842
|
+
if (mode.kind !== "update") return;
|
|
2843
|
+
const chunks = mode.operation.chunks;
|
|
2844
|
+
if (chunks.length === 0) invalidHunk(`Update file hunk for path '${mode.operation.path}' is empty`, mode.hunkLine);
|
|
2845
|
+
const last = chunks.at(-1);
|
|
2846
|
+
if (last.oldLines.length === 0 && last.newLines.length === 0) {
|
|
2847
|
+
if (nextLine === "*** End Patch") invalidHunk("Update hunk does not contain any lines", lineNumber);
|
|
2848
|
+
invalidHunk(`Unexpected line found in update hunk: '${nextLine}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, lineNumber);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
/** Parse the supported Add File and Update File Codex patch subset. */
|
|
2852
|
+
function parsePatch(input) {
|
|
2853
|
+
if (typeof input !== "string") invalidPatch("input must be a string");
|
|
2854
|
+
const lines = splitPatchLines(input);
|
|
2855
|
+
const operations = [];
|
|
2856
|
+
let mode = { kind: "started" };
|
|
2857
|
+
const startOperation = (trimmed, lineNumber) => {
|
|
2858
|
+
if (trimmed.startsWith(DELETE_FILE)) unsupported("Delete File is not supported; remove file content explicitly", lineNumber);
|
|
2859
|
+
if (trimmed.startsWith(MOVE_TO)) unsupported("Move to is not supported; use an explicit Add File/Update File patch", lineNumber);
|
|
2860
|
+
if (trimmed.startsWith(ADD_FILE)) {
|
|
2861
|
+
ensureUpdateHunkIsNotEmpty(mode, trimmed, lineNumber);
|
|
2862
|
+
const path = trimmed.slice(14);
|
|
2863
|
+
if (path.length === 0) invalidPatch("Add File path cannot be empty");
|
|
2864
|
+
const operation = {
|
|
2865
|
+
kind: "add",
|
|
2866
|
+
path,
|
|
2867
|
+
content: "",
|
|
2868
|
+
line: lineNumber
|
|
2869
|
+
};
|
|
2870
|
+
operations.push(operation);
|
|
2871
|
+
mode = {
|
|
2872
|
+
kind: "add",
|
|
2873
|
+
operation
|
|
2874
|
+
};
|
|
2875
|
+
return true;
|
|
2876
|
+
}
|
|
2877
|
+
if (trimmed.startsWith(UPDATE_FILE)) {
|
|
2878
|
+
ensureUpdateHunkIsNotEmpty(mode, trimmed, lineNumber);
|
|
2879
|
+
const path = trimmed.slice(17);
|
|
2880
|
+
if (path.length === 0) invalidPatch("Update File path cannot be empty");
|
|
2881
|
+
const operation = {
|
|
2882
|
+
kind: "update",
|
|
2883
|
+
path,
|
|
2884
|
+
chunks: [],
|
|
2885
|
+
line: lineNumber
|
|
2886
|
+
};
|
|
2887
|
+
operations.push(operation);
|
|
2888
|
+
mode = {
|
|
2889
|
+
kind: "update",
|
|
2890
|
+
operation,
|
|
2891
|
+
hunkLine: lineNumber
|
|
2892
|
+
};
|
|
2893
|
+
return true;
|
|
2894
|
+
}
|
|
2895
|
+
return false;
|
|
2896
|
+
};
|
|
2897
|
+
for (let index = 1; index < lines.length - 1; index += 1) {
|
|
2898
|
+
const line = lines[index];
|
|
2899
|
+
const lineNumber = index + 1;
|
|
2900
|
+
const activeMode = mode;
|
|
2901
|
+
if (activeMode.kind !== "update") {
|
|
2902
|
+
const trimmed = rustTrim(line);
|
|
2903
|
+
if (startOperation(trimmed, lineNumber)) continue;
|
|
2904
|
+
if (activeMode.kind === "add" && line.startsWith("+")) {
|
|
2905
|
+
activeMode.operation.content += `${line.slice(1)}\n`;
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
invalidHunk(`'${trimmed}' is not a valid hunk header. Valid hunk headers: '${ADD_FILE}{path}', '${UPDATE_FILE}{path}'`, lineNumber);
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
const updateMode = activeMode;
|
|
2912
|
+
const updateLine = rustTrimEnd(line);
|
|
2913
|
+
if (startOperation(updateLine, lineNumber)) continue;
|
|
2914
|
+
const chunks = updateMode.operation.chunks;
|
|
2915
|
+
const last = chunks.at(-1);
|
|
2916
|
+
if (last?.isEndOfFile) {
|
|
2917
|
+
if (updateLine.length === 0) continue;
|
|
2918
|
+
if (updateLine !== EMPTY_CHANGE_CONTEXT && !updateLine.startsWith(CHANGE_CONTEXT)) invalidHunk(`Expected update hunk to start with a @@ context marker, got: '${line}'`, lineNumber);
|
|
2919
|
+
}
|
|
2920
|
+
if (updateLine === EMPTY_CHANGE_CONTEXT) {
|
|
2921
|
+
if (last !== void 0 && last.oldLines.length === 0 && last.newLines.length === 0) invalidHunk(`Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, lineNumber);
|
|
2922
|
+
chunks.push(newChunk(lineNumber));
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
if (updateLine.startsWith(CHANGE_CONTEXT)) {
|
|
2926
|
+
if (last !== void 0 && last.oldLines.length === 0 && last.newLines.length === 0) invalidHunk(`Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, lineNumber);
|
|
2927
|
+
chunks.push(newChunk(lineNumber, updateLine.slice(3)));
|
|
2928
|
+
continue;
|
|
2929
|
+
}
|
|
2930
|
+
if (updateLine === END_OF_FILE) {
|
|
2931
|
+
if (last !== void 0 && last.oldLines.length === 0 && last.newLines.length === 0) invalidHunk("Update hunk does not contain any lines", lineNumber);
|
|
2932
|
+
if (last === void 0) invalidHunk("End of File requires an update hunk", lineNumber);
|
|
2933
|
+
last.isEndOfFile = true;
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
if (line.length === 0 || line.startsWith(" ")) {
|
|
2937
|
+
const chunk = chunks.at(-1) ?? newChunk(lineNumber);
|
|
2938
|
+
if (chunks.length === 0) chunks.push(chunk);
|
|
2939
|
+
chunk.contextLineIndices.push([chunk.oldLines.length, chunk.newLines.length]);
|
|
2940
|
+
const context = line.length === 0 ? "" : line.slice(1);
|
|
2941
|
+
chunk.oldLines.push(context);
|
|
2942
|
+
chunk.newLines.push(context);
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
if (line.startsWith("+")) {
|
|
2946
|
+
const chunk = chunks.at(-1) ?? newChunk(lineNumber);
|
|
2947
|
+
if (chunks.length === 0) chunks.push(chunk);
|
|
2948
|
+
chunk.newLines.push(line.slice(1));
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
if (line.startsWith("-")) {
|
|
2952
|
+
const chunk = chunks.at(-1) ?? newChunk(lineNumber);
|
|
2953
|
+
if (chunks.length === 0) chunks.push(chunk);
|
|
2954
|
+
chunk.oldLines.push(line.slice(1));
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
if (last !== void 0 && (last.oldLines.length > 0 || last.newLines.length > 0)) invalidHunk(`Expected update hunk to start with a @@ context marker, got: '${line}'`, lineNumber);
|
|
2958
|
+
invalidHunk(`Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, lineNumber);
|
|
2959
|
+
}
|
|
2960
|
+
ensureUpdateHunkIsNotEmpty(mode, END_PATCH, lines.length);
|
|
2961
|
+
if (operations.length === 0) invalidPatch("patch contains no file operations");
|
|
2962
|
+
return {
|
|
2963
|
+
operations,
|
|
2964
|
+
normalizedPatch: lines.join("\n")
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
//#endregion
|
|
2968
|
+
//#region src/apply-patch/tool.ts
|
|
2969
|
+
const DEFAULT_APPLY_PATCH_LIMITS = {
|
|
2970
|
+
maxPatchChars: DEFAULT_MAX_PATCH_CHARS,
|
|
2971
|
+
maxPatchFiles: 64,
|
|
2972
|
+
maxPatchFileBytes: DEFAULT_MAX_PATCH_FILE_BYTES
|
|
2973
|
+
};
|
|
2974
|
+
function workspaceCwd(exec) {
|
|
2975
|
+
const cwd = exec.agent?.session?.header?.cwd;
|
|
2976
|
+
if (typeof cwd !== "string" || cwd.length === 0) throw new PatchError("apply_patch requires an active Agent workspace", "PATCH_PATH_INVALID");
|
|
2977
|
+
return cwd;
|
|
2978
|
+
}
|
|
2979
|
+
function byteLength(value) {
|
|
2980
|
+
return new TextEncoder().encode(value).byteLength;
|
|
2981
|
+
}
|
|
2982
|
+
function parseArguments(value, limits) {
|
|
2983
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new PatchError("apply_patch expects an object containing one non-empty patch string", "PATCH_INVALID");
|
|
2984
|
+
const record = value;
|
|
2985
|
+
if (Object.keys(record).some((key) => key !== "patch") || typeof record.patch !== "string" || record.patch.length === 0) throw new PatchError("apply_patch expects an object containing one non-empty patch string", "PATCH_INVALID");
|
|
2986
|
+
if (record.patch.length > limits.maxPatchChars) throw new PatchError(`apply_patch input exceeds the ${limits.maxPatchChars}-character limit`, "PATCH_TOO_LARGE");
|
|
2987
|
+
return record.patch;
|
|
2988
|
+
}
|
|
2989
|
+
function operationAction(operation) {
|
|
2990
|
+
return operation.kind;
|
|
2991
|
+
}
|
|
2992
|
+
function errorCode(error) {
|
|
2993
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
|
|
2994
|
+
return "PATCH_IO";
|
|
2995
|
+
}
|
|
2996
|
+
function isCancellation(error, signal) {
|
|
2997
|
+
return signal.aborted || errorCode(error) === "FS_ABORTED" || error instanceof Error && error.name === "AbortError";
|
|
2998
|
+
}
|
|
2999
|
+
function observeVersion(services, target, version, exec) {
|
|
3000
|
+
services.ctx.emit(scopeTarget(services.fs, exec.agent), "fs/observed", target, {
|
|
3001
|
+
kind: "present",
|
|
3002
|
+
version
|
|
3003
|
+
}, exec);
|
|
3004
|
+
}
|
|
3005
|
+
async function writeIntent(services, target, fallback, exec) {
|
|
3006
|
+
return await services.ctx.waterfall(scopeTarget(services.fs, exec.agent), "fs/write-intent", target, exec, () => Promise.resolve(fallback)) ?? fallback;
|
|
3007
|
+
}
|
|
3008
|
+
async function editIntent(services, target, fallback, exec) {
|
|
3009
|
+
return await services.ctx.waterfall(scopeTarget(services.fs, exec.agent), "fs/edit-intent", target, exec, () => Promise.resolve(fallback)) ?? fallback;
|
|
3010
|
+
}
|
|
3011
|
+
function checkFileSize(info, path, limits) {
|
|
3012
|
+
if (info.size !== void 0 && info.size > limits.maxPatchFileBytes) throw new PatchError(`apply_patch target '${path}' exceeds the ${limits.maxPatchFileBytes}-byte limit`, "PATCH_TOO_LARGE");
|
|
3013
|
+
}
|
|
3014
|
+
async function preflight(patch, services, exec, limits) {
|
|
3015
|
+
if (patch.operations.length > limits.maxPatchFiles) throw new PatchError(`apply_patch contains more than ${limits.maxPatchFiles} file operations`, "PATCH_TOO_LARGE");
|
|
3016
|
+
const cwd = workspaceCwd(exec);
|
|
3017
|
+
const workspace = await services.fs.resolve(cwd, {
|
|
3018
|
+
cwd,
|
|
3019
|
+
signal: exec.signal
|
|
3020
|
+
});
|
|
3021
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3022
|
+
const staged = [];
|
|
3023
|
+
for (const operation of patch.operations) {
|
|
3024
|
+
exec.signal.throwIfAborted();
|
|
3025
|
+
if (operation.path.trim().length === 0) throw new PatchError(`apply_patch ${operation.kind === "add" ? "Add" : "Update"} File path cannot be blank`, "PATCH_PATH_INVALID", { line: operation.line });
|
|
3026
|
+
const target = await services.fs.resolve(operation.path, {
|
|
3027
|
+
cwd,
|
|
3028
|
+
signal: exec.signal
|
|
3029
|
+
});
|
|
3030
|
+
exec.signal.throwIfAborted();
|
|
3031
|
+
if (!services.fs.contains(workspace, target)) throw new PatchError(`apply_patch target '${operation.path}' must stay inside the active workspace`, "PATCH_PATH_INVALID", { line: operation.line });
|
|
3032
|
+
const previousPath = targets.get(target.targetKey);
|
|
3033
|
+
if (previousPath !== void 0) throw new PatchError(`apply_patch contains duplicate target '${operation.path}' (same target as '${previousPath}')`, "PATCH_INVALID", { line: operation.line });
|
|
3034
|
+
targets.set(target.targetKey, operation.path);
|
|
3035
|
+
const info = await services.fs.stat(target, exec.signal);
|
|
3036
|
+
exec.signal.throwIfAborted();
|
|
3037
|
+
if (operation.kind === "add") {
|
|
3038
|
+
if (info !== void 0) throw new PatchError(`apply_patch cannot add '${operation.path}': the target already exists`, "PATCH_INVALID", { line: operation.line });
|
|
3039
|
+
if (byteLength(operation.content) > limits.maxPatchFileBytes) throw new PatchError(`apply_patch added file '${operation.path}' exceeds the ${limits.maxPatchFileBytes}-byte limit`, "PATCH_TOO_LARGE", { line: operation.line });
|
|
3040
|
+
const expected = await writeIntent(services, target, { kind: "createIfAbsent" }, exec);
|
|
3041
|
+
if (expected.kind !== "createIfAbsent") throw new FsError(`cannot add '${operation.path}': the target changed after its absence was observed`, "FS_STALE_VERSION");
|
|
3042
|
+
staged.push({
|
|
3043
|
+
operation,
|
|
3044
|
+
target,
|
|
3045
|
+
before: null,
|
|
3046
|
+
after: operation.content,
|
|
3047
|
+
expected
|
|
3048
|
+
});
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
if (info === void 0) throw new PatchError(`apply_patch cannot update '${operation.path}': the target does not exist`, "PATCH_INVALID", { line: operation.line });
|
|
3052
|
+
if (info.type !== "file") throw new PatchError(`apply_patch cannot update '${operation.path}': the target is not a regular text file`, "PATCH_INVALID", { line: operation.line });
|
|
3053
|
+
const edit = await editIntent(services, target, { version: info.version }, exec);
|
|
3054
|
+
if (edit.version !== info.version) throw new FsError(`cannot update '${operation.path}': the target changed after preflight stat`, "FS_STALE_VERSION");
|
|
3055
|
+
const expected = await writeIntent(services, target, {
|
|
3056
|
+
kind: "replaceIfVersion",
|
|
3057
|
+
version: edit.version
|
|
3058
|
+
}, exec);
|
|
3059
|
+
if (expected.kind !== "replaceIfVersion" || expected.version !== info.version) throw new FsError(`cannot update '${operation.path}': the target is not observed at its current version`, "FS_STALE_VERSION");
|
|
3060
|
+
checkFileSize(info, operation.path, limits);
|
|
3061
|
+
const before = await services.fs.readText(target, exec.signal);
|
|
3062
|
+
exec.signal.throwIfAborted();
|
|
3063
|
+
if (byteLength(before) > limits.maxPatchFileBytes) throw new PatchError(`apply_patch target '${operation.path}' exceeds the ${limits.maxPatchFileBytes}-byte limit`, "PATCH_TOO_LARGE", { line: operation.line });
|
|
3064
|
+
const after = applyChunks(before, operation.path, operation.chunks);
|
|
3065
|
+
if (byteLength(after) > limits.maxPatchFileBytes) throw new PatchError(`apply_patch updated file '${operation.path}' exceeds the ${limits.maxPatchFileBytes}-byte limit`, "PATCH_TOO_LARGE", { line: operation.line });
|
|
3066
|
+
staged.push({
|
|
3067
|
+
operation,
|
|
3068
|
+
target,
|
|
3069
|
+
before,
|
|
3070
|
+
after,
|
|
3071
|
+
expected
|
|
3072
|
+
});
|
|
3073
|
+
exec.signal.throwIfAborted();
|
|
3074
|
+
}
|
|
3075
|
+
return staged;
|
|
3076
|
+
}
|
|
3077
|
+
function operationSummary(operations, status) {
|
|
3078
|
+
const committed = operations.filter((operation) => operation.status === "committed").map((operation) => operation.path);
|
|
3079
|
+
const failed = operations.find((operation) => operation.status === "failed");
|
|
3080
|
+
const unattempted = operations.filter((operation) => operation.status === "unattempted").map((operation) => operation.path);
|
|
3081
|
+
if (status === "applied") return `Applied patch to ${committed.length} file${committed.length === 1 ? "" : "s"}: ${committed.join(", ")}.`;
|
|
3082
|
+
const details = [`${status === "cancelled" ? "Patch publication cancelled" : "Patch publication failed"}.`];
|
|
3083
|
+
if (committed.length > 0) details.push(`Committed: ${committed.join(", ")}.`);
|
|
3084
|
+
if (failed !== void 0) details.push(`Failed: ${failed.path}.`);
|
|
3085
|
+
if (unattempted.length > 0) details.push(`Unattempted: ${unattempted.join(", ")}.`);
|
|
3086
|
+
return details.join(" ");
|
|
3087
|
+
}
|
|
3088
|
+
function unifiedDiff(diff) {
|
|
3089
|
+
const oldLines = diff.oldText === null ? [] : diff.oldText.split("\n");
|
|
3090
|
+
const newLines = diff.newText.split("\n");
|
|
3091
|
+
return [
|
|
3092
|
+
`--- ${diff.oldText === null ? "/dev/null" : `a/${diff.path}`}`,
|
|
3093
|
+
`+++ b/${diff.path}`,
|
|
3094
|
+
...oldLines.map((line) => `-${line}`),
|
|
3095
|
+
...newLines.map((line) => `+${line}`)
|
|
3096
|
+
].join("\n");
|
|
3097
|
+
}
|
|
3098
|
+
function diffFromWrite(staged, outcome) {
|
|
3099
|
+
return {
|
|
3100
|
+
path: staged.operation.path,
|
|
3101
|
+
oldText: outcome.before ?? staged.before,
|
|
3102
|
+
newText: outcome.after
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
function outputFor(status, operations, diffs, failure) {
|
|
3106
|
+
return {
|
|
3107
|
+
status,
|
|
3108
|
+
summary: operationSummary(operations, status),
|
|
3109
|
+
diff: diffs.map(unifiedDiff).join("\n"),
|
|
3110
|
+
operations,
|
|
3111
|
+
diffs,
|
|
3112
|
+
...failure === void 0 ? {} : { error: failure }
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Preflight and publish a supported Codex patch through the DSH filesystem.
|
|
3117
|
+
*
|
|
3118
|
+
* Every operation is resolved, observed, read, matched, and calculated before
|
|
3119
|
+
* the first guarded atomic write. Publication is ordered and intentionally
|
|
3120
|
+
* non-transactional: a later failure leaves committed earlier files in place
|
|
3121
|
+
* and returns their diffs with explicit per-operation progress.
|
|
3122
|
+
*/
|
|
3123
|
+
async function applyPatchToFileSystem(input, services, exec, limits = DEFAULT_APPLY_PATCH_LIMITS) {
|
|
3124
|
+
if (input.length > limits.maxPatchChars) throw new PatchError(`apply_patch input exceeds the ${limits.maxPatchChars}-character limit`, "PATCH_TOO_LARGE");
|
|
3125
|
+
const staged = await preflight(parsePatch(input), services, exec, limits);
|
|
3126
|
+
const operations = staged.map(({ operation }) => ({
|
|
3127
|
+
path: operation.path,
|
|
3128
|
+
action: operationAction(operation),
|
|
3129
|
+
status: "unattempted"
|
|
3130
|
+
}));
|
|
3131
|
+
const diffs = [];
|
|
3132
|
+
for (const [index, candidate] of staged.entries()) {
|
|
3133
|
+
let writeStarted = false;
|
|
3134
|
+
try {
|
|
3135
|
+
exec.signal.throwIfAborted();
|
|
3136
|
+
writeStarted = true;
|
|
3137
|
+
const write = await services.fs.writeText(candidate.target, candidate.after, candidate.expected, exec.signal);
|
|
3138
|
+
const operation = operations[index];
|
|
3139
|
+
if (operation === void 0) throw new Error("patch operation disappeared");
|
|
3140
|
+
operations[index] = {
|
|
3141
|
+
...operation,
|
|
3142
|
+
status: "committed"
|
|
3143
|
+
};
|
|
3144
|
+
diffs.push(diffFromWrite(candidate, write));
|
|
3145
|
+
observeVersion(services, candidate.target, write.version, exec);
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
const code = isCancellation(error, exec.signal) ? "FS_ABORTED" : errorCode(error);
|
|
3148
|
+
const message = isCancellation(error, exec.signal) ? "patch publication was cancelled" : patchErrorMessage(error);
|
|
3149
|
+
const operation = operations[index];
|
|
3150
|
+
if (operation !== void 0 && writeStarted) operations[index] = {
|
|
3151
|
+
...operation,
|
|
3152
|
+
status: "failed",
|
|
3153
|
+
error: {
|
|
3154
|
+
code,
|
|
3155
|
+
message
|
|
3156
|
+
}
|
|
3157
|
+
};
|
|
3158
|
+
return outputFor(isCancellation(error, exec.signal) ? "cancelled" : diffs.length > 0 ? "partial" : "failed", operations, diffs, {
|
|
3159
|
+
code,
|
|
3160
|
+
message
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return outputFor("applied", operations, diffs);
|
|
3165
|
+
}
|
|
3166
|
+
function outcomeText(outcome) {
|
|
3167
|
+
const error = outcome.error;
|
|
3168
|
+
return [
|
|
3169
|
+
outcome.summary,
|
|
3170
|
+
error === void 0 ? "" : `Error (${error.code}): ${error.message}`,
|
|
3171
|
+
outcome.diff.length === 0 ? "" : `\n${outcome.diff}`
|
|
3172
|
+
].filter(Boolean).join("\n");
|
|
3173
|
+
}
|
|
3174
|
+
function renderOutcome(_args, value) {
|
|
3175
|
+
return [{
|
|
3176
|
+
type: "text",
|
|
3177
|
+
text: outcomeText(value)
|
|
3178
|
+
}];
|
|
3179
|
+
}
|
|
3180
|
+
function presentationDiffs(value) {
|
|
3181
|
+
return value.diffs.map((diff) => ({
|
|
3182
|
+
path: diff.path,
|
|
3183
|
+
oldText: diff.oldText,
|
|
3184
|
+
newText: diff.newText
|
|
3185
|
+
}));
|
|
3186
|
+
}
|
|
3187
|
+
function isDiffResultMeta(value) {
|
|
3188
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3189
|
+
const diffs = value.diffs;
|
|
3190
|
+
return Array.isArray(diffs) && diffs.every((diff) => typeof diff === "object" && diff !== null && typeof diff.path === "string" && typeof diff.newText === "string");
|
|
3191
|
+
}
|
|
3192
|
+
function callView(args, limits) {
|
|
3193
|
+
try {
|
|
3194
|
+
const patch = parseArguments(args, limits);
|
|
3195
|
+
const locations = parsePatch(patch).operations.map((operation) => ({ path: operation.path }));
|
|
3196
|
+
return {
|
|
3197
|
+
card: "generic",
|
|
3198
|
+
title: `Apply patch (${locations.length} file${locations.length === 1 ? "" : "s"})`,
|
|
3199
|
+
kind: "edit",
|
|
3200
|
+
rawInput: patch,
|
|
3201
|
+
locations
|
|
3202
|
+
};
|
|
3203
|
+
} catch {
|
|
3204
|
+
return {
|
|
3205
|
+
card: "generic",
|
|
3206
|
+
title: "Apply patch",
|
|
3207
|
+
kind: "edit",
|
|
3208
|
+
rawInput: args
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function resultView(_args, result) {
|
|
3213
|
+
if (!result.isError && isDiffResultMeta(result.meta)) return {
|
|
3214
|
+
card: "diff",
|
|
3215
|
+
diffs: result.meta.diffs
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
const parameters = {
|
|
3219
|
+
type: "object",
|
|
3220
|
+
additionalProperties: false,
|
|
3221
|
+
properties: { patch: {
|
|
3222
|
+
type: "string",
|
|
3223
|
+
description: "Complete raw Codex Add File/Update File patch text."
|
|
3224
|
+
} },
|
|
3225
|
+
required: ["patch"]
|
|
3226
|
+
};
|
|
3227
|
+
const outputSchema = {
|
|
3228
|
+
type: "object",
|
|
3229
|
+
additionalProperties: false,
|
|
3230
|
+
properties: {
|
|
3231
|
+
status: {
|
|
3232
|
+
type: "string",
|
|
3233
|
+
enum: [
|
|
3234
|
+
"applied",
|
|
3235
|
+
"partial",
|
|
3236
|
+
"failed",
|
|
3237
|
+
"cancelled"
|
|
3238
|
+
]
|
|
3239
|
+
},
|
|
3240
|
+
summary: { type: "string" },
|
|
3241
|
+
diff: { type: "string" },
|
|
3242
|
+
operations: {
|
|
3243
|
+
type: "array",
|
|
3244
|
+
items: {
|
|
3245
|
+
type: "object",
|
|
3246
|
+
additionalProperties: false,
|
|
3247
|
+
properties: {
|
|
3248
|
+
path: { type: "string" },
|
|
3249
|
+
action: {
|
|
3250
|
+
type: "string",
|
|
3251
|
+
enum: ["add", "update"]
|
|
3252
|
+
},
|
|
3253
|
+
status: {
|
|
3254
|
+
type: "string",
|
|
3255
|
+
enum: [
|
|
3256
|
+
"committed",
|
|
3257
|
+
"failed",
|
|
3258
|
+
"unattempted"
|
|
3259
|
+
]
|
|
3260
|
+
},
|
|
3261
|
+
error: {
|
|
3262
|
+
type: "object",
|
|
3263
|
+
additionalProperties: false,
|
|
3264
|
+
properties: {
|
|
3265
|
+
code: { type: "string" },
|
|
3266
|
+
message: { type: "string" }
|
|
3267
|
+
},
|
|
3268
|
+
required: ["code", "message"]
|
|
3269
|
+
}
|
|
3270
|
+
},
|
|
3271
|
+
required: [
|
|
3272
|
+
"path",
|
|
3273
|
+
"action",
|
|
3274
|
+
"status"
|
|
3275
|
+
]
|
|
3276
|
+
}
|
|
3277
|
+
},
|
|
3278
|
+
diffs: {
|
|
3279
|
+
type: "array",
|
|
3280
|
+
items: {
|
|
3281
|
+
type: "object",
|
|
3282
|
+
additionalProperties: false,
|
|
3283
|
+
properties: {
|
|
3284
|
+
path: { type: "string" },
|
|
3285
|
+
oldText: { oneOf: [{ type: "string" }, { type: "null" }] },
|
|
3286
|
+
newText: { type: "string" }
|
|
3287
|
+
},
|
|
3288
|
+
required: [
|
|
3289
|
+
"path",
|
|
3290
|
+
"oldText",
|
|
3291
|
+
"newText"
|
|
3292
|
+
]
|
|
3293
|
+
}
|
|
3294
|
+
},
|
|
3295
|
+
error: {
|
|
3296
|
+
type: "object",
|
|
3297
|
+
additionalProperties: false,
|
|
3298
|
+
properties: {
|
|
3299
|
+
code: { type: "string" },
|
|
3300
|
+
message: { type: "string" }
|
|
3301
|
+
},
|
|
3302
|
+
required: ["code", "message"]
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
required: [
|
|
3306
|
+
"status",
|
|
3307
|
+
"summary",
|
|
3308
|
+
"diff",
|
|
3309
|
+
"operations",
|
|
3310
|
+
"diffs"
|
|
3311
|
+
]
|
|
3312
|
+
};
|
|
3313
|
+
/** Build the registry definition for the direct raw patch tool. */
|
|
3314
|
+
function createApplyPatchTool(services, progressByExecution, limits = DEFAULT_APPLY_PATCH_LIMITS) {
|
|
3315
|
+
return {
|
|
3316
|
+
name: APPLY_PATCH_NAME,
|
|
3317
|
+
description: APPLY_PATCH_DESCRIPTION,
|
|
3318
|
+
parameters,
|
|
3319
|
+
output: {
|
|
3320
|
+
schema: outputSchema,
|
|
3321
|
+
render: renderOutcome,
|
|
3322
|
+
presentationMeta: (_args, value) => ({ diffs: presentationDiffs(value).map((diff) => ({
|
|
3323
|
+
path: diff.path,
|
|
3324
|
+
oldText: diff.oldText,
|
|
3325
|
+
newText: diff.newText
|
|
3326
|
+
})) })
|
|
3327
|
+
},
|
|
3328
|
+
async execute(args, exec) {
|
|
3329
|
+
const outcome = await applyPatchToFileSystem(parseArguments(args, limits), services, exec, limits);
|
|
3330
|
+
progressByExecution.set(exec, { outcome });
|
|
3331
|
+
return outcome;
|
|
3332
|
+
},
|
|
3333
|
+
finalizeContent(exec, result) {
|
|
3334
|
+
const progress = progressByExecution.get(exec);
|
|
3335
|
+
progressByExecution.delete(exec);
|
|
3336
|
+
if (progress === void 0 || !result.isError) return void 0;
|
|
3337
|
+
return renderOutcome(exec.arguments, progress.outcome);
|
|
3338
|
+
},
|
|
3339
|
+
presentCall: (args) => callView(args, limits),
|
|
3340
|
+
presentResult: resultView
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
//#endregion
|
|
3344
|
+
//#region src/index.ts
|
|
3345
|
+
const name = PLUGIN_NAME;
|
|
3346
|
+
const inject = [
|
|
3347
|
+
"agents",
|
|
3348
|
+
"sessions",
|
|
3349
|
+
"settings",
|
|
3350
|
+
"credentials",
|
|
3351
|
+
"llm",
|
|
3352
|
+
"attachments",
|
|
3353
|
+
"commands",
|
|
3354
|
+
"systemPrompt",
|
|
3355
|
+
"tools",
|
|
3356
|
+
"fs",
|
|
3357
|
+
"storageDomain"
|
|
3358
|
+
];
|
|
3359
|
+
/** Install the selected Nanocodex AgentFactory and private compaction owner. */
|
|
3360
|
+
async function apply(ctx) {
|
|
3361
|
+
const checkpoints = await ctx.storageDomain.open(nanocodexCheckpointDomain);
|
|
3362
|
+
ctx.effect(() => () => checkpoints.close(), "dsh-nanocodex.checkpoints");
|
|
3363
|
+
const settings = ctx.settings.register(SETTINGS_NAMESPACE, NanocodexSettingsSchema, { applies: "live" });
|
|
3364
|
+
const adapter = new NanocodexLlmAdapter(ctx);
|
|
3365
|
+
const routes = configuredNanocodexProviders(settings.get());
|
|
3366
|
+
const adapterRegistration = ctx.llm.registerAdapter(routes.length > 0 ? routes : ["openai"], adapter);
|
|
3367
|
+
if (routes.length === 0) adapterRegistration.replace([]);
|
|
3368
|
+
ctx.effect(() => settings.watch((next) => {
|
|
3369
|
+
const nextRoutes = configuredNanocodexProviders(next);
|
|
3370
|
+
adapterRegistration.replace(nextRoutes);
|
|
3371
|
+
}), "dsh-nanocodex.settings");
|
|
3372
|
+
ctx.llm.registerConfigurableProviders([{
|
|
3373
|
+
provider: "openai",
|
|
3374
|
+
displayName: "OpenAI Responses",
|
|
3375
|
+
settingsNs: SETTINGS_NAMESPACE,
|
|
3376
|
+
settingsPath: ["providers", "openai"],
|
|
3377
|
+
declared: false
|
|
3378
|
+
}, {
|
|
3379
|
+
provider: "openai-codex-responses",
|
|
3380
|
+
displayName: "OpenAI Codex Responses",
|
|
3381
|
+
settingsNs: SETTINGS_NAMESPACE,
|
|
3382
|
+
settingsPath: ["providers", "openai-codex-responses"],
|
|
3383
|
+
declared: false
|
|
3384
|
+
}]);
|
|
3385
|
+
ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
|
|
3386
|
+
const engine = new NanocodexEngine(ctx, checkpoints.table("sessions"));
|
|
3387
|
+
const patchProgress = /* @__PURE__ */ new WeakMap();
|
|
3388
|
+
ctx.effect(() => {
|
|
3389
|
+
return ctx.tools.register(createApplyPatchTool({
|
|
3390
|
+
fs: ctx.fs,
|
|
3391
|
+
ctx
|
|
3392
|
+
}, patchProgress));
|
|
3393
|
+
}, "dsh-nanocodex.apply-patch");
|
|
3394
|
+
new NanocodexCompactionEngine(ctx, engine);
|
|
3395
|
+
const factory = new NanocodexFactory(ctx, engine);
|
|
3396
|
+
ctx.effect(() => {
|
|
3397
|
+
const unregister = ctx.agents.setFactory(factory);
|
|
3398
|
+
return async () => {
|
|
3399
|
+
unregister();
|
|
3400
|
+
await factory.disposeAll();
|
|
3401
|
+
};
|
|
3402
|
+
}, "dsh-nanocodex.factory");
|
|
3403
|
+
}
|
|
3404
|
+
//#endregion
|
|
3405
|
+
export { APPLY_PATCH_DESCRIPTION, APPLY_PATCH_GRAMMAR, APPLY_PATCH_NAME, DEFAULT_MAX_PATCH_CHARS, DEFAULT_MAX_PATCH_FILES, DEFAULT_MAX_PATCH_FILE_BYTES, INTERRUPTED_TOOL_OUTPUT, MODEL_CONTEXT_WINDOW, NanocodexAgent, NanocodexCompactionEngine, NanocodexEngine, NanocodexFactory, NanocodexLlmAdapter, NanocodexSettingsSchema, PLUGIN_NAME, PatchError, SETTINGS_NAMESPACE, SUPPORTED_MODELS, SUPPORTED_PROVIDERS, apply, applyChunks, applyPatchToFileSystem, buildHistoryProjection, buildHistorySeed, buildPromptInput, configuredNanocodexProviders, createApplyPatchTool, createToolBridge, historyToolCallId, inject, isSupportedModel, isSupportedRoute, name, nanocodexCheckpointDomain, normalizeNanocodexSessionId, parsePatch, plainText, resolveNanocodexRoute, seekSequence, supportedThinking };
|