@nathapp/nax-agent 0.88.0-canary.1 → 0.88.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/CHANGELOG.md +8 -0
- package/dist/coding-tools/coding-tool-support.d.ts +16 -0
- package/dist/coding-tools/coding-tool-support.js +12 -0
- package/dist/config/schemas-sandbox.d.ts +1 -0
- package/dist/config/schemas-sandbox.js +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/process-kill.d.ts +6 -2
- package/dist/internal/process-kill.js +6 -2
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/mcp/connect.js +27 -1
- package/dist/mcp/types.d.ts +8 -0
- package/dist/native/auth.d.ts +2 -2
- package/dist/native/auth.js +4 -4
- package/dist/native/failed-round-trip-spend.d.ts +16 -0
- package/dist/native/failed-round-trip-spend.js +33 -0
- package/dist/native/session/partial-spend.d.ts +20 -0
- package/dist/native/session/partial-spend.js +14 -0
- package/dist/native/session/session.js +11 -4
- package/dist/native/session/turn-complete-step.js +23 -3
- package/dist/native/session/turn-retry.d.ts +3 -3
- package/dist/native/session/turn-retry.js +25 -6
- package/dist/native/session/turn-types.d.ts +1 -1
- package/dist/native/session-adapter.js +3 -2
- package/dist/permissions/bash-lex.js +36 -4
- package/dist/sandbox/claude-settings.d.ts +46 -0
- package/dist/sandbox/claude-settings.js +76 -0
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +1 -0
- package/dist/sandbox/launcher.js +4 -0
- package/dist/sandbox/messages.d.ts +1 -1
- package/dist/sandbox/messages.js +15 -3
- package/dist/sandbox/policy-builder.js +27 -2
- package/dist/sandbox/session-sandbox.d.ts +3 -0
- package/dist/sandbox/session-sandbox.js +9 -0
- package/dist/sandbox/srt-backend.d.ts +10 -1
- package/dist/sandbox/srt-backend.js +14 -4
- package/dist/sandbox/types.d.ts +23 -0
- package/dist/session/agent-session-options.js +1 -1
- package/dist/session/agent-session-types.d.ts +1 -1
- package/dist/session/native-backend-options.js +5 -3
- package/dist/session/session-backend.d.ts +9 -0
- package/dist/session/session-tool-support.d.ts +1 -1
- package/dist/session/session-tool-support.js +4 -2
- package/dist/session/session-types.d.ts +10 -0
- package/dist/tools/bash-canonical.d.ts +10 -0
- package/dist/tools/bash-canonical.js +207 -0
- package/dist/tools/bash.d.ts +7 -1
- package/dist/tools/bash.js +18 -3
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/nax-revert.d.ts +12 -0
- package/dist/tools/nax-revert.js +2 -2
- package/dist/tools/nax-stash-restore.d.ts +9 -0
- package/dist/tools/nax-stash-restore.js +105 -0
- package/dist/tools/nax-state-guard.js +16 -30
- package/dist/tools/nax-verb-gaps.d.ts +3 -0
- package/dist/tools/nax-verb-gaps.js +32 -0
- package/dist/tools/policy-bash.js +39 -6
- package/dist/tools/policy.js +9 -0
- package/dist/tools/provider-advertise.d.ts +13 -0
- package/dist/tools/provider-advertise.js +41 -1
- package/dist/tools/provider-instructions.d.ts +33 -0
- package/dist/tools/provider-instructions.js +63 -0
- package/dist/tools/provider-types.d.ts +12 -0
- package/dist/tools/run-command.d.ts +15 -0
- package/dist/tools/run-command.js +16 -1
- package/dist/tools/tool-audit.d.ts +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
|
|
5
5
|
release may change the public API.
|
|
6
6
|
|
|
7
|
+
## [0.88.0] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
10
|
+
|
|
11
|
+
## [0.88.0-canary.2] - 2026-10-15
|
|
12
|
+
|
|
13
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
14
|
+
|
|
7
15
|
## [0.88.0-canary.1] - 2026-10-13
|
|
8
16
|
|
|
9
17
|
- Bash guard for nax run state (#2490): `git update-index --skip-worktree` / `--assume-unchanged` (and the `--no-` forms) on a `.nax` path, `git stash` / `push` / `pop` / `apply` in a repo that holds a `.nax/` directory (unless the pathspec excludes `.nax`), and `git checkout` / `restore` of a `.nax` path are now refused in every segment of a compound command, in gated and raw mode, also behind `git -C`/`-c`, env assignments, `env`/`command`/`nice`/`timeout`/`xargs` wrappers and `bash -c`/`eval`, plus a coarse text backstop for wrappers it cannot enumerate. `RULE_SET_VERSION` is now 4 (git global options, `-c alias.*` and more wrappers are resolved when locating the subcommand).
|
|
@@ -32,6 +32,16 @@ export interface CodingToolSupport {
|
|
|
32
32
|
readonly tools: readonly CodingTool[];
|
|
33
33
|
readonly surface: ToolSurfaceDraft;
|
|
34
34
|
};
|
|
35
|
+
/** nax#2500: the dispatch launcher's sandbox as data, attached by the nax resolver for ACP opens. */
|
|
36
|
+
readonly acpSandbox?: import("#src/sandbox/types").SessionSandbox;
|
|
37
|
+
/**
|
|
38
|
+
* The rendered `## MCP servers` section (nax#2517), built by the nax resolver
|
|
39
|
+
* with `renderProviderInstructions` from this hop's resolved provider
|
|
40
|
+
* instructions. The session open folds it into the session's `system` field
|
|
41
|
+
* (native) or hands it to the ACP tool host. ABSENT when no attached provider
|
|
42
|
+
* contributed instructions, so "no section" and "an empty one" stay distinct.
|
|
43
|
+
*/
|
|
44
|
+
readonly providerInstructions?: string;
|
|
35
45
|
}
|
|
36
46
|
export declare function buildCodingToolSupport(args: {
|
|
37
47
|
root?: string;
|
|
@@ -134,6 +144,12 @@ export declare function buildCodingToolSupport(args: {
|
|
|
134
144
|
launcher?: CommandLauncher;
|
|
135
145
|
/** US-001: forwarded into ToolRunContext so Bash/Exec can SIGKILL on cancel. */
|
|
136
146
|
abortSignal?: AbortSignal;
|
|
147
|
+
/**
|
|
148
|
+
* The rendered `## MCP servers` section for this hop (nax#2517), produced by
|
|
149
|
+
* the resolver from `ResolvedProviderTools.instructions`; returned on the
|
|
150
|
+
* support unchanged.
|
|
151
|
+
*/
|
|
152
|
+
providerInstructions?: string;
|
|
137
153
|
}): CodingToolSupport | undefined;
|
|
138
154
|
/**
|
|
139
155
|
* Ledger session name.
|
|
@@ -144,8 +144,20 @@ export function buildCodingToolSupport(args) {
|
|
|
144
144
|
auditSink: sink,
|
|
145
145
|
declaredTools: args.declared,
|
|
146
146
|
...(args.toolSurface !== undefined ? { toolSurface: args.toolSurface } : {}),
|
|
147
|
+
...providerInstructionsField(args),
|
|
147
148
|
};
|
|
148
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* The support's optional `providerInstructions` (#2517) as a spreadable field —
|
|
152
|
+
* ABSENT when unset, so `"providerInstructions" in support` answers "is there
|
|
153
|
+
* an MCP section?". A helper rather than an inline conditional because
|
|
154
|
+
* `buildCodingToolSupport` sits at its complexity baseline.
|
|
155
|
+
*/
|
|
156
|
+
function providerInstructionsField(args) {
|
|
157
|
+
if (args.providerInstructions === undefined)
|
|
158
|
+
return {};
|
|
159
|
+
return { providerInstructions: args.providerInstructions };
|
|
160
|
+
}
|
|
149
161
|
/** The audit sink a support resolution records through; no dir ⇒ a non-recording sink. */
|
|
150
162
|
function supportSink(args) {
|
|
151
163
|
if (args.auditDir === undefined)
|
|
@@ -24,6 +24,7 @@ export declare const SandboxConfigSchema: z.ZodObject<{
|
|
|
24
24
|
}, z.core.$strip>>;
|
|
25
25
|
network: z.ZodPrefault<z.ZodObject<{
|
|
26
26
|
allowedDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
27
|
+
allowLocalBinding: z.ZodOptional<z.ZodBoolean>;
|
|
27
28
|
}, z.core.$strip>>;
|
|
28
29
|
}, z.core.$strip>;
|
|
29
30
|
export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
|
|
@@ -38,6 +38,15 @@ export const SandboxConfigSchema = z.object({
|
|
|
38
38
|
.object({
|
|
39
39
|
/** absent = unrestricted (spec S2); [] = no network; a list = allow-list. */
|
|
40
40
|
allowedDomains: z.array(z.string()).optional(),
|
|
41
|
+
/**
|
|
42
|
+
* US-002 (#2538): let a sandboxed command reach loopback ports.
|
|
43
|
+
*
|
|
44
|
+
* Off when absent, and it only changes anything alongside an
|
|
45
|
+
* `allowedDomains` list: with an open network srt's macOS profile already
|
|
46
|
+
* allows all traffic. srt reads it once per process, from the config given
|
|
47
|
+
* to `SandboxManager.initialize`.
|
|
48
|
+
*/
|
|
49
|
+
allowLocalBinding: z.boolean().optional(),
|
|
41
50
|
})
|
|
42
51
|
.prefault({}),
|
|
43
52
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { AfterResponsePatch, AfterResponsePayload, AfterToolPatch, AfterToo
|
|
|
26
26
|
export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
|
|
27
27
|
export { type ApprovalAuditRow, type ApprovalEntry, type ApprovalsFile, type ApprovalsFileRead, type ApprovalsFileState, type ApprovalsTaint, ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, type AskControl, type AskDecidedBy, type AskDecision, type AskLink, type AskLinkOutcome, type AskRequest, type AskResolver, type AskVerdict, appendApproval, appendApprovalAudit, approvalId, approvalsPath, type BashLexResult, type BashRedirect, type BashSegment, type BashSegmentSeparator, type BashToken, type ClearTaintOutcome, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, type MaskResult, maskForPrompt, type PrepareApprovalsStoreOptions, parseRuleList, parseToolExpression, prepareApprovalsStore, type RemovalDecision, type RemovalResult, type ResolvedPermissions, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, type SecretSpan, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
|
|
28
28
|
export { type AgentGlobOptions, type AgentRuntime, type AgentSpawnOptions, type AgentSpawnResult, type AgentSpawnStdin, getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
|
|
29
|
-
export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, type GitLayout, type LaunchRequest, type LaunchResult, type LaunchSpec, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, type ProbeResult, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, type SandboxBackend, type SandboxBackendName, type SandboxNetworkPolicy, type SandboxPolicy, type SandboxPolicyInput, type SandboxRecord, type SandboxState, type SandboxWrapRequest, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
|
|
29
|
+
export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, type GitLayout, type LaunchRequest, type LaunchResult, type LaunchSpec, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, type ProbeResult, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, type SandboxBackend, type SandboxBackendName, type SandboxNetworkPolicy, type SandboxPolicy, type SandboxPolicyInput, type SandboxRecord, type SandboxState, type SandboxWrapRequest, type SessionSandbox, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
|
|
30
30
|
export type { AdapterFailure } from "#src/session/adapter-failure";
|
|
31
31
|
export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
|
|
32
32
|
export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
|
|
@@ -42,5 +42,5 @@ export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/sessi
|
|
|
42
42
|
export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
|
|
43
43
|
export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
|
|
44
44
|
export type { TurnEvent, TurnEventSink } from "#src/session/turn-event";
|
|
45
|
-
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
45
|
+
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
46
46
|
export { EMPTY_OWNED_PATHS_POLICY, type OwnedBashCandidate, type OwnedPathsPolicy, } from "#src/tools/owned-paths";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOut
|
|
|
21
21
|
export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
|
|
22
22
|
export { ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, appendApproval, appendApprovalAudit, approvalId, approvalsPath, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, maskForPrompt, parseRuleList, parseToolExpression, prepareApprovalsStore, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
|
|
23
23
|
export { getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
|
|
24
|
-
export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
|
|
24
|
+
export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
|
|
25
25
|
export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
|
|
26
26
|
export { AgentSessionError } from "#src/session/agent-session-errors";
|
|
27
27
|
export { NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
|
|
@@ -29,5 +29,5 @@ export { nativeBackend } from "#src/session/native-backend";
|
|
|
29
29
|
export { SessionTurnError, } from "#src/session/session-types";
|
|
30
30
|
export { attachTurnSpend, readTurnSpend } from "#src/session/turn-spend";
|
|
31
31
|
export { createTurnDeadline, } from "#src/session/turn-deadline";
|
|
32
|
-
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
32
|
+
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
33
33
|
export { EMPTY_OWNED_PATHS_POLICY, } from "#src/tools/owned-paths";
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* 2. Falls back to killing the single process if group kill fails
|
|
13
13
|
*
|
|
14
14
|
* @param pid - Process ID (positive, not negative)
|
|
15
|
-
* @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
|
|
15
|
+
* @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
|
|
16
|
+
* @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
|
|
17
|
+
* exited: the bare pid may have been reused by an unrelated process by then.
|
|
16
18
|
* @returns true if kill was sent successfully, false if process already exited
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
@@ -24,4 +26,6 @@
|
|
|
24
26
|
* }
|
|
25
27
|
* ```
|
|
26
28
|
*/
|
|
27
|
-
export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number
|
|
29
|
+
export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number, opts?: {
|
|
30
|
+
readonly singleProcessFallback?: boolean;
|
|
31
|
+
}): boolean;
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* 2. Falls back to killing the single process if group kill fails
|
|
13
13
|
*
|
|
14
14
|
* @param pid - Process ID (positive, not negative)
|
|
15
|
-
* @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
|
|
15
|
+
* @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
|
|
16
|
+
* @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
|
|
17
|
+
* exited: the bare pid may have been reused by an unrelated process by then.
|
|
16
18
|
* @returns true if kill was sent successfully, false if process already exited
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
* }
|
|
25
27
|
* ```
|
|
26
28
|
*/
|
|
27
|
-
export function killProcessGroup(pid, signal) {
|
|
29
|
+
export function killProcessGroup(pid, signal, opts = {}) {
|
|
28
30
|
// A pid that is not a real child reaches other processes: 0 is our own group,
|
|
29
31
|
// 1 (as -1) is every process we may signal, and a negative pid is init or a
|
|
30
32
|
// foreign group. A spawn that failed has no pid, so refuse rather than signal.
|
|
@@ -39,6 +41,8 @@ export function killProcessGroup(pid, signal) {
|
|
|
39
41
|
const err = error;
|
|
40
42
|
// ESRCH = no such process — process group doesn't exist, try single process
|
|
41
43
|
if (err.code === "ESRCH") {
|
|
44
|
+
if (opts.singleProcessFallback === false)
|
|
45
|
+
return false;
|
|
42
46
|
try {
|
|
43
47
|
process.kill(pid, signal);
|
|
44
48
|
return true;
|
package/dist/internal.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
|
|
|
93
93
|
export * from "#src/tools/grep";
|
|
94
94
|
export * from "#src/tools/index";
|
|
95
95
|
export { _instructionAccessDeps } from "#src/tools/instruction-access";
|
|
96
|
+
export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
|
|
96
97
|
export * from "#src/tools/owned-paths";
|
|
97
98
|
export * from "#src/tools/package-managers";
|
|
98
99
|
export * from "#src/tools/policy";
|
package/dist/internal.js
CHANGED
|
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
|
|
|
93
93
|
export * from "#src/tools/grep";
|
|
94
94
|
export * from "#src/tools/index";
|
|
95
95
|
export { _instructionAccessDeps } from "#src/tools/instruction-access";
|
|
96
|
+
export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
|
|
96
97
|
// S3-2: the port (nax supplies the policy behind it from its own module).
|
|
97
98
|
export * from "#src/tools/owned-paths";
|
|
98
99
|
export * from "#src/tools/package-managers";
|
package/dist/mcp/connect.js
CHANGED
|
@@ -83,6 +83,15 @@ function createHandle(config) {
|
|
|
83
83
|
throw new McpConnectError(`MCP connect failed: ${messageOf(error)}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The server's `initialize` instructions (#2517), or `undefined` for anything
|
|
88
|
+
* that is not a non-empty string: an empty one would render an empty prompt
|
|
89
|
+
* section, and a `getInstructions()` that returns nothing is the same silence.
|
|
90
|
+
*/
|
|
91
|
+
function serverInstructions(client) {
|
|
92
|
+
const text = client.getInstructions();
|
|
93
|
+
return typeof text === "string" && text !== "" ? text : undefined;
|
|
94
|
+
}
|
|
86
95
|
export async function connectMcp(config, opts) {
|
|
87
96
|
const handle = createHandle(config);
|
|
88
97
|
const client = new Client({ name: opts.clientInfo.name, version: opts.clientInfo.version });
|
|
@@ -93,9 +102,25 @@ export async function connectMcp(config, opts) {
|
|
|
93
102
|
try {
|
|
94
103
|
if (signal.aborted)
|
|
95
104
|
throw new McpConnectError("MCP connect failed: aborted");
|
|
105
|
+
if (config.kind === "stdio" && opts.onSpawn !== undefined) {
|
|
106
|
+
const start = handle.transport.start;
|
|
107
|
+
let notified = false;
|
|
108
|
+
handle.transport.start = async function () {
|
|
109
|
+
await start.call(this);
|
|
110
|
+
const pid = handle.pid();
|
|
111
|
+
if (pid !== null && !notified) {
|
|
112
|
+
notified = true;
|
|
113
|
+
opts.onSpawn?.(pid);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
96
117
|
await client.connect(handle.transport, { signal, timeout: opts.timeoutMs });
|
|
97
118
|
const tools = await listAll(client, signal, opts.timeoutMs);
|
|
98
|
-
return liveConnection(config, client, handle, tools, {
|
|
119
|
+
return liveConnection(config, client, handle, tools, {
|
|
120
|
+
graceMs,
|
|
121
|
+
life,
|
|
122
|
+
instructions: serverInstructions(client),
|
|
123
|
+
});
|
|
99
124
|
}
|
|
100
125
|
catch (error) {
|
|
101
126
|
await closeTransport(client, handle, graceMs, life);
|
|
@@ -127,6 +152,7 @@ function liveConnection(config, client, handle, tools, state) {
|
|
|
127
152
|
return handle.pid();
|
|
128
153
|
},
|
|
129
154
|
tools,
|
|
155
|
+
...(state.instructions !== undefined ? { instructions: state.instructions } : {}),
|
|
130
156
|
call(name, input, opts) {
|
|
131
157
|
if (closing !== undefined || life.exited)
|
|
132
158
|
return Promise.reject(new McpCallError(`MCP call ${name} failed: the connection is closed`));
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface McpConnection {
|
|
|
31
31
|
/** The stdio child process pid; null for HTTP. Read before close() — the SDK may clear it after. */
|
|
32
32
|
readonly pid: number | null;
|
|
33
33
|
readonly tools: readonly McpToolInfo[];
|
|
34
|
+
/**
|
|
35
|
+
* The server's own `initialize` instructions — how to use it, in its words
|
|
36
|
+
* (#2517). Absent when it sent none (or an empty string): an MCP server that
|
|
37
|
+
* says nothing must not produce an empty prompt section.
|
|
38
|
+
*/
|
|
39
|
+
readonly instructions?: string;
|
|
34
40
|
/** Server-side tool errors are `isError` results; transport failures throw McpCallError. */
|
|
35
41
|
call(name: string, input: unknown, opts: McpCallOptions): Promise<McpCallResult>;
|
|
36
42
|
/** stdio only: called once when the process exits without close() having been called. */
|
|
@@ -45,6 +51,8 @@ export interface ConnectMcpOptions {
|
|
|
45
51
|
readonly name: string;
|
|
46
52
|
readonly version: string;
|
|
47
53
|
};
|
|
54
|
+
/** stdio only: called once after spawn, before the initialize request is sent. */
|
|
55
|
+
readonly onSpawn?: (pid: number) => void;
|
|
48
56
|
/** stdio: how long close() waits for the process before SIGKILL. Default 3000. */
|
|
49
57
|
readonly closeGraceMs?: number;
|
|
50
58
|
}
|
package/dist/native/auth.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export declare const _authDeps: {
|
|
|
29
29
|
ambientAuthAvailable: typeof ambientAuthAvailable;
|
|
30
30
|
providerIds: () => Promise<string[]>;
|
|
31
31
|
/**
|
|
32
|
-
* Injectable timer pair — lets tests drive
|
|
33
|
-
* expiry
|
|
32
|
+
* Injectable timer pair — lets tests drive ambient credential probes'
|
|
33
|
+
* expiry branches off a virtual clock instead of waiting the full 2s
|
|
34
34
|
* production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
|
|
35
35
|
*
|
|
36
36
|
* @internal
|
package/dist/native/auth.js
CHANGED
|
@@ -41,8 +41,8 @@ export const _authDeps = {
|
|
|
41
41
|
ambientAuthAvailable,
|
|
42
42
|
providerIds: async () => (await defaultProviders()).map((provider) => provider.id),
|
|
43
43
|
/**
|
|
44
|
-
* Injectable timer pair — lets tests drive
|
|
45
|
-
* expiry
|
|
44
|
+
* Injectable timer pair — lets tests drive ambient credential probes'
|
|
45
|
+
* expiry branches off a virtual clock instead of waiting the full 2s
|
|
46
46
|
* production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
|
|
47
47
|
*
|
|
48
48
|
* @internal
|
|
@@ -301,14 +301,14 @@ export async function providersWithoutCredentials(providerIds) {
|
|
|
301
301
|
})).then((missing) => missing.filter((id) => id !== undefined));
|
|
302
302
|
let timer;
|
|
303
303
|
const expiry = new Promise((resolve) => {
|
|
304
|
-
timer = setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
|
|
304
|
+
timer = _authDeps.setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
|
|
305
305
|
});
|
|
306
306
|
try {
|
|
307
307
|
return await Promise.race([sweep, expiry]);
|
|
308
308
|
}
|
|
309
309
|
finally {
|
|
310
310
|
if (timer !== undefined)
|
|
311
|
-
clearTimeout(timer);
|
|
311
|
+
_authDeps.clearTimeout(timer);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prices the tokens a failed or aborted round trip was billed for and attaches
|
|
3
|
+
* them to the thrown error (`session/partial-spend.ts`), where the turn loop's
|
|
4
|
+
* `completeWithRecovery` folds them into the turn's accumulated usage.
|
|
5
|
+
*
|
|
6
|
+
* nax-ai's `collectStream` hands the partial usage over on the error
|
|
7
|
+
* (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
|
|
8
|
+
* here, once, with the same `priceCall` the success path uses.
|
|
9
|
+
*/
|
|
10
|
+
import type { Pricing } from "#src/cost/standard-types";
|
|
11
|
+
/**
|
|
12
|
+
* Runs one round trip; if it throws, prices whatever it was billed first and
|
|
13
|
+
* attaches that to the error, which is rethrown untouched. A wrapper (rather
|
|
14
|
+
* than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function billedOnFailure<T>(rates: Pricing, roundTrip: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prices the tokens a failed or aborted round trip was billed for and attaches
|
|
3
|
+
* them to the thrown error (`session/partial-spend.ts`), where the turn loop's
|
|
4
|
+
* `completeWithRecovery` folds them into the turn's accumulated usage.
|
|
5
|
+
*
|
|
6
|
+
* nax-ai's `collectStream` hands the partial usage over on the error
|
|
7
|
+
* (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
|
|
8
|
+
* here, once, with the same `priceCall` the success path uses.
|
|
9
|
+
*/
|
|
10
|
+
import { partialUsageOf } from "@nathapp/nax-ai";
|
|
11
|
+
import { priceCall } from "#src/cost/core/index";
|
|
12
|
+
import { attachPartialSpend } from "./session/partial-spend.js";
|
|
13
|
+
function recordFailedRoundTripSpend(err, rates) {
|
|
14
|
+
const usage = partialUsageOf(err);
|
|
15
|
+
if (usage === undefined)
|
|
16
|
+
return;
|
|
17
|
+
const { costUsd, resolvedRates } = priceCall(usage, rates);
|
|
18
|
+
attachPartialSpend(err, { usage, costUsd, rates: resolvedRates });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs one round trip; if it throws, prices whatever it was billed first and
|
|
22
|
+
* attaches that to the error, which is rethrown untouched. A wrapper (rather
|
|
23
|
+
* than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
|
|
24
|
+
*/
|
|
25
|
+
export async function billedOnFailure(rates, roundTrip) {
|
|
26
|
+
try {
|
|
27
|
+
return await roundTrip();
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
recordFailedRoundTripSpend(err, rates);
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spend billed by a round trip that then failed.
|
|
3
|
+
*
|
|
4
|
+
* A failed or aborted request still bills the tokens it streamed (nax-ai's
|
|
5
|
+
* `partialUsageOf`). The adapter's `complete` closure is the only place that
|
|
6
|
+
* can price them, but it can only throw, so the priced spend rides on the
|
|
7
|
+
* thrown error's identity (a WeakMap, like `turn-spend.ts`) and the turn loop's
|
|
8
|
+
* `completeWithRecovery` folds it into the turn accumulator. `take` removes the
|
|
9
|
+
* entry, so an error reaching the fold twice cannot count twice.
|
|
10
|
+
*/
|
|
11
|
+
import type { PricingRates, TokenUsage } from "#src/cost/standard-types";
|
|
12
|
+
export interface PartialSpend {
|
|
13
|
+
readonly usage: TokenUsage;
|
|
14
|
+
readonly costUsd: number;
|
|
15
|
+
readonly rates?: PricingRates;
|
|
16
|
+
}
|
|
17
|
+
/** Attaches the priced partial spend of a failed round trip to the error it threw. */
|
|
18
|
+
export declare function attachPartialSpend(err: unknown, spend: PartialSpend): void;
|
|
19
|
+
/** Removes and returns the partial spend attached to `err`, if any. */
|
|
20
|
+
export declare function takePartialSpend(err: unknown): PartialSpend | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const partialByError = new WeakMap();
|
|
2
|
+
/** Attaches the priced partial spend of a failed round trip to the error it threw. */
|
|
3
|
+
export function attachPartialSpend(err, spend) {
|
|
4
|
+
if (typeof err === "object" && err !== null)
|
|
5
|
+
partialByError.set(err, spend);
|
|
6
|
+
}
|
|
7
|
+
/** Removes and returns the partial spend attached to `err`, if any. */
|
|
8
|
+
export function takePartialSpend(err) {
|
|
9
|
+
if (typeof err !== "object" || err === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
const spend = partialByError.get(err);
|
|
12
|
+
partialByError.delete(err);
|
|
13
|
+
return spend;
|
|
14
|
+
}
|
|
@@ -83,12 +83,19 @@ function openTranscriptStore(name, opts) {
|
|
|
83
83
|
/**
|
|
84
84
|
* Records, or clears, the session's system prompt at open. A helper so that
|
|
85
85
|
* openNativeSession gains no branch.
|
|
86
|
+
*
|
|
87
|
+
* `providerInstructions` (the rendered `## MCP servers` section, #2517) is
|
|
88
|
+
* folded in HERE rather than kept in a map of its own: the model sees one
|
|
89
|
+
* `system` string, and every consumer of `state.systemPrompts`
|
|
90
|
+
* (`systemFieldFor`, the adapter's context-budget setup) keeps working
|
|
91
|
+
* unchanged.
|
|
86
92
|
*/
|
|
87
|
-
function recordSystemPrompt(state, name,
|
|
88
|
-
|
|
93
|
+
function recordSystemPrompt(state, name, prompts) {
|
|
94
|
+
const parts = [prompts.systemPrompt, prompts.providerInstructions].filter((part) => part !== undefined && part !== "");
|
|
95
|
+
if (parts.length === 0)
|
|
89
96
|
state.systemPrompts.delete(name);
|
|
90
97
|
else
|
|
91
|
-
state.systemPrompts.set(name,
|
|
98
|
+
state.systemPrompts.set(name, parts.join("\n\n"));
|
|
92
99
|
}
|
|
93
100
|
/** The request field for the session's system prompt: `{ system }`, or `{}` when it has none. */
|
|
94
101
|
export function systemFieldFor(state, sessionName) {
|
|
@@ -119,7 +126,7 @@ export async function openNativeSession(state, name, opts) {
|
|
|
119
126
|
state.carryHistoryAcrossModels.add(name);
|
|
120
127
|
else
|
|
121
128
|
state.carryHistoryAcrossModels.delete(name);
|
|
122
|
-
recordSystemPrompt(state, name, opts
|
|
129
|
+
recordSystemPrompt(state, name, opts);
|
|
123
130
|
const instructions = await openRepositoryInstructions(name, opts, store);
|
|
124
131
|
state.repositoryInstructions.set(name, instructions);
|
|
125
132
|
state.transcripts.set(name, {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { getSafeLogger } from "#src/infra/index";
|
|
22
22
|
import { applyHistoryPatch } from "./loop-events/cache-boundary.js";
|
|
23
|
+
import { takePartialSpend } from "./partial-spend.js";
|
|
24
|
+
import { usageBeat } from "./turn-accumulator.js";
|
|
23
25
|
import { runOverflowCompaction } from "./turn-compaction-step.js";
|
|
24
26
|
import { realSleep, retryTransportFault } from "./turn-retry.js";
|
|
25
27
|
/**
|
|
@@ -89,8 +91,26 @@ export async function completeWithRecovery(args) {
|
|
|
89
91
|
honoured = wire.honoured;
|
|
90
92
|
return deps.complete(wire.messages, tools, options, turnEvents.deltaSink(roundTrip));
|
|
91
93
|
};
|
|
94
|
+
// A failed round trip was still billed for what it streamed. Folding it in
|
|
95
|
+
// here, per attempt, is what makes a retry count both attempts and an
|
|
96
|
+
// aborted or timed-out one count what it cost; `takePartialSpend` consumes
|
|
97
|
+
// the entry, and the retry loop rethrows the error it was given without
|
|
98
|
+
// re-entering this wrapper, so nothing counts twice.
|
|
99
|
+
const countedRequest = async (msgs) => {
|
|
100
|
+
try {
|
|
101
|
+
return await request(msgs);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const spend = takePartialSpend(err);
|
|
105
|
+
if (spend !== undefined) {
|
|
106
|
+
usage.add(spend.usage, spend.costUsd, spend.rates);
|
|
107
|
+
deps.onActivity?.(usageBeat(spend.usage, spend.costUsd));
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
92
112
|
try {
|
|
93
|
-
res = await
|
|
113
|
+
res = await countedRequest(messages);
|
|
94
114
|
}
|
|
95
115
|
catch (err) {
|
|
96
116
|
// Written as one guarded `if` (not a separate `canRetry` boolean) so
|
|
@@ -109,7 +129,7 @@ export async function completeWithRecovery(args) {
|
|
|
109
129
|
if (deps.transportRetry === undefined)
|
|
110
130
|
throw err;
|
|
111
131
|
res = await retryTransportFault(err, {
|
|
112
|
-
attempt: () =>
|
|
132
|
+
attempt: () => countedRequest(messages),
|
|
113
133
|
config: deps.transportRetry,
|
|
114
134
|
deadline: deps.deadline,
|
|
115
135
|
signal,
|
|
@@ -161,7 +181,7 @@ export async function completeWithRecovery(args) {
|
|
|
161
181
|
turnEvents.emit({ type: "compaction", reason: "overflow" });
|
|
162
182
|
// Retried once. A second overflow propagates: compacting further would be
|
|
163
183
|
// guessing, and the failure now carries a correct diagnosis.
|
|
164
|
-
res = await
|
|
184
|
+
res = await countedRequest(messages);
|
|
165
185
|
}
|
|
166
186
|
}
|
|
167
187
|
return { res, messages, compacted, honoured, boundary: compacted };
|
|
@@ -80,9 +80,9 @@ export declare function turnRetryFitsBudget(delayMs: number, remainingMs: number
|
|
|
80
80
|
* nax-ai's retry.ts `abortableSleep` so a pending backoff cannot outlive the
|
|
81
81
|
* caller's own cancellation and go on to start a request nobody wants.
|
|
82
82
|
*/
|
|
83
|
-
export declare function abortableSleep(ms: number, sleep: (ms: number) => Promise<void>, signal?: AbortSignal): Promise<void>;
|
|
83
|
+
export declare function abortableSleep(ms: number, sleep: (ms: number, signal?: AbortSignal) => Promise<void>, signal?: AbortSignal): Promise<void>;
|
|
84
84
|
/** The real timer. Named and exported so the only production wiring of it is one reference. */
|
|
85
|
-
export declare function realSleep(ms: number): Promise<void>;
|
|
85
|
+
export declare function realSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
86
86
|
export interface TurnRetryDeps<T> {
|
|
87
87
|
/** Re-issues the round trip. Called once per retry, never for the triggering attempt. */
|
|
88
88
|
readonly attempt: () => Promise<T>;
|
|
@@ -99,7 +99,7 @@ export interface TurnRetryDeps<T> {
|
|
|
99
99
|
* and leaving it optional let a test omit it and silently wait out a real
|
|
100
100
|
* multi-second backoff. Required makes that a compile error instead.
|
|
101
101
|
*/
|
|
102
|
-
readonly sleep: (ms: number) => Promise<void>;
|
|
102
|
+
readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
103
103
|
/** Injectable jitter source, for deterministic tests. Defaults to Math.random. */
|
|
104
104
|
readonly random?: () => number;
|
|
105
105
|
/** Fired once per retry, before the backoff delay, with a 1-based retry number. */
|
|
@@ -104,7 +104,7 @@ export function turnRetryFitsBudget(delayMs, remainingMs) {
|
|
|
104
104
|
return remainingMs === undefined || delayMs <= remainingMs;
|
|
105
105
|
}
|
|
106
106
|
function abortError(signal) {
|
|
107
|
-
return signal
|
|
107
|
+
return signal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* A sleep that resolves early, and rejects, when `signal` fires — mirrors
|
|
@@ -113,21 +113,40 @@ function abortError(signal) {
|
|
|
113
113
|
*/
|
|
114
114
|
export function abortableSleep(ms, sleep, signal) {
|
|
115
115
|
if (signal === undefined)
|
|
116
|
-
return sleep(ms);
|
|
116
|
+
return sleep(ms, signal);
|
|
117
117
|
if (signal.aborted)
|
|
118
118
|
return Promise.reject(abortError(signal));
|
|
119
119
|
return new Promise((resolve, reject) => {
|
|
120
120
|
const onAbort = () => reject(abortError(signal));
|
|
121
121
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
122
|
-
|
|
122
|
+
// Keep the race for injected sleeps that do not honour cancellation.
|
|
123
|
+
Promise.resolve()
|
|
124
|
+
.then(() => sleep(ms, signal))
|
|
125
|
+
.then(() => {
|
|
123
126
|
signal.removeEventListener("abort", onAbort);
|
|
124
127
|
resolve();
|
|
125
|
-
},
|
|
128
|
+
}, (error) => {
|
|
129
|
+
signal.removeEventListener("abort", onAbort);
|
|
130
|
+
reject(error);
|
|
131
|
+
});
|
|
126
132
|
});
|
|
127
133
|
}
|
|
128
134
|
/** The real timer. Named and exported so the only production wiring of it is one reference. */
|
|
129
|
-
export function realSleep(ms) {
|
|
130
|
-
|
|
135
|
+
export function realSleep(ms, signal) {
|
|
136
|
+
if (signal?.aborted)
|
|
137
|
+
return Promise.reject(abortError(signal));
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const onAbort = () => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
reject(abortError(signal));
|
|
142
|
+
};
|
|
143
|
+
// Cancellable timer: abort must release the pending backoff immediately.
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
signal?.removeEventListener("abort", onAbort);
|
|
146
|
+
resolve();
|
|
147
|
+
}, ms);
|
|
148
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
});
|
|
131
150
|
}
|
|
132
151
|
/**
|
|
133
152
|
* Retries `attempt` while the thrown error is a transport/overloaded fault,
|
|
@@ -97,7 +97,7 @@ export interface TurnDeps {
|
|
|
97
97
|
* actually wait (forbidden-patterns-tests.md). Absent uses a real timer —
|
|
98
98
|
* a real session always wants to actually wait.
|
|
99
99
|
*/
|
|
100
|
-
sleep?: (ms: number) => Promise<void>;
|
|
100
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
101
101
|
/**
|
|
102
102
|
* Live repetition-breaker instance (nax#2013, session-lifetime since #2047). Absent disables the breaker.
|
|
103
103
|
*/
|
|
@@ -15,6 +15,7 @@ import { _adapterDeps, authFields, isProtocolStreamError } from "./adapter-deps.
|
|
|
15
15
|
import { _clientDeps, getNativeClient } from "./client.js";
|
|
16
16
|
import { createSessionCredentialStore } from "./credentials/session-source.js";
|
|
17
17
|
import { toAdapterFailure } from "./errors.js";
|
|
18
|
+
import { billedOnFailure } from "./failed-round-trip-spend.js";
|
|
18
19
|
import { buildRateCard, NATIVE_AGENT, parseNativeModel, resolveContextWindow, toThinkingLevel } from "./models.js";
|
|
19
20
|
import { createInstructionInteraction } from "./session/instruction-interaction.js";
|
|
20
21
|
import { closeNativeSession, createNativeSessionState, markNativeTurnOutcome, openNativeSession, systemFieldFor, } from "./session/session.js";
|
|
@@ -305,7 +306,7 @@ export class NativeSessionAdapter {
|
|
|
305
306
|
const requestThinking = requestOptions?.thinking === false ? undefined : thinking;
|
|
306
307
|
try {
|
|
307
308
|
instructionInteraction.acknowledge();
|
|
308
|
-
const res = await streamComplete(client, resolved, {
|
|
309
|
+
const res = await billedOnFailure(rates, () => streamComplete(client, resolved, {
|
|
309
310
|
messages,
|
|
310
311
|
...(tools.length > 0 ? { tools } : {}),
|
|
311
312
|
sessionId,
|
|
@@ -324,7 +325,7 @@ export class NativeSessionAdapter {
|
|
|
324
325
|
// to reuse the entry, so a cache write there costs more than it
|
|
325
326
|
// saves.
|
|
326
327
|
cacheRetention: "short",
|
|
327
|
-
}, onDelta);
|
|
328
|
+
}, onDelta));
|
|
328
329
|
const usage = res.usage;
|
|
329
330
|
// Single `priceCall` invocation: `costUsd` and `resolvedRates`
|
|
330
331
|
// come from the same call so they cannot diverge — the
|