@nathapp/nax-agent 0.85.0 → 0.85.1
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 +14 -0
- package/README.md +3 -2
- package/dist/internal/redact-structured.d.ts +26 -0
- package/dist/internal/redact-structured.js +72 -0
- package/dist/internal/redact.d.ts +0 -9
- package/dist/internal/redact.js +11 -2
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/native/session/compaction-settings.d.ts +20 -0
- package/dist/native/session/compaction-settings.js +26 -0
- package/dist/session/native-backend-options.d.ts +12 -0
- package/dist/session/native-backend-options.js +2 -0
- package/dist/session/native-backend.js +2 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.85.1] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `nativeBackend({ compaction })`: `{ enabled?, compactAtPercent?, keepRecentPercent? }`, validated with the same schema as nax's `execution.compaction` (integer percentages of the context window, 50-99 and 5-79, `keepRecentPercent` at least 20 points below `compactAtPercent`). An invalid value throws `AGENT_SESSION_INVALID_OPTIONS`. `compactionSettingsSchema`, `DEFAULT_COMPACTION` and `resolveCompaction` are on `./internal` for nax and the ACP server (#2427).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Behaviour change: conversational sessions (`createAgentSession`, `resumeAgentSession`) now compact their history by default, as `nax run` does. A history past `compactAtPercent` (default 90) of the window is summarised before the round trip, and a context-overflow error is compacted and retried once, so long chats no longer end `errored` at the window. Each compaction adds one billed summary call; it emits no `usage` event. The existing `compaction` event (`reason: "proactive" | "overflow"`) is now emitted on the session stream, and the stored transcript after the turn is the compacted history. Pass `compaction: { enabled: false }` to `nativeBackend` for the old behaviour (#2427).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Secret redaction now masks values under secret-named keys in JSON text (`{"apiKey": "..."}`, including JSON escaped inside a JSON string) and in env, dotenv, shell and YAML-style lines (`API_TOKEN=...`, `export KEY="..."`, `password: ...`, `Authorization: Bearer ...`), keeping the key and structure. This reaches `tool_result.preview`, `tool_call.input` and the run log. Usage counts (`tokens`, `max_tokens`) and references (`$VAR`, `process.env.X`) are left alone (#2346).
|
|
20
|
+
|
|
7
21
|
## [0.85.0] - 2026-10-09
|
|
8
22
|
|
|
9
23
|
### Added
|
package/README.md
CHANGED
|
@@ -86,12 +86,13 @@ Profiles set what a session may do:
|
|
|
86
86
|
| `ask` | every Write, Edit, Delete, GitCommit and Bash is put to `answer()`; |
|
|
87
87
|
| `full` | no prompts; `bashApproval` and the sandbox floor apply. |
|
|
88
88
|
|
|
89
|
-
`createAgentSession` takes a `SessionBackend`. `nativeBackend(opts)` takes `instructionFileName` (default `AGENTS.md`), `model`, `credentials`, `catalogOverrides`, `loopHandlers`, `hostPorts`, `bashApproval` and `
|
|
89
|
+
`createAgentSession` takes a `SessionBackend`. `nativeBackend(opts)` takes `instructionFileName` (default `AGENTS.md`), `model`, `credentials`, `catalogOverrides`, `loopHandlers`, `hostPorts`, `bashApproval`, `allowUnsandboxed` and `compaction`. The ACP backend ships in `@nathapp/nax-agent-acp`.
|
|
90
90
|
|
|
91
91
|
`usage.costSource` is `computed` (native), `reported` or `unpriced`; never sum `unpriced` rows as cost.
|
|
92
92
|
|
|
93
93
|
- **One turn at a time.** `send()` claims the session's turn slot at once. A second `send()` while a turn runs throws `AGENT_SESSION_BUSY`. The returned iterable is single-use, and the turn starts on its first `next()`. Breaking out of the loop cancels the turn.
|
|
94
|
-
- **Events.** `turn_start`, `text_delta`, `thinking_delta`, `stream_reset`, `tool_call`, `tool_result`, `approval_requested`, `approval_resolved`, `question`, `usage`, `compaction` and `turn_end`.
|
|
94
|
+
- **Events.** `turn_start`, `text_delta`, `thinking_delta`, `stream_reset`, `tool_call`, `tool_result`, `approval_requested`, `approval_resolved`, `question`, `usage`, `compaction` and `turn_end`. Each carries `sessionId`, `turnId`, `at` and your `metadata`. `turn_end` is always last, and a failed turn arrives as `turn_end`, never as a throw.
|
|
95
|
+
- **Compaction (native only).** On by default, so a long chat keeps answering. When the history crosses `compactAtPercent` of the model's context window (default 90), the session summarises the older part with one extra model call, keeps the most recent `keepRecentPercent` of the window verbatim (default 30) and emits `compaction { reason: "proactive" }` before that round. A context-overflow error from the provider is compacted and retried once, emitting `compaction { reason: "overflow" }`. The summary call is billed but emits no `usage` event, so a turn's `usage` events cover the round trips only. The stored transcript after the turn is the compacted history, and `resumeAgentSession` loads it. Pass `nativeBackend({ model, compaction: { enabled, compactAtPercent, keepRecentPercent } })` to tune it, or `{ enabled: false }` to turn it off, in which case a conversation that outgrows the window ends `errored`. `compactAtPercent` is an integer from 50 to 99, `keepRecentPercent` an integer from 5 to 79 and at least 20 points below `compactAtPercent`; anything else throws `AGENT_SESSION_INVALID_OPTIONS`. The ACP backend ignores compaction: the external agent manages its own context.
|
|
95
96
|
- **Deltas are provisional.** `stream_reset` means the deltas of that round so far are void: the provider call was retried after a transient fault (up to 3 attempts; a rate-limit retry waits the provider's `retryAfter` silently, and `cancel()` ends the wait). `turn_end.output` and the stored transcript are authoritative. While your consumer lags, adjacent deltas are merged; control events are never merged or dropped.
|
|
96
97
|
- **Approvals and questions.** Answer them with `session.answer(requestId, { decision: "allow" | "deny" })` or `{ text }`. An unanswered one is denied after `approvalTimeoutMs` (default 600000, range 30000..3600000). `answer` returns `"accepted"`, `"expired"`, `"cancelled"` or `"unknown"`.
|
|
97
98
|
- **Credentials.** A session's `credentials` (`memory` or `exec`) and `catalogOverrides` give it its own client. Without them it uses the process-wide `configureCredentials` slot.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-aware redaction of secrets embedded in JSON- and env-shaped text.
|
|
3
|
+
*
|
|
4
|
+
* `SECRET_VALUE_PATTERNS` matches token *shapes*; it cannot see
|
|
5
|
+
* `{"client_secret": "plainvalue123"}` or `API_TOKEN=abc` because the value has
|
|
6
|
+
* no recognisable shape. These patterns key off the NAME instead, using the
|
|
7
|
+
* same unanchored-substring secret-key set as `SECRET_KEY_PATTERN` (a name
|
|
8
|
+
* merely containing a secret-ish word counts), and mask only the value so the
|
|
9
|
+
* key and the surrounding structure stay readable.
|
|
10
|
+
*
|
|
11
|
+
* These patterns are linear in the input (no nested quantifiers, no
|
|
12
|
+
* catastrophic backtracking): keys are capped at 64 characters either side of
|
|
13
|
+
* the keyword, and a value is the last element of its pattern so consuming it
|
|
14
|
+
* never backtracks. `redactSecrets` as a whole also runs the older
|
|
15
|
+
* `SECRET_VALUE_PATTERNS`, which carry their own cost profile. Values are not
|
|
16
|
+
* length-capped here; callers bound the scan (`capStrings`).
|
|
17
|
+
*
|
|
18
|
+
* `_URL` is not a text-side key: `avatar_url` / `BASE_URL` values stay visible,
|
|
19
|
+
* and credentials in a URL are masked by the userinfo pattern.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT here: `SECRET_VALUE_PATTERNS` stays the shell-command
|
|
22
|
+
* masker's list (approval prompts reject spans containing quotes), so these
|
|
23
|
+
* live in their own list that only `redactString` applies.
|
|
24
|
+
*/
|
|
25
|
+
/** Mask the value of every secret-named key in JSON-, dotenv-, shell- or YAML-shaped text. */
|
|
26
|
+
export declare function redactStructured(text: string): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-aware redaction of secrets embedded in JSON- and env-shaped text.
|
|
3
|
+
*
|
|
4
|
+
* `SECRET_VALUE_PATTERNS` matches token *shapes*; it cannot see
|
|
5
|
+
* `{"client_secret": "plainvalue123"}` or `API_TOKEN=abc` because the value has
|
|
6
|
+
* no recognisable shape. These patterns key off the NAME instead, using the
|
|
7
|
+
* same unanchored-substring secret-key set as `SECRET_KEY_PATTERN` (a name
|
|
8
|
+
* merely containing a secret-ish word counts), and mask only the value so the
|
|
9
|
+
* key and the surrounding structure stay readable.
|
|
10
|
+
*
|
|
11
|
+
* These patterns are linear in the input (no nested quantifiers, no
|
|
12
|
+
* catastrophic backtracking): keys are capped at 64 characters either side of
|
|
13
|
+
* the keyword, and a value is the last element of its pattern so consuming it
|
|
14
|
+
* never backtracks. `redactSecrets` as a whole also runs the older
|
|
15
|
+
* `SECRET_VALUE_PATTERNS`, which carry their own cost profile. Values are not
|
|
16
|
+
* length-capped here; callers bound the scan (`capStrings`).
|
|
17
|
+
*
|
|
18
|
+
* `_URL` is not a text-side key: `avatar_url` / `BASE_URL` values stay visible,
|
|
19
|
+
* and credentials in a URL are masked by the userinfo pattern.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT here: `SECRET_VALUE_PATTERNS` stays the shell-command
|
|
22
|
+
* masker's list (approval prompts reject spans containing quotes), so these
|
|
23
|
+
* live in their own list that only `redactString` applies.
|
|
24
|
+
*/
|
|
25
|
+
const REDACTED = "[REDACTED]";
|
|
26
|
+
// Text-side keyword set. Same intent as SECRET_KEY_PATTERN; TOKEN(?!s\b) keeps
|
|
27
|
+
// plural usage counts (tokens, inputTokens, max_tokens) out. The key separator
|
|
28
|
+
// may be `-` as well as `_` (x-api-key). `_URI`/`_DSN` need a character before
|
|
29
|
+
// the underscore, as in SECRET_KEY_PATTERN.
|
|
30
|
+
const KEYWORD = String.raw `(?:SECRET|TOKEN(?!s\b)|API[_-]?KEY|PASSWORD|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|WEBHOOK|AUTHORIZATION|COOKIE|CREDENTIAL|PASSWD|(?<=[\w.-])_(?:URI|DSN)|CONNECTION_?STRING)`;
|
|
31
|
+
const KEY = String.raw `[\w.-]{0,64}?${KEYWORD}[\w.-]{0,64}`;
|
|
32
|
+
/** A double-quoted JSON/YAML string body: escapes consumed pairwise, never past the closing quote. */
|
|
33
|
+
const DQ_BODY = String.raw `(?:[^"\\\r\n]|\\.)+`;
|
|
34
|
+
/** The body of a string inside JSON-in-a-string, where the closing quote is itself escaped (`\"`). */
|
|
35
|
+
const ESCAPED_BODY = String.raw `(?:[^"\\\r\n]|\\(?!\\*"))+`;
|
|
36
|
+
const SQ_BODY = String.raw `[^'\r\n]+`;
|
|
37
|
+
/**
|
|
38
|
+
* An unquoted value is left alone when it is a reference or a type rather than
|
|
39
|
+
* a credential: `$VAR`, `${VAR}`, `process.env.X`, `: string`, `=true`, and an
|
|
40
|
+
* already-masked span.
|
|
41
|
+
*/
|
|
42
|
+
const NOT_A_REFERENCE = String.raw `(?!["'$<{(\[]|process\.|import\.meta|os\.|env\.|await\b|new\b|[\w.]+\(|(?:string|number|boolean|str|int|any|unknown|undefined|null|true|false|none)(?![\w-]))`;
|
|
43
|
+
/** `token_count: 5` is a usage figure, not a credential. */
|
|
44
|
+
const NOT_A_TOKEN_COUNT = String.raw `(?![\w.-]{0,64}?TOKEN(?!s\b)[\w.-]{0,64}[ \t]{0,8}[=:][ \t]{0,8}\d+(?![\w.-]))`;
|
|
45
|
+
const AUTH_SCHEME = String.raw `(?:(?:Bearer|Basic|Digest|Token)[ \t]+)?`;
|
|
46
|
+
/** Each pattern captures the text to keep as group 1 and the value to mask as group 2. */
|
|
47
|
+
const STRUCTURED_PATTERNS = [
|
|
48
|
+
// {"apiKey": "v"}, any whitespace around the colon.
|
|
49
|
+
{ re: new RegExp(String.raw `("${KEY}"\s{0,256}:\s{0,256}")(${DQ_BODY})`, "gi") },
|
|
50
|
+
// Python reprs / single-quoted JSON: {'apiKey': 'v'}, {'apiKey': "v"}.
|
|
51
|
+
{ re: new RegExp(String.raw `('${KEY}'\s{0,256}:\s{0,256}')(${SQ_BODY})`, "gi") },
|
|
52
|
+
{ re: new RegExp(String.raw `('${KEY}'\s{0,256}:\s{0,256}")(${DQ_BODY})`, "gi") },
|
|
53
|
+
// JSON inside a JSON string: {\"apiKey\": \"v\"}.
|
|
54
|
+
{ re: new RegExp(String.raw `(\\{1,3}"${KEY}\\{1,3}"\s{0,256}:\s{0,256}\\{1,3}")(${ESCAPED_BODY})`, "gi") },
|
|
55
|
+
// export OPENAI_API_KEY="v", secret_key: "v".
|
|
56
|
+
{ re: new RegExp(String.raw `(?<![\w.-])(${KEY}[ \t]{0,8}[=:][ \t]{0,8}")(${DQ_BODY})`, "gi") },
|
|
57
|
+
// SECRET_KEY='v'
|
|
58
|
+
{ re: new RegExp(String.raw `(?<![\w.-])(${KEY}[ \t]{0,8}[=:][ \t]{0,8}')(${SQ_BODY})`, "gi") },
|
|
59
|
+
// API_TOKEN=v, password: v, x-api-key: v, Authorization: Bearer v.
|
|
60
|
+
{
|
|
61
|
+
re: new RegExp(String.raw `(?<![\w.-])${NOT_A_TOKEN_COUNT}(${KEY}[ \t]{0,8}[=:][ \t]{0,8})${NOT_A_REFERENCE}(${AUTH_SCHEME}[^\s"',]+)`, "gi"),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
/** Mask the value of every secret-named key in JSON-, dotenv-, shell- or YAML-shaped text. */
|
|
65
|
+
export function redactStructured(text) {
|
|
66
|
+
let out = text;
|
|
67
|
+
for (const { re } of STRUCTURED_PATTERNS) {
|
|
68
|
+
re.lastIndex = 0;
|
|
69
|
+
out = out.replace(re, (_match, keep) => `${keep}${REDACTED}`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secret-redaction helper for the JSONL logger write path.
|
|
3
|
-
*
|
|
4
|
-
* Two layers of protection:
|
|
5
|
-
* 1. Key-based: any field whose name matches SECRET_KEY_PATTERN gets its value
|
|
6
|
-
* replaced with "[REDACTED]" regardless of content.
|
|
7
|
-
* 2. Pattern-based: free-text strings (in values that did NOT trigger layer 1)
|
|
8
|
-
* are scanned for token-shaped substrings (API keys, PATs, etc.).
|
|
9
|
-
*/
|
|
10
1
|
/**
|
|
11
2
|
* Patterns are reset via `re.lastIndex = 0` before every call because they carry
|
|
12
3
|
* the `/g` flag (required for `String.replace`). Resetting prevents the stale
|
package/dist/internal/redact.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 2. Pattern-based: free-text strings (in values that did NOT trigger layer 1)
|
|
8
8
|
* are scanned for token-shaped substrings (API keys, PATs, etc.).
|
|
9
9
|
*/
|
|
10
|
+
import { redactStructured } from "#src/internal/redact-structured";
|
|
10
11
|
// TOKEN(?!s\b) prevents matching plural metric keys like "tokens", "inputTokens",
|
|
11
12
|
// "totalTokens" (which are counts, not credentials) while still matching "token",
|
|
12
13
|
// "GITHUB_TOKEN", "accessToken", etc.
|
|
@@ -93,13 +94,21 @@ export const SECRET_VALUE_PATTERNS = [
|
|
|
93
94
|
{ kind: "url-credentials", re: /\b[a-z][a-z0-9+.-]*:\/\/(?:[^/\s@]*:[^/\s@]+)@/gi },
|
|
94
95
|
];
|
|
95
96
|
const REDACTED = "[REDACTED]";
|
|
97
|
+
/**
|
|
98
|
+
* Kinds that `redactStructured` supersedes for text redaction. They consume
|
|
99
|
+
* the key (`API_TOKEN=abc` -> `API_[REDACTED]`); the structured pass masks only
|
|
100
|
+
* the value. They stay in `SECRET_VALUE_PATTERNS` for the shell-span masker.
|
|
101
|
+
*/
|
|
102
|
+
const STRUCTURED_KINDS = new Set(["assignment", "api-key-header"]);
|
|
96
103
|
function redactString(value) {
|
|
97
104
|
let out = value;
|
|
98
|
-
for (const { re } of SECRET_VALUE_PATTERNS) {
|
|
105
|
+
for (const { kind, re } of SECRET_VALUE_PATTERNS) {
|
|
106
|
+
if (STRUCTURED_KINDS.has(kind))
|
|
107
|
+
continue;
|
|
99
108
|
re.lastIndex = 0;
|
|
100
109
|
out = out.replace(re, REDACTED);
|
|
101
110
|
}
|
|
102
|
-
return out;
|
|
111
|
+
return redactStructured(out);
|
|
103
112
|
}
|
|
104
113
|
/**
|
|
105
114
|
* Recursively walk `input` and return a new value with secrets removed.
|
package/dist/internal.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export * from "#src/native/model-resolver";
|
|
|
59
59
|
export * from "#src/native/models";
|
|
60
60
|
export * from "#src/native/provider-models";
|
|
61
61
|
export * from "#src/native/session/compaction";
|
|
62
|
+
export { compactionSettingsSchema, DEFAULT_COMPACTION, resolveCompaction, } from "#src/native/session/compaction-settings";
|
|
62
63
|
export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
|
|
63
64
|
export * from "#src/native/session/loop-events/external-handler";
|
|
64
65
|
export * from "#src/native/session/loop-events/index";
|
package/dist/internal.js
CHANGED
|
@@ -59,6 +59,7 @@ export * from "#src/native/model-resolver";
|
|
|
59
59
|
export * from "#src/native/models";
|
|
60
60
|
export * from "#src/native/provider-models";
|
|
61
61
|
export * from "#src/native/session/compaction";
|
|
62
|
+
export { compactionSettingsSchema, DEFAULT_COMPACTION, resolveCompaction, } from "#src/native/session/compaction-settings";
|
|
62
63
|
export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
|
|
63
64
|
export * from "#src/native/session/loop-events/external-handler";
|
|
64
65
|
export * from "#src/native/session/loop-events/index";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compaction settings schema, shared by nax (`execution.compaction`) and the
|
|
3
|
+
* facade's native backend (`NativeBackendOptions.compaction`). One definition,
|
|
4
|
+
* so the two surfaces cannot drift on ranges, defaults or messages.
|
|
5
|
+
*
|
|
6
|
+
* Percentages of the model's window rather than absolute token counts:
|
|
7
|
+
* nax-ai's catalog spans 4095 to 3.5M tokens, and an absolute reserve is
|
|
8
|
+
* negative on the smallest windows -- compaction would fire every round trip
|
|
9
|
+
* and shrink nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { ResolvedCompaction } from "./compaction.ts";
|
|
13
|
+
export declare const DEFAULT_COMPACTION: ResolvedCompaction;
|
|
14
|
+
export declare const compactionSettingsSchema: z.ZodDefault<z.ZodObject<{
|
|
15
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
compactAtPercent: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
keepRecentPercent: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
/** Applies the defaults to a raw settings object. Throws a ZodError on an invalid one. */
|
|
20
|
+
export declare function resolveCompaction(input?: unknown): ResolvedCompaction;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compaction settings schema, shared by nax (`execution.compaction`) and the
|
|
3
|
+
* facade's native backend (`NativeBackendOptions.compaction`). One definition,
|
|
4
|
+
* so the two surfaces cannot drift on ranges, defaults or messages.
|
|
5
|
+
*
|
|
6
|
+
* Percentages of the model's window rather than absolute token counts:
|
|
7
|
+
* nax-ai's catalog spans 4095 to 3.5M tokens, and an absolute reserve is
|
|
8
|
+
* negative on the smallest windows -- compaction would fire every round trip
|
|
9
|
+
* and shrink nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
export const DEFAULT_COMPACTION = { enabled: true, compactAtPercent: 90, keepRecentPercent: 30 };
|
|
13
|
+
export const compactionSettingsSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
enabled: z.boolean().default(DEFAULT_COMPACTION.enabled),
|
|
16
|
+
compactAtPercent: z.number().int().min(50).max(99).default(DEFAULT_COMPACTION.compactAtPercent),
|
|
17
|
+
keepRecentPercent: z.number().int().min(5).max(79).default(DEFAULT_COMPACTION.keepRecentPercent),
|
|
18
|
+
})
|
|
19
|
+
.refine((c) => c.keepRecentPercent <= c.compactAtPercent - 20, {
|
|
20
|
+
message: "keepRecentPercent must be at least 20 points below compactAtPercent",
|
|
21
|
+
})
|
|
22
|
+
.default({ ...DEFAULT_COMPACTION });
|
|
23
|
+
/** Applies the defaults to a raw settings object. Throws a ZodError on an invalid one. */
|
|
24
|
+
export function resolveCompaction(input) {
|
|
25
|
+
return compactionSettingsSchema.parse(input);
|
|
26
|
+
}
|
|
@@ -15,6 +15,18 @@ export interface NativeBackendOptions {
|
|
|
15
15
|
readonly allowUnsandboxed?: boolean;
|
|
16
16
|
/** Keep a resumed or continued conversation when the model differs (ACP server model switch, S5-3). */
|
|
17
17
|
readonly carryHistoryAcrossModels?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Context compaction for the session's history. On by default: a history that
|
|
20
|
+
* crosses `compactAtPercent` of the model's window is summarised (one billed
|
|
21
|
+
* model call) and a context overflow is compacted and retried once. Percentages
|
|
22
|
+
* are of the window; `keepRecentPercent` must sit at least 20 points below
|
|
23
|
+
* `compactAtPercent`. `{ enabled: false }` turns it off.
|
|
24
|
+
*/
|
|
25
|
+
readonly compaction?: {
|
|
26
|
+
readonly enabled?: boolean;
|
|
27
|
+
readonly compactAtPercent?: number;
|
|
28
|
+
readonly keepRecentPercent?: number;
|
|
29
|
+
};
|
|
18
30
|
}
|
|
19
31
|
export interface ResolvedNativeOptions {
|
|
20
32
|
/** The caller's object, unchanged. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { parseNativeModel } from "#src/native/models";
|
|
9
|
+
import { compactionSettingsSchema } from "#src/native/session/compaction-settings";
|
|
9
10
|
import { isInstructionFileName } from "#src/native/session/instruction-file-name";
|
|
10
11
|
import { AgentSessionError } from "./agent-session-errors.js";
|
|
11
12
|
function isRecord(value) {
|
|
@@ -37,6 +38,7 @@ const OptionsSchema = z.strictObject({
|
|
|
37
38
|
bashApproval: z.enum(["raw", "gated", "escalate"]).optional(),
|
|
38
39
|
allowUnsandboxed: z.boolean().optional(),
|
|
39
40
|
carryHistoryAcrossModels: z.boolean().optional(),
|
|
41
|
+
compaction: compactionSettingsSchema.optional(),
|
|
40
42
|
});
|
|
41
43
|
function invalid(message, context = {}) {
|
|
42
44
|
return new AgentSessionError(`Invalid agent session options: ${message}`, "AGENT_SESSION_INVALID_OPTIONS", context);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { DEFAULT_SPIN_BREAKER_SETTINGS } from "#src/infra/spin-breaker/index";
|
|
8
8
|
import { NATIVE_AGENT } from "#src/native/models";
|
|
9
|
+
import { resolveCompaction } from "#src/native/session/compaction-settings";
|
|
9
10
|
import { transcriptModelIdentity } from "#src/native/session/transcript-identity";
|
|
10
11
|
import { NativeSessionAdapter } from "#src/native/session-adapter";
|
|
11
12
|
import { AgentSessionError } from "./agent-session-errors.js";
|
|
@@ -84,6 +85,7 @@ async function openNative(resolved, ctx) {
|
|
|
84
85
|
resume: ctx.resume !== undefined,
|
|
85
86
|
spinBreaker: DEFAULT_SPIN_BREAKER_SETTINGS,
|
|
86
87
|
transportRetry: SESSION_TRANSPORT_RETRY,
|
|
88
|
+
compaction: resolveCompaction(raw.compaction),
|
|
87
89
|
...(raw.carryHistoryAcrossModels === true ? { carryHistoryAcrossModels: true } : {}),
|
|
88
90
|
...(ctx.instructions !== undefined && ctx.instructions !== "" ? { systemPrompt: ctx.instructions } : {}),
|
|
89
91
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax-agent",
|
|
3
|
-
"version": "0.85.
|
|
3
|
+
"version": "0.85.1",
|
|
4
4
|
"description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "William Khoo",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
50
|
-
"@nathapp/nax-ai": "0.85.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
50
|
+
"@nathapp/nax-ai": "0.85.1",
|
|
51
51
|
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|