@armadra/agent 0.7.3 → 0.7.5
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 +48 -0
- package/CHANGELOG.zh-CN.md +48 -0
- package/dist/agent/retry.d.ts +14 -5
- package/dist/agent/retry.js +30 -8
- package/dist/agent/session-cache-key.d.ts +8 -0
- package/dist/agent/session-cache-key.js +46 -0
- package/dist/agent/session-cache.d.ts +2 -3
- package/dist/agent/session-cache.js +19 -37
- package/dist/agent/session-classifier.d.ts +3 -2
- package/dist/agent/session-classifier.js +36 -10
- package/dist/agent/session-compaction.d.ts +24 -5
- package/dist/agent/session-compaction.js +54 -20
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-run.d.ts +12 -0
- package/dist/agent/session-run.js +29 -10
- package/dist/agent/session-subagent.d.ts +8 -1
- package/dist/agent/session-subagent.js +37 -14
- package/dist/agent/session-sync.d.ts +8 -2
- package/dist/agent/session-sync.js +7 -10
- package/dist/agent/session-tools.js +3 -2
- package/dist/agent/subagent-fork.d.ts +67 -0
- package/dist/agent/subagent-fork.js +87 -0
- package/dist/agent/subagent-registry.d.ts +2 -2
- package/dist/agent/subagent-registry.js +2 -2
- package/dist/agent/tool-availability.d.ts +8 -0
- package/dist/agent/tool-availability.js +8 -0
- package/dist/agent/tool-runner.js +13 -4
- package/dist/agent/worktree.d.ts +8 -0
- package/dist/agent/worktree.js +51 -4
- package/dist/agents/parse.js +6 -1
- package/dist/agents/task-record.js +3 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/ai/apis/anthropic-messages.js +10 -5
- package/dist/ai/apis/anthropic-request.d.ts +2 -1
- package/dist/ai/apis/anthropic-request.js +15 -4
- package/dist/ai/apis/cache-params.js +2 -1
- package/dist/ai/apis/google-generative-ai.js +4 -2
- package/dist/ai/apis/google-request.js +2 -1
- package/dist/ai/apis/max-tokens.d.ts +42 -0
- package/dist/ai/apis/max-tokens.js +121 -0
- package/dist/ai/apis/openai-completions.js +10 -5
- package/dist/ai/apis/openai-request.js +4 -2
- package/dist/ai/apis/openai-responses-request.js +3 -2
- package/dist/ai/apis/openai-responses.js +9 -5
- package/dist/ai/apis/shared.d.ts +3 -1
- package/dist/ai/apis/shared.js +21 -3
- package/dist/ai/cache/fingerprint.d.ts +4 -1
- package/dist/ai/cache/fingerprint.js +18 -1
- package/dist/ai/cache/miss.d.ts +2 -1
- package/dist/ai/cache/miss.js +9 -3
- package/dist/ai/cache/reporting.js +5 -1
- package/dist/ai/cache/types.d.ts +10 -2
- package/dist/ai/cache/warmer.d.ts +6 -1
- package/dist/ai/cache/warmer.js +7 -0
- package/dist/ai/context.d.ts +12 -4
- package/dist/ai/context.js +43 -24
- package/dist/ai/fake/fake-provider.d.ts +8 -1
- package/dist/ai/fake/fake-provider.js +19 -7
- package/dist/ai/fake/fake-script.d.ts +2 -0
- package/dist/ai/http.d.ts +17 -9
- package/dist/ai/http.js +33 -13
- package/dist/ai/image-intern.d.ts +29 -0
- package/dist/ai/image-intern.js +58 -0
- package/dist/ai/json-body.d.ts +63 -0
- package/dist/ai/json-body.js +286 -0
- package/dist/ai/overflow.js +5 -0
- package/dist/ai/providers/catalog-data.js +13 -13
- package/dist/ai/providers/catalog.d.ts +33 -0
- package/dist/ai/providers/catalog.js +105 -6
- package/dist/ai/providers/enrich.d.ts +13 -3
- package/dist/ai/providers/enrich.js +87 -31
- package/dist/ai/sse.d.ts +3 -2
- package/dist/ai/sse.js +3 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +3637 -1973
- package/dist/cli/bootstrap.js +13 -5
- package/dist/cli/compose-agents.js +3 -0
- package/dist/cli/compose-extensions.js +2 -0
- package/dist/cli/compose-request.d.ts +20 -0
- package/dist/cli/compose-request.js +37 -0
- package/dist/cli/compose-session.js +2 -2
- package/dist/cli/deps.d.ts +1 -1
- package/dist/cli/subcommands/model-meta.d.ts +2 -1
- package/dist/cli/subcommands/model-meta.js +3 -2
- package/dist/codemode/host-side.d.ts +11 -4
- package/dist/codemode/host-side.js +25 -6
- package/dist/codemode/tool.d.ts +2 -0
- package/dist/codemode/tool.js +4 -0
- package/dist/config/json-schema.js +9 -2
- package/dist/config/key-docs.js +13 -4
- package/dist/config/schema-w5.js +9 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +19 -3
- package/dist/config/settings-registry.js +6 -0
- package/dist/config/types-w5.d.ts +2 -0
- package/dist/config/types.d.ts +19 -1
- package/dist/config/types.js +4 -0
- package/dist/i18n/catalog.d.ts +13 -2
- package/dist/i18n/messages/cli.d.ts +2 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +10 -0
- package/dist/i18n/messages/config-keys.js +12 -2
- package/dist/i18n/messages/config.d.ts +14 -0
- package/dist/i18n/messages/config.js +4 -0
- package/dist/i18n/messages/errors.d.ts +3 -2
- package/dist/i18n/messages/errors.js +15 -2
- package/dist/i18n/messages/report.d.ts +2 -0
- package/dist/i18n/messages/report.js +2 -0
- package/dist/i18n/messages/settings.d.ts +4 -0
- package/dist/i18n/messages/settings.js +6 -2
- package/dist/i18n/messages/subcommands.d.ts +2 -2
- package/dist/i18n/messages/subcommands.js +4 -2
- package/dist/modes/acp/acp-server.js +30 -18
- package/dist/modes/acp/acp-sessions.d.ts +22 -0
- package/dist/modes/acp/acp-sessions.js +68 -0
- package/dist/modes/context-report.js +3 -2
- package/dist/modes/print/json-event.d.ts +12 -1
- package/dist/modes/print/json-event.js +48 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -0
- package/dist/modes/rpc/rpc-mode.js +9 -2
- package/dist/modes/session-report.d.ts +1 -1
- package/dist/modes/session-report.js +12 -6
- package/dist/rpc.d.ts +5 -2
- package/dist/session/line-reader.d.ts +28 -0
- package/dist/session/line-reader.js +82 -0
- package/dist/session/list.d.ts +14 -0
- package/dist/session/list.js +150 -0
- package/dist/session/manager.d.ts +23 -3
- package/dist/session/manager.js +91 -62
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- package/dist/session/offload.d.ts +51 -0
- package/dist/session/offload.js +195 -0
- package/dist/session/projection.d.ts +16 -3
- package/dist/session/projection.js +96 -29
- package/dist/session/scan.d.ts +3 -3
- package/dist/session/scan.js +9 -18
- package/dist/session/store.d.ts +15 -4
- package/dist/session/store.js +54 -38
- package/dist/session/types.d.ts +7 -2
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.js +5 -4
- package/dist/tools/glob.d.ts +2 -2
- package/dist/tools/glob.js +2 -2
- package/dist/tools/grep.js +4 -6
- package/dist/tools/image-file.js +2 -1
- package/dist/tools/read-lines.d.ts +71 -0
- package/dist/tools/read-lines.js +224 -0
- package/dist/tools/read.d.ts +6 -2
- package/dist/tools/read.js +47 -27
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +21 -8
- package/dist/tools/truncate.d.ts +7 -0
- package/dist/tools/truncate.js +11 -0
- package/dist/tools/types.d.ts +13 -0
- package/dist/trace/build-index.js +13 -1
- package/dist/tui/ansi.d.ts +2 -0
- package/dist/tui/ansi.js +7 -3
- package/dist/tui/components/editor-buffer.js +2 -2
- package/dist/tui/components/editor.js +5 -6
- package/dist/tui/keys.js +2 -2
- package/dist/tui/stdin-buffer.js +3 -3
- package/docs/acp.md +8 -1
- package/docs/agents.md +42 -6
- package/docs/codemode.md +4 -2
- package/docs/en/acp.md +3 -2
- package/docs/en/host-api.md +1 -1
- package/docs/en/permissions.md +2 -2
- package/docs/en/providers.md +54 -20
- package/docs/en/rpc.md +23 -7
- package/docs/en/sessions.md +4 -2
- package/docs/en/tui.md +2 -2
- package/docs/permissions.md +4 -1
- package/docs/providers.md +85 -22
- package/docs/rpc.md +22 -6
- package/docs/session-format.md +13 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ English · [简体中文](CHANGELOG.zh-CN.md)
|
|
|
5
5
|
> This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
|
|
6
6
|
> [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
|
|
7
7
|
|
|
8
|
+
## 0.7.5 (2026-10-10)
|
|
9
|
+
|
|
10
|
+
### Memory footprint
|
|
11
|
+
|
|
12
|
+
- **`read` no longer blocks the event loop on large files**: the byte-window path for text files over 1 MiB now reads 64 KiB chunks asynchronously, awaiting once per chunk, so TUI/RPC/ACP stay responsive while a 256 MB file is scanned (longest event-loop stall about 170–320 ms → under 20 ms). Output is byte-for-byte identical (the sync and async readers share one line parser); peak memory is unchanged (still one chunk buffer).
|
|
13
|
+
- **Request bodies with images no longer copy each image**: image data goes from the transcript straight into the request body instead of first being joined into a new data-URL string on every request, its escape check runs once per image, and the body is encoded in chunks of at most 256 Ki characters instead of one whole image at a time. Mock 300-step session with 15 images: peak RSS about 630–690 → 390–400 MB, peak heapUsed about 270 → 85 MB. Request bytes are unchanged; SDK `onPayload` callbacks now see large images as objects with `toJSON` (`JSON.stringify` gives the same text).
|
|
14
|
+
- **Old images released after downgrade**: once `context_edit` (for example `image_budget`) replaces a message on the active branch, its image base64 is no longer kept in memory — only the line's position in the session file — and is read back on demand: `get_entries` and fork return the original, and moving the leaf back before the edit (`/tree`, rewind) restores the images. Resuming a session also strips those images while reading, so they no longer count toward the peak (55 MB session with 10 downgraded 3 MB images: peak RSS about 254 → 210 MB). The JSONL format is unchanged; in `entries()` / `branch()` / SDK `session.entries` such image blocks now have `data: ""`.
|
|
15
|
+
- **Closed ACP sessions free the startup session too**: `Runtime.session` and `Runtime.sessionManager` now follow the current session instead of staying on the one created at startup, so after `session/close` its transcript and images are garbage-collected like any other session's (8 sessions × 4 rounds, all closed: external 16.3 → 7.0 MB). SDK code that reads `runtime.session` after a session switch now gets the new session.
|
|
16
|
+
- **ACP `session/prompt` usage covers only its own turn**: a prompt that waited for a background subagent's notification turn no longer counts that turn's tokens in `usage`.
|
|
17
|
+
- **`session/close` during a notification turn**: it used to fail with -32603 and keep the session in memory; now running background subagents are stopped, the running turn (notification turns included) is interrupted, already queued notifications no longer start a turn, and the session is released with no further `session/update`. Closing stdin quiets sessions the same way.
|
|
18
|
+
|
|
19
|
+
## 0.7.4 (2026-10-10)
|
|
20
|
+
|
|
21
|
+
- **Parallel worktree isolation**: two isolated tasks starting together could fail with `could not read .git/worktrees/<id>/commondir`, because `git worktree add` read the other task's half-created admin directory. Worktree add / remove and branch deletion now run one at a time per repository, and the same transient errors from another ama process are retried briefly.
|
|
22
|
+
|
|
23
|
+
### Model efficiency
|
|
24
|
+
|
|
25
|
+
Fewer full-price re-reads of the prompt prefix and fewer failed requests (docs/model-efficiency-plan.md; measurements in docs/benchmarks/efficiency-2026-10.md). Grouped by area: sub-agents, prompt prefix and compaction, the request layer, then catalog and tools.
|
|
26
|
+
|
|
27
|
+
- **Fork-style sub-agents** (`task.context: "fork"`, or `context: fork` in an agent definition; default stays `fresh`): the sub-agent inherits the conversation up to the `task` call, with the same system prompt and tool table as the parent, so its first request reuses the parent's cached prefix (measured on relays: Kimi 97.5% cached, DeepSeek the same as the parent's own next turn). Type tool limits are enforced at execution time instead of changing the tool table. Falls back to `fresh` (logged, `details.context` and `TaskInfo.context` show the actual mode) when another model or thinking level is requested, the parent has not sent a request yet, or the parent context exceeds half of the usable window. The turn-limit final round no longer sends `toolChoice: "none"`, which broke the cached prefix; the report prompt alone asks for no tools.
|
|
28
|
+
- **The head of the prompt is written once per session**: compaction no longer folds mid-session system patches back into the leading system prompt — the checkpoint replays only what was sent before the conversation started and later patches follow the summary as a `<system-reminder>`, so the first request after compaction has the same system + tools bytes as before. Removing a tool mid-session keeps its declaration in the tool table, adds a reminder that it is no longer available and rejects calls with `Tool "X" is not available in this session.` (also the text for unknown tools, which used to be `Tool X not found`); adding it back only says it is available again. The reminder's closing sentence now also mentions tool availability notes.
|
|
29
|
+
- **Compaction summarizes by continuation instead of pruning first**: when pruning tool results would still leave the context over budget and the cache is warm, the summary continues the last request's cached prefix without pruning (pruning would break that prefix and force a full-price standalone summary); with a cold cache it prunes first as before and no longer attempts the continuation. With thinking enabled, the continuation's output limit is the summary limit plus the thinking budget.
|
|
30
|
+
- **Soft context window and per-section fingerprints**: `compaction.contextBudget` now takes effect — pruning, summarization, the circuit breaker and `context_pressure` use min(model window, budget). `cache_miss.detail` names the changed system sections (`system:hooks,memory`) and `/cache fingerprint` lists a hash per section. Cache warming on `openai-responses` uses `maxTokens` 16 (the protocol minimum) instead of 1.
|
|
31
|
+
- **Fourth Anthropic cache breakpoint**: besides the last user message, end of system and last tool, the second-to-last user message (where the previous request wrote) is now marked too, so a turn with many parallel tool results no longer rewrites the whole stretch when it falls outside the lookback window. With `maxCacheBreakpoints` below 4, the last tool is dropped first.
|
|
32
|
+
- **Retries honor Retry-After and treat rate limits separately**: the backoff is `max(exponential backoff, Retry-After)`, capped at `retry.maxDelayMs`, with ±20 % jitter; 429 / 529 / "rate limit" errors get two more attempts than `retry.maxRetries` (`auto_retry_start.maxAttempts` shows it). 5xx is only recognized as a leading status code or after `status` / `HTTP`, so "500 tokens" in an error is no longer a server error. With `fallbackModel` set, overloaded is retried once quickly (1 s) before switching models.
|
|
33
|
+
- **max_tokens no longer fails the first request**: when the context window is known, `max_tokens` is clamped to what is left in the window (Anthropic budgeted thinking excepted); a 400 that states the endpoint's limit (`Range of max_tokens should be [1, N]` and similar) is resent once with that limit, and later requests in the same process use it directly. Anthropic's `input length and max_tokens exceed context limit` is resent with the room left, or handled as an overflow when under 1024 tokens.
|
|
34
|
+
- **Tool-call arguments are replayed byte for byte**: Completions and Responses requests send back the model's own arguments string instead of re-serializing it, so spacing differences no longer break the prefix cache. Older sessions fall back to serializing.
|
|
35
|
+
- **Separate idle timeout inside the stream**: `request.idleTimeoutMs` (300 s) now only covers waiting for the response headers; gaps between chunks once the stream has started use the new `request.streamIdleTimeoutMs` (default 180 s, `AMA_STREAM_IDLE_TIMEOUT_MS`, 0 disables). Previously `idleTimeoutMs` covered both, so if you raised or disabled it for a slow endpoint, set `streamIdleTimeoutMs` as well.
|
|
36
|
+
- **Relay models inherit the official catalog**: a relay or custom model whose id uniquely matches a built-in catalog entry (lowercased, one vendor prefix and `:latest` stripped, also via catalog `aliases`) inherits `reasoning`, `input`, `thinkingLevelMap`, `promptCache.minTokens` and `compat.requiresReasoningContentOnAssistantMessages`, and models.dev matches that entry's snapshot; prices, TTLs and `thinkingFormat` are not inherited. Ids with a thinking-tier suffix (`gemini-3.8-flash-low`) match after stripping it, inheriting images and window without turning thinking on — relay Gemini Flash models now accept images instead of being refused. `models[].catalog: false` turns it off, `"provider/id"` names the entry; `ama models list` shows `catalog (via id)` and the entry. `deepseek-v4-flash` is an alias of `deepseek-flash`.
|
|
37
|
+
- **Tool results are cut once**: `read`, `grep` and `bash` truncate to the session's `tools.maxToolResultChars` (capped at 50 KB) themselves, so the note states the real limit and where to continue instead of a second middle cut by the session; their descriptions no longer quote fixed sizes (shorter prefix). `glob` now returns at most 200 files by default (was 1000).
|
|
38
|
+
- **auto mode classifies with a small model**: without `permission.autoModel`, the classifier uses the session provider's catalog small model (`small` in the catalog: deepseek-flash, claude-haiku-4-5, gpt-6-luna, gemini-3.5-flash-lite, kimi-k2.6, …) when it exists and has a key, otherwise the session model.
|
|
39
|
+
- **Compatibility**: everything above adds only optional fields — `rawArguments` on tool-call blocks and `retryAfterMs` on failed assistant messages (session files and RPC), `context` on `ama.task` data and `TaskInfo`, `system:<sections>` values of `cache_miss.detail`, `aliases` / `small` in the built-in catalog, and the config keys `compaction.contextBudget`, `request.streamIdleTimeoutMs` and `models[].catalog`. Session format and RPC / ACP protocol versions are unchanged.
|
|
40
|
+
|
|
41
|
+
### Memory footprint
|
|
42
|
+
|
|
43
|
+
Lower peaks with large inputs and long sessions, and closed ACP sessions are freed (docs/memory-plan.md; report in docs/research/memory-2026-10.md, before / after measurements in docs/benchmarks/memory-2026-10.md). Grouped by area: large files and sessions, requests and images, protocol modes, then distribution and limits. Session files, request bytes and tool output are unchanged.
|
|
44
|
+
|
|
45
|
+
- **`read` on large files no longer loads the whole file**: text files over 1 MiB are scanned in 64 KiB blocks and only the lines being shown are decoded; reading 100 lines of a 256 MB file peaks at about 103 MB instead of 775 MB. Output (line numbers, total line count, truncation notes) is byte-for-byte the same.
|
|
46
|
+
- **Session list and resume no longer read whole files**: session files are read line by line in chunks. `ama sessions list` (and the resume picker and ACP `session/list`, which share it) only parses each file's header, first entry, renames and first prompt — 4 × 55 MB sessions: about 540 → 88 MB; `--resume` parses line by line without a whole-file string and split array — 55 MB session: about 300 → 218 MB. The listed fields are unchanged.
|
|
47
|
+
- **Request bodies with images no longer go through one giant string**: when a request carries strings of 64 KiB or more (image base64), the body is assembled piece by piece and streamed with an exact `content-length` (never chunked); small requests are sent exactly as before. The bytes on the wire are identical (byte-for-byte tests against `JSON.stringify`), so prompt caching is unaffected. A local 300-step run with 15 image reads peaks at about 0.7 GB instead of 1.03 GB.
|
|
48
|
+
- **Images are deduplicated by content**: an image read several times by `read`, attached with `--image` / `@path` and then read again, or repeated in a resumed session keeps a single base64 copy in memory. With these changes a TUI session with three 2.4 MB images peaked at 218 MB instead of 276 MB on a real model.
|
|
49
|
+
- **ACP: closed sessions are freed**: a closed session stayed in memory with its transcript and images — the cancel listener of each `session/prompt` was never removed, a session opened next to it kept a reference to the session that was in the foreground at the time, and the process-wide cache-reporting table kept each endpoint's last request whole. All three are fixed; after 8 sessions × 4 rounds are closed, heap and external memory return to about 16 MB each. The session created at process start (claimed by the first `session/new`) is still held until exit.
|
|
50
|
+
- **ACP: `session/prompt` during a background subagent's notification turn** (#139): the prompt failed with "a run is in progress" (-32603) because the notification turn does not go through the ACP prompt queue. It now waits for that turn to end and then starts; `session/cancel` while it waits answers `cancelled` and the prompt is not sent later.
|
|
51
|
+
- **RPC `compact_events`**: a client that declares it with `set_client_capabilities` no longer gets the body of tool results (and of user messages with images) repeated in `turn_end`, `message_start` and `entry_appended` — marked `contentOmitted: true`, the body still comes in `message_end` and `tool_execution_end`. In a 100-step fake run stdout drops from 91.6 MB to 37.0 MB. `hello.capabilities` now lists it; without the declaration, and in `stream-json`, events are unchanged.
|
|
52
|
+
- **The global `ama` command runs the single-file bundle**: `bin.ama` now points to `dist/bundle/ama.cjs` instead of the ESM entry, so an npm-installed `ama --version` starts in about 0.10 s instead of 0.18 s with about 25 MB less peak RSS; library imports (`@armadra/agent`, `/host`, `/rpc`, `/tui`, `/acp`) are unchanged. After `pnpm link`, run `pnpm build` so the bundle exists.
|
|
53
|
+
- **Fewer retained sub-agent sessions**: finished sub-agent sessions kept in memory for `taskId` follow-ups drop from 16 to 4 (least recently used are released; the JSONL stays and a released task reopens from it when continued). Configure with `subagents.retainSessions` (user-level, 0 releases on finish).
|
|
54
|
+
- **Codemode heap limit**: the script subprocess starts with `--max-old-space-size=256`; a script that exceeds it ends with the script error `Script exceeded the codemode memory limit (256 MB)` instead of growing until the timeout. Configure with `codemode.maxHeapMb` (user-level, 0 disables).
|
|
55
|
+
|
|
8
56
|
## 0.7.3 (2026-10-09)
|
|
9
57
|
|
|
10
58
|
- **`/context` before the first request**: it showed `0 / <window>` and left the system prompt and tool declarations out while the status bar already showed the baseline; it now estimates both from what the first request will send (shared with the status-bar baseline), and "Used" always matches the status bar and `getStats()`.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,54 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.7.5(2026-10-10)
|
|
8
|
+
|
|
9
|
+
### 内存占用
|
|
10
|
+
|
|
11
|
+
- **`read` 读大文件不再阻塞事件循环**:超过 1 MiB 的文本走的字节窗口改为异步按 64 KiB 分块读,每块一次 `await`,扫描 256 MB 文件期间 TUI / RPC / ACP 仍能响应(事件循环最长停顿约 170–320 ms → 20 ms 以内)。输出逐字节不变(同步与异步共用同一个行解析器),峰值内存不变(仍只有一块缓冲)。
|
|
12
|
+
- **带图请求体不再复制每张图**:图片数据从转录直接写入请求体,不再每个请求先拼出一份新的 data URL 字符串;转义检查每张图只做一次;请求体按每块最多 256 Ki 码元编码,不再一次编码一整张图。mock 300 步带 15 张图:峰值 RSS 约 630–690 → 390–400 MB,heapUsed 峰值约 270 → 85 MB。请求字节不变;SDK 的 `onPayload` 回调里大图变为带 `toJSON` 的对象(`JSON.stringify` 结果相同)。
|
|
13
|
+
- **降级后释放旧图**:活动分支上的消息被 `context_edit`(如 `image_budget`)改写后,图片 base64 不再常驻内存,只记该行在会话文件中的位置,需要时按位置读回:`get_entries` 与 fork 返回原文,换叶子回到编辑之前(`/tree`、rewind)时恢复图片。续会话读文件时随读随剥,这些旧图不再计入峰值(55 MB、含 10 张被降级的 3 MB 图的会话:峰值 RSS 约 254 → 210 MB)。JSONL 格式不变;`entries()` / `branch()` / SDK `session.entries` 里这类图片块的 `data` 为空串。
|
|
14
|
+
- **ACP 关掉的启动会话也能回收**:`Runtime.session` 与 `Runtime.sessionManager` 改为跟随当前会话(原先固定指向启动时建的会话),`session/close` 之后它的转录与图片和其它会话一样被回收(8 会话 × 4 轮全部关闭后 external 16.3 → 7.0 MB)。SDK 代码在切换会话后读 `runtime.session` 得到的是新会话。
|
|
15
|
+
- **ACP `session/prompt` 的 usage 只算本回合**:等过后台子 Agent 通知回合的提示,`usage` 不再计入那一回合的 token。
|
|
16
|
+
- **通知回合在跑时 `session/close`**:原先以 -32603 失败且会话留在内存;现在先停在跑的后台子 Agent、中断在跑的回合(含通知回合)、已排着的通知不再开回合,然后释放会话,之后不再有 `session/update`。stdin 关闭时同样这样收尾。
|
|
17
|
+
|
|
18
|
+
## 0.7.4(2026-10-10)
|
|
19
|
+
|
|
20
|
+
- **并行 worktree 隔离**:两个隔离任务同时开始时,可能报 `无法读取 .git/worktrees/<id>/commondir`——`git worktree add` 读到了另一个任务建了一半的管理目录。现在同一仓库的 worktree 增删与删分支按顺序执行,另一个 ama 进程造成的同类瞬时错误短暂重试。
|
|
21
|
+
|
|
22
|
+
### 模型调用效率
|
|
23
|
+
|
|
24
|
+
少按全价重读提示前缀、少一些失败请求(docs/model-efficiency-plan.md;实测见 docs/benchmarks/efficiency-2026-10.md)。按子 Agent、提示前缀与压缩、请求层、目录与工具的顺序排列。
|
|
25
|
+
|
|
26
|
+
- **fork 式子 Agent**(`task.context: "fork"`,或类型定义里写 `context: fork`;缺省仍是 `fresh`):子会话继承父会话到这次 `task` 调用之前的对话,系统提示与工具表与父相同,首个请求直接复用父的缓存前缀(中转实测:Kimi 命中 97.5%,DeepSeek 与父自己的下一回合相同)。类型限制的工具改为在执行层拒绝,不再改工具表。指定了不同的模型或思考级别、父还没发过请求、或父上下文超过可用窗口一半时回落为 `fresh`(记日志,`details.context` 与 `TaskInfo.context` 标实际模式)。轮数用尽的收尾一轮不再发 `toolChoice: "none"`(它会断开缓存前缀),只靠报告提示要求不调用工具。
|
|
27
|
+
- **提示开头在会话内只写一次**:压缩不再把会话中途的 system 补丁折回开头——检查点只重放对话开始前发过的内容,之后的补丁以 `<system-reminder>` 跟在摘要后面,压缩后首个请求的 system + tools 与压缩前逐字节相同。会话中途移除工具时工具表保留其声明、尾部提醒「已不可用」,调用一律以 `Tool "X" is not available in this session.` 拒绝(未知工具也改用这句,原为 `Tool X not found`);再加回只提醒「又可用」。提醒的收尾句同时说明工具可用性。
|
|
28
|
+
- **压缩改为续写摘要而不是先裁剪**:裁掉工具结果后仍超预算、且缓存未冷时,不再裁剪,直接续写上一次请求的缓存前缀生成摘要(裁剪会断开这段前缀、变成一次全价的独立摘要请求);缓存已冷时照旧先裁,也不再尝试续写。开着思考时,续写的输出上限 = 摘要上限 + 思考预算。
|
|
29
|
+
- **软窗口与按节指纹**:`compaction.contextBudget` 生效——档一裁剪、档二摘要、熔断与 `context_pressure` 都按 min(模型窗口, 它) 计算。`cache_miss.detail` 写出变化的 system 节(`system:hooks,memory`),`/cache fingerprint` 逐节列出哈希。`openai-responses` 上的保温请求 `maxTokens` 用协议下限 16 而不是 1。
|
|
30
|
+
- **Anthropic 第 4 个缓存断点**:除最后一条 user、system 末与最后一个工具外,倒数第二条 user(上一次请求的写入点)也打断点,一个回合里并行工具结果很多、超出回看窗口时不再整段重写。`maxCacheBreakpoints` 不足 4 时先舍去最后一个工具。
|
|
31
|
+
- **重试遵守 Retry-After,限流单独计**:退避取 `max(指数退避, Retry-After)`、上限 `retry.maxDelayMs`,再加 ±20% 抖动;429 / 529 / rate limit 比 `retry.maxRetries` 多重试 2 次(`auto_retry_start.maxAttempts` 随之)。5xx 只认文案开头的状态码或 `status` / `HTTP` 之后的,错误文案里的「500 tokens」不再当作服务端错误。配了 `fallbackModel` 时,overloaded 先快速重试一次(1 s)再切模型。
|
|
32
|
+
- **max_tokens 不再让首个请求失败**:上下文窗口已知时把 `max_tokens` 收紧到窗口剩余量以内(Anthropic 预算型思考除外);端点以 400 说出上限(`Range of max_tokens should be [1, N]` 等)时以该上限重发一次,同一进程里之后的请求直接用它。Anthropic 的 `input length and max_tokens exceed context limit` 按剩余量重发,不足 1024 token 时按上下文溢出处理。
|
|
33
|
+
- **工具调用参数逐字节回放**:Completions 与 Responses 请求把模型输出的参数串原样发回,不再重新序列化,空格差异不再打断前缀缓存;旧会话回落为序列化。
|
|
34
|
+
- **流中空闲超时单独设置**:`request.idleTimeoutMs`(300 s)现在只管等响应头;流开始后两块数据之间改由新的 `request.streamIdleTimeoutMs`(缺省 180 s,`AMA_STREAM_IDLE_TIMEOUT_MS`,0 关闭)管。之前 `idleTimeoutMs` 两段都管,如果你为慢端点调大或关掉了它,请同时设置 `streamIdleTimeoutMs`。
|
|
35
|
+
- **中转模型继承官方目录**:中转 / 自定义模型的 id 唯一命中内置目录条目时(小写、去一层厂商前缀与 `:latest`,也认目录的 `aliases`),继承 `reasoning`、`input`、`thinkingLevelMap`、`promptCache.minTokens` 与 `compat.requiresReasoningContentOnAssistantMessages`,models.dev 改为匹配该条目的快照;不继承价格、TTL 与 `thinkingFormat`。带思考档后缀的 id(`gemini-3.8-flash-low`)去掉后缀再匹配,继承图片与窗口、不打开思考——中转上的 Gemini Flash 不再因「不支持图片」被拒。`models[].catalog: false` 关闭,`"provider/id"` 显式指定;`ama models list` 标「目录(按 id 匹配)」并列出条目。`deepseek-v4-flash` 是 `deepseek-flash` 的别名。
|
|
36
|
+
- **工具结果一次截到位**:`read`、`grep`、`bash` 按会话的 `tools.maxToolResultChars`(不超过 50 KB)自己截断,说明里写实际上限与续读位置,不再被会话层二次截中段;描述不再写死大小(前缀更短)。`glob` 缺省最多返回 200 个文件(原 1000)。
|
|
37
|
+
- **auto 模式用小模型分类**:没配 `permission.autoModel` 时,分类器用会话供应商目录里的小模型(目录的 `small`:deepseek-flash、claude-haiku-4-5、gpt-6-luna、gemini-3.5-flash-lite、kimi-k2.6 等),找得到且有 key 才用,否则用会话模型。
|
|
38
|
+
- **兼容性**:以上只加可选字段——工具调用块的 `rawArguments`、失败助手消息的 `retryAfterMs`(会话文件与 RPC),`ama.task` 的 data 与 `TaskInfo` 的 `context`,`cache_miss.detail` 的 `system:<节名>` 取值,内置模型目录的 `aliases` / `small`,以及配置键 `compaction.contextBudget`、`request.streamIdleTimeoutMs`、`models[].catalog`。会话格式与 RPC / ACP 协议版本不变。
|
|
39
|
+
|
|
40
|
+
### 内存占用
|
|
41
|
+
|
|
42
|
+
大输入与长会话的峰值降下来,ACP 关掉的会话能释放(docs/memory-plan.md;测量报告见 docs/research/memory-2026-10.md,前后实测见 docs/benchmarks/memory-2026-10.md)。按大文件与会话、请求与图片、协议模式、分发与上限分组。会话文件、请求字节与工具输出都不变。
|
|
43
|
+
|
|
44
|
+
- **`read` 读大文件不再整文件进内存**:超过 1 MiB 的文本按 64 KiB 块扫描,只解码要显示的行;读 256 MB 文件的 100 行峰值约 103 MB(原来 775 MB)。输出(行号、总行数、截断说明)与之前逐字节相同。
|
|
45
|
+
- **会话列表与恢复不再整读文件**:会话文件按块逐行读取。`ama sessions list`(恢复选择器与 ACP `session/list` 走同一条路径)只解析每个文件的头、首条条目、改名与首条提示——4 个 55 MB 会话:约 540 → 88 MB;`--resume` 逐行解析,不再生成整份字符串与 split 数组——55 MB 会话:约 300 → 218 MB。列表各字段不变。
|
|
46
|
+
- **带图请求体不再拼成一整个大字符串**:请求里有 64 KiB 以上的字符串(图片 base64)时按片段组装,以准确的 `content-length` 流式发出(不用 chunked);小请求与以前完全一样。线上字节不变(逐字节比对 `JSON.stringify` 的测试守护),提示缓存不受影响。本地 300 步、读图 15 次的场景峰值从约 1.03 GB 降到约 0.7 GB。
|
|
47
|
+
- **图片按内容去重**:`read` 反复读同一张图、`--image` / `@路径` 附图后又读同一张、恢复的会话里重复出现的图片,内存里只留一份 base64。连同上一条,真实模型的 TUI 会话(3 张 2.4 MB 图)峰值从 276 MB 降到 218 MB。
|
|
48
|
+
- **ACP:关闭的会话可被回收**:关掉的会话连同转录与图片一直留在内存里——每次 `session/prompt` 的取消监听没有摘掉,新开的兄弟会话会引用开它时的前台会话,进程级的缓存上报表还整条保留每个端点的上一条请求。三处都已修正;8 个会话 × 4 轮全部关闭后,堆与堆外内存各回到约 16 MB。进程启动时建的那个会话(被第一个 `session/new` 认领)仍由进程持有到退出。
|
|
49
|
+
- **ACP:后台子 Agent 通知回合进行中的 `session/prompt`**(#139):通知回合不经 ACP 的提示队列,此时发提示会报「a run is in progress」(-32603)。现在等通知回合结束再开始;等待中 `session/cancel` 回 `cancelled`,这条提示之后也不会再发出。
|
|
50
|
+
- **RPC `compact_events`**:客户端用 `set_client_capabilities` 声明后,`turn_end`、`message_start`、`entry_appended` 不再重复携带工具结果(以及带图用户消息)的正文,改标 `contentOmitted: true`,正文仍在 `message_end` 与 `tool_execution_end` 里。fake 100 步的 stdout 由 91.6 MB 降到 37.0 MB。`hello.capabilities` 列出这一项;不声明时与 `stream-json` 的事件不变。
|
|
51
|
+
- **全局 `ama` 命令走单文件 bundle**:`bin.ama` 从 ESM 入口改为 `dist/bundle/ama.cjs`,npm 安装后的 `ama --version` 启动从约 0.18 s 降到约 0.10 s,峰值 RSS 少约 25 MB;库导入(`@armadra/agent`、`/host`、`/rpc`、`/tui`、`/acp`)不变。`pnpm link` 之后先 `pnpm build` 生成 bundle。
|
|
52
|
+
- **子 Agent 会话少保留**:为 `taskId` 续聊留在内存里的已结束子会话从 16 个降到 4 个(最久未用的先释放;JSONL 一直在,被释放的任务续聊时从它重开)。用 `subagents.retainSessions` 调整(只认用户级,0 = 结束即释放)。
|
|
53
|
+
- **codemode 堆上限**:脚本子进程以 `--max-old-space-size=256` 启动,超出时以脚本错误 `Script exceeded the codemode memory limit (256 MB)` 结束,不再一路涨到超时。用 `codemode.maxHeapMb` 调整(只认用户级,0 不设上限)。
|
|
54
|
+
|
|
7
55
|
## 0.7.3(2026-10-09)
|
|
8
56
|
|
|
9
57
|
- **首次请求前的 `/context`**:状态栏已显示启动基线时,它仍是 `0 / <窗口>`、不计系统提示与工具声明;现在按首次请求将要发送的内容估算这两类(与状态栏基线共用同一份装配),「已用」始终与状态栏和 `getStats()` 一致。
|
package/dist/agent/retry.d.ts
CHANGED
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
* 会话层重试(设计 §3.6「重试」「溢出」)。[B2]
|
|
3
3
|
*
|
|
4
4
|
* 判定顺序:上下文溢出(不重试,走压缩)→ 不可重试(配额 / 计费 / key / 401 / 403,快速失败)→
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* 限流(429、529、rate limit;[ME-C] 上限多 2 次)→ 可重试(5xx、overloaded、网络错误、断流、
|
|
6
|
+
* 空闲超时)→ 其它(不重试)。5xx 只认文案开头的状态码或 `status` / `HTTP` 后的(「500 tokens」不算)。
|
|
7
|
+
* 延迟 `min(maxDelayMs, max(baseDelayMs × 2^(attempt−1), Retry-After)) × U(0.8, 1.2)`(D8);
|
|
8
|
+
* `sleep` 可被 abort 打断。
|
|
7
9
|
* 协议层自身不重试。溢出文案识别只有一份:ai/overflow.ts 的 `isOverflowErrorText`(缺省),可注入替换。
|
|
8
10
|
*/
|
|
9
11
|
import type { AssistantMessage } from "../ai/types.js";
|
|
10
12
|
import type { RetrySettings } from "./types.js";
|
|
11
13
|
export declare const DEFAULT_RETRY_SETTINGS: RetrySettings;
|
|
12
|
-
export type FailureKind = "overflow" | "fatal" | "retryable" | "other";
|
|
14
|
+
export type FailureKind = "overflow" | "fatal" | "rate_limited" | "retryable" | "other";
|
|
13
15
|
export interface ClassifyOptions {
|
|
14
16
|
isContextOverflow?: (errorMessage: string) => boolean;
|
|
15
17
|
}
|
|
16
18
|
/** 对 `stopReason: "error"` 的助手消息分类;`length` 且无工具调用也算 overflow。 */
|
|
17
19
|
export declare function classifyFailure(message: AssistantMessage, options?: ClassifyOptions): FailureKind;
|
|
18
20
|
export declare function resolveRetrySettings(partial?: Partial<RetrySettings>): RetrySettings;
|
|
19
|
-
/**
|
|
20
|
-
export declare function
|
|
21
|
+
/** 会话层要重试的失败种类。 */
|
|
22
|
+
export declare function isRetryableKind(kind: FailureKind): boolean;
|
|
23
|
+
/** 该种类的重试上限:限流多给 2 次(服务端多半在 Retry-After 后就恢复)。 */
|
|
24
|
+
export declare function maxRetriesFor(kind: FailureKind, settings: RetrySettings): number;
|
|
25
|
+
/**
|
|
26
|
+
* 第 attempt 次重试(1 起)前的等待:指数退避与服务端 `Retry-After` 取大、截到 `maxDelayMs`,再乘
|
|
27
|
+
* U(0.8, 1.2) 抖动(并发会话不在同一时刻一起重试);取整到毫秒。
|
|
28
|
+
*/
|
|
29
|
+
export declare function retryDelayMs(attempt: number, settings: RetrySettings, retryAfterMs?: number, random?: () => number): number;
|
|
21
30
|
/** 可被 abort 打断的等待;被打断时抛 AmaError{code:"aborted"}。 */
|
|
22
31
|
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
package/dist/agent/retry.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* 会话层重试(设计 §3.6「重试」「溢出」)。[B2]
|
|
3
3
|
*
|
|
4
4
|
* 判定顺序:上下文溢出(不重试,走压缩)→ 不可重试(配额 / 计费 / key / 401 / 403,快速失败)→
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* 限流(429、529、rate limit;[ME-C] 上限多 2 次)→ 可重试(5xx、overloaded、网络错误、断流、
|
|
6
|
+
* 空闲超时)→ 其它(不重试)。5xx 只认文案开头的状态码或 `status` / `HTTP` 后的(「500 tokens」不算)。
|
|
7
|
+
* 延迟 `min(maxDelayMs, max(baseDelayMs × 2^(attempt−1), Retry-After)) × U(0.8, 1.2)`(D8);
|
|
8
|
+
* `sleep` 可被 abort 打断。
|
|
7
9
|
* 协议层自身不重试。溢出文案识别只有一份:ai/overflow.ts 的 `isOverflowErrorText`(缺省),可注入替换。
|
|
8
10
|
*/
|
|
9
11
|
import { isOverflowErrorText } from "../ai/overflow.js";
|
|
@@ -27,11 +29,16 @@ const FATAL_PATTERNS = [
|
|
|
27
29
|
/permission denied/i,
|
|
28
30
|
/authentication/i,
|
|
29
31
|
];
|
|
30
|
-
|
|
32
|
+
/** 限流:先于 RETRYABLE_PATTERNS 判(529 overloaded 也在这里)。 */
|
|
33
|
+
const RATE_LIMIT_PATTERNS = [
|
|
31
34
|
/\b429\b/,
|
|
35
|
+
/\b529\b/,
|
|
32
36
|
/rate[_ ]?limit/i,
|
|
33
37
|
/too many requests/i,
|
|
34
|
-
|
|
38
|
+
];
|
|
39
|
+
const RETRYABLE_PATTERNS = [
|
|
40
|
+
/^\s*5\d\d\b/,
|
|
41
|
+
/\b(?:status|HTTP)\s*[:=]?\s*5\d\d\b/i,
|
|
35
42
|
/overloaded/i,
|
|
36
43
|
/server error/i,
|
|
37
44
|
/service unavailable/i,
|
|
@@ -61,6 +68,8 @@ export function classifyFailure(message, options = {}) {
|
|
|
61
68
|
return "overflow";
|
|
62
69
|
if (FATAL_PATTERNS.some((pattern) => pattern.test(text)))
|
|
63
70
|
return "fatal";
|
|
71
|
+
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(text)))
|
|
72
|
+
return "rate_limited";
|
|
64
73
|
if (RETRYABLE_PATTERNS.some((pattern) => pattern.test(text)))
|
|
65
74
|
return "retryable";
|
|
66
75
|
return "other";
|
|
@@ -68,10 +77,23 @@ export function classifyFailure(message, options = {}) {
|
|
|
68
77
|
export function resolveRetrySettings(partial = {}) {
|
|
69
78
|
return { ...DEFAULT_RETRY_SETTINGS, ...partial };
|
|
70
79
|
}
|
|
71
|
-
/**
|
|
72
|
-
export function
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
/** 会话层要重试的失败种类。 */
|
|
81
|
+
export function isRetryableKind(kind) {
|
|
82
|
+
return kind === "retryable" || kind === "rate_limited";
|
|
83
|
+
}
|
|
84
|
+
/** 该种类的重试上限:限流多给 2 次(服务端多半在 Retry-After 后就恢复)。 */
|
|
85
|
+
export function maxRetriesFor(kind, settings) {
|
|
86
|
+
return kind === "rate_limited" ? settings.maxRetries + 2 : settings.maxRetries;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 第 attempt 次重试(1 起)前的等待:指数退避与服务端 `Retry-After` 取大、截到 `maxDelayMs`,再乘
|
|
90
|
+
* U(0.8, 1.2) 抖动(并发会话不在同一时刻一起重试);取整到毫秒。
|
|
91
|
+
*/
|
|
92
|
+
export function retryDelayMs(attempt, settings, retryAfterMs, random = Math.random) {
|
|
93
|
+
const backoff = settings.baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
94
|
+
const wanted = Math.max(backoff, retryAfterMs ?? 0);
|
|
95
|
+
const capped = Math.min(wanted, settings.maxDelayMs);
|
|
96
|
+
return Math.round(capped * (0.8 + 0.4 * random()));
|
|
75
97
|
}
|
|
76
98
|
/** 可被 abort 打断的等待;被打断时抛 AmaError{code:"aborted"}。 */
|
|
77
99
|
export function sleep(ms, signal) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prompt_cache_key` 的取值(docs/model-efficiency-plan.md §1.8,从 session-cache.ts 搬出):
|
|
3
|
+
* fork 出的会话沿用 fork 链上的根会话 id(只是路由提示);task 子会话与非 fork 会话用自己的 id。
|
|
4
|
+
* [ME-A] fork 式子会话(首条 `ama.task{context:"fork"}`)继承父的前缀,不当作 task 子会话:沿用父链根 id。
|
|
5
|
+
*/
|
|
6
|
+
import type { SessionManager } from "../session/manager.js";
|
|
7
|
+
/** fork 链上的根会话 id(task 子会话与非 fork 会话用自己的 id)。 */
|
|
8
|
+
export declare function cacheKeyOf(manager: SessionManager): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prompt_cache_key` 的取值(docs/model-efficiency-plan.md §1.8,从 session-cache.ts 搬出):
|
|
3
|
+
* fork 出的会话沿用 fork 链上的根会话 id(只是路由提示);task 子会话与非 fork 会话用自己的 id。
|
|
4
|
+
* [ME-A] fork 式子会话(首条 `ama.task{context:"fork"}`)继承父的前缀,不当作 task 子会话:沿用父链根 id。
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
const TASK_CUSTOM_TYPE = "ama.task";
|
|
8
|
+
/** 首条是不是 fresh 式 task 子会话的标记(fork 式子会话不算)。 */
|
|
9
|
+
function isTaskHead(entry) {
|
|
10
|
+
if (entry?.customType !== TASK_CUSTOM_TYPE)
|
|
11
|
+
return false;
|
|
12
|
+
const data = entry.data;
|
|
13
|
+
return data?.context !== "fork";
|
|
14
|
+
}
|
|
15
|
+
function readHead(file) {
|
|
16
|
+
try {
|
|
17
|
+
const [header, first] = readFileSync(file, "utf8").split("\n", 2);
|
|
18
|
+
const parsed = JSON.parse(header ?? "{}");
|
|
19
|
+
const entry = first ? JSON.parse(first) : undefined;
|
|
20
|
+
return { ...parsed, task: isTaskHead(entry) };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { task: false };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** fork 链上的根会话 id(task 子会话与非 fork 会话用自己的 id)。 */
|
|
27
|
+
export function cacheKeyOf(manager) {
|
|
28
|
+
const header = manager.header();
|
|
29
|
+
const entries = manager.entries();
|
|
30
|
+
const head = entries[0];
|
|
31
|
+
const forkTask = head?.type === "custom" && head.customType === TASK_CUSTOM_TYPE && !isTaskHead(head);
|
|
32
|
+
const isTask = !forkTask &&
|
|
33
|
+
entries.some((entry) => entry.type === "custom" && entry.customType === TASK_CUSTOM_TYPE);
|
|
34
|
+
if (header.parentSession === undefined || isTask)
|
|
35
|
+
return manager.id;
|
|
36
|
+
let id = manager.id;
|
|
37
|
+
let file = header.parentSession;
|
|
38
|
+
for (let depth = 0; depth < 8 && file !== undefined; depth++) {
|
|
39
|
+
const next = readHead(file);
|
|
40
|
+
if (next.id === undefined || next.task)
|
|
41
|
+
break;
|
|
42
|
+
id = next.id;
|
|
43
|
+
file = next.parentSession;
|
|
44
|
+
}
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
@@ -12,7 +12,6 @@ import type { RequestRecord, WarmingDecisionHandler, WarmingMode } from "../ai/c
|
|
|
12
12
|
import { type WarmerTimers } from "../ai/cache/warmer.js";
|
|
13
13
|
import type { CacheRetention, Model } from "../ai/types.js";
|
|
14
14
|
import { type SummaryContinuation } from "../compaction/summarize-tier.js";
|
|
15
|
-
import type { SessionManager } from "../session/manager.js";
|
|
16
15
|
import type { StreamFn } from "./loop.js";
|
|
17
16
|
import type { SessionCore } from "./session-core.js";
|
|
18
17
|
import type { CacheSettings, SessionCacheStats } from "./types.js";
|
|
@@ -29,8 +28,6 @@ export interface SessionCacheDeps {
|
|
|
29
28
|
}
|
|
30
29
|
/** 目录 TTL(毫秒):long 保留且有 long 档时用 long;无承诺 → undefined。 */
|
|
31
30
|
export declare function cacheTtlMs(model: Model, retention?: CacheRetention): number | undefined;
|
|
32
|
-
/** fork 链上的根会话 id(task 子会话与非 fork 会话用自己的 id)。 */
|
|
33
|
-
export declare function cacheKeyOf(manager: SessionManager): string;
|
|
34
31
|
export declare class SessionCacheController {
|
|
35
32
|
private readonly core;
|
|
36
33
|
private settings;
|
|
@@ -91,6 +88,8 @@ export declare class SessionCacheController {
|
|
|
91
88
|
/**
|
|
92
89
|
* 摘要续写的前缀(第三波 §1.8):当前转录按回合同样的方式转换,且上一次 turn 请求的消息
|
|
93
90
|
* 逐条是它的前缀(缓存必然命中)、加上摘要输出放得进窗口;否则 undefined(走独立请求)。
|
|
91
|
+
* [ME-B] D6:缓存已冷(`isCold()`)不续写——前缀要全价重写,不如裁剪后的独立请求小;思考开启
|
|
92
|
+
* 且不是预算型协议时,输出上限 = 摘要上限 + 思考预算(不超过模型上限),免得思考吃掉摘要。
|
|
94
93
|
*/
|
|
95
94
|
summaryContinuation(): SummaryContinuation | undefined;
|
|
96
95
|
dispose(): void;
|
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
* fork 出的会话沿用根会话 id 作 `prompt_cache_key`(只是路由提示);task 子会话不沿用。
|
|
9
9
|
*/
|
|
10
10
|
import { modelRefOf } from "../ai/providers/channels.js";
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
11
|
import { evaluateWarm } from "../ai/cache/economics.js";
|
|
13
12
|
import { fingerprintContext } from "../ai/cache/fingerprint.js";
|
|
14
13
|
import { DEFAULT_MIN_CACHE_TOKENS, IMPLICIT_CACHE_TTL_MS, detectMiss } from "../ai/cache/miss.js";
|
|
15
14
|
import { endpointKey, sharedCacheReporting, } from "../ai/cache/reporting.js";
|
|
16
|
-
import { CacheWarmer, replayBlocker } from "../ai/cache/warmer.js";
|
|
15
|
+
import { CacheWarmer, replayBlocker, warmReplayMaxTokens, } from "../ai/cache/warmer.js";
|
|
16
|
+
import { thinkingBudget } from "../ai/thinking.js";
|
|
17
17
|
import { estimateTextTokens } from "../compaction/estimate.js";
|
|
18
18
|
import { SUMMARY_MAX_TOKENS } from "../compaction/summarize-tier.js";
|
|
19
|
+
import { cacheKeyOf } from "./session-cache-key.js";
|
|
20
|
+
import { effectiveWindow } from "./session-compaction.js";
|
|
19
21
|
import { convertToLlm } from "./transform.js";
|
|
20
22
|
export const DEFAULT_CACHE_SETTINGS = Object.freeze({
|
|
21
23
|
warming: "streaming",
|
|
@@ -26,7 +28,6 @@ export const DEFAULT_CACHE_SETTINGS = Object.freeze({
|
|
|
26
28
|
});
|
|
27
29
|
export const PRESSURE_THRESHOLDS = [70, 90];
|
|
28
30
|
const GROWTH_SAMPLES = 5;
|
|
29
|
-
const TASK_CUSTOM_TYPE = "ama.task";
|
|
30
31
|
export function resolveCacheSettings(partial = {}) {
|
|
31
32
|
const settings = { ...DEFAULT_CACHE_SETTINGS };
|
|
32
33
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -41,36 +42,6 @@ export function cacheTtlMs(model, retention) {
|
|
|
41
42
|
const seconds = retention === "long" && cache?.long !== undefined ? cache.long : cache?.short;
|
|
42
43
|
return seconds === undefined ? undefined : seconds * 1000;
|
|
43
44
|
}
|
|
44
|
-
function readHead(file) {
|
|
45
|
-
try {
|
|
46
|
-
const [header, first] = readFileSync(file, "utf8").split("\n", 2);
|
|
47
|
-
const parsed = JSON.parse(header ?? "{}");
|
|
48
|
-
const entry = first ? JSON.parse(first) : undefined;
|
|
49
|
-
return { ...parsed, task: entry?.customType === TASK_CUSTOM_TYPE };
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return { task: false };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/** fork 链上的根会话 id(task 子会话与非 fork 会话用自己的 id)。 */
|
|
56
|
-
export function cacheKeyOf(manager) {
|
|
57
|
-
const header = manager.header();
|
|
58
|
-
const isTask = manager
|
|
59
|
-
.entries()
|
|
60
|
-
.some((entry) => entry.type === "custom" && entry.customType === TASK_CUSTOM_TYPE);
|
|
61
|
-
if (header.parentSession === undefined || isTask)
|
|
62
|
-
return manager.id;
|
|
63
|
-
let id = manager.id;
|
|
64
|
-
let file = header.parentSession;
|
|
65
|
-
for (let depth = 0; depth < 8 && file !== undefined; depth++) {
|
|
66
|
-
const head = readHead(file);
|
|
67
|
-
if (head.id === undefined || head.task)
|
|
68
|
-
break;
|
|
69
|
-
id = head.id;
|
|
70
|
-
file = head.parentSession;
|
|
71
|
-
}
|
|
72
|
-
return id;
|
|
73
|
-
}
|
|
74
45
|
export class SessionCacheController {
|
|
75
46
|
core;
|
|
76
47
|
settings;
|
|
@@ -318,11 +289,13 @@ export class SessionCacheController {
|
|
|
318
289
|
/**
|
|
319
290
|
* 摘要续写的前缀(第三波 §1.8):当前转录按回合同样的方式转换,且上一次 turn 请求的消息
|
|
320
291
|
* 逐条是它的前缀(缓存必然命中)、加上摘要输出放得进窗口;否则 undefined(走独立请求)。
|
|
292
|
+
* [ME-B] D6:缓存已冷(`isCold()`)不续写——前缀要全价重写,不如裁剪后的独立请求小;思考开启
|
|
293
|
+
* 且不是预算型协议时,输出上限 = 摘要上限 + 思考预算(不超过模型上限),免得思考吃掉摘要。
|
|
321
294
|
*/
|
|
322
295
|
summaryContinuation() {
|
|
323
296
|
const record = this.lastTurnRecord;
|
|
324
297
|
const model = this.core.model();
|
|
325
|
-
if (record === undefined || this.disposed)
|
|
298
|
+
if (record === undefined || this.disposed || this.isCold())
|
|
326
299
|
return undefined;
|
|
327
300
|
if (model.provider !== record.model.provider ||
|
|
328
301
|
model.id !== record.model.id ||
|
|
@@ -351,8 +324,11 @@ export class SessionCacheController {
|
|
|
351
324
|
payloadReplaced: false,
|
|
352
325
|
reporting: "reported",
|
|
353
326
|
});
|
|
327
|
+
const level = rest.thinkingLevel;
|
|
354
328
|
if (budget === "thinking_budget")
|
|
355
329
|
streamOptions.maxTokens = maxTokens ?? model.maxTokens;
|
|
330
|
+
else if (model.reasoning && level !== undefined && level !== "off")
|
|
331
|
+
streamOptions.maxTokens = Math.min(model.maxTokens, SUMMARY_MAX_TOKENS + thinkingBudget(model, level));
|
|
356
332
|
return { prefix: { messages }, streamOptions };
|
|
357
333
|
}
|
|
358
334
|
dispose() {
|
|
@@ -426,7 +402,7 @@ export class SessionCacheController {
|
|
|
426
402
|
return reporting;
|
|
427
403
|
}
|
|
428
404
|
checkPressure(record, model) {
|
|
429
|
-
const window = model.contextWindow;
|
|
405
|
+
const window = effectiveWindow(model.contextWindow, this.core.options.compaction);
|
|
430
406
|
if (window === undefined || window <= 0)
|
|
431
407
|
return;
|
|
432
408
|
const used = record.promptTokens + record.usage.output;
|
|
@@ -489,8 +465,14 @@ export class SessionCacheController {
|
|
|
489
465
|
const inner = this.inner;
|
|
490
466
|
if (inner === undefined)
|
|
491
467
|
throw new Error("stream not wrapped");
|
|
492
|
-
const
|
|
493
|
-
|
|
468
|
+
const model = this.modelOf(record);
|
|
469
|
+
const options = {
|
|
470
|
+
...record.options,
|
|
471
|
+
maxTokens: warmReplayMaxTokens(model.api),
|
|
472
|
+
purpose: "warm",
|
|
473
|
+
signal,
|
|
474
|
+
};
|
|
475
|
+
return inner(model, record.contextRef, options).result();
|
|
494
476
|
}
|
|
495
477
|
onWarmed(record, message, sentAt) {
|
|
496
478
|
const usage = message.usage;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* auto 权限模式的分类请求(§7.4 第 3 层):把 permissions/classifier.ts 的 `complete` 接到会话上。
|
|
3
3
|
*
|
|
4
|
-
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel
|
|
5
|
-
*
|
|
4
|
+
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
|
|
5
|
+
* 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
|
|
6
|
+
* `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
|
|
6
7
|
* - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
|
|
7
8
|
* 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
|
|
8
9
|
* `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* auto 权限模式的分类请求(§7.4 第 3 层):把 permissions/classifier.ts 的 `complete` 接到会话上。
|
|
3
3
|
*
|
|
4
|
-
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel
|
|
5
|
-
*
|
|
4
|
+
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
|
|
5
|
+
* 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
|
|
6
|
+
* `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
|
|
6
7
|
* - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
|
|
7
8
|
* 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
|
|
8
9
|
* `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
|
|
9
10
|
* - 用量记一条 `usage{kind:"permission_classify"}` 条目:计入 `/session` 费用,不进上下文。
|
|
10
11
|
*/
|
|
11
12
|
import { CLASSIFIER_MAX_TOKENS, PermissionClassifier, } from "../permissions/classifier.js";
|
|
13
|
+
import { catalogSmall } from "../ai/providers/catalog.js";
|
|
12
14
|
export const CLASSIFY_USAGE_KIND = "permission_classify";
|
|
13
15
|
function textOf(message) {
|
|
14
16
|
return message.content
|
|
@@ -17,18 +19,42 @@ function textOf(message) {
|
|
|
17
19
|
.join("")
|
|
18
20
|
.trim();
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
/** 会话供应商的目录小模型:能找到且 key 可用(或无需 key)才用。 */
|
|
23
|
+
async function smallModel(core, session) {
|
|
24
|
+
const small = catalogSmall(session.provider);
|
|
25
|
+
if (small === undefined || small === session.id)
|
|
26
|
+
return undefined;
|
|
27
|
+
const lookup = core.options.providers.findModel(`${session.provider}/${small}`);
|
|
28
|
+
if (!lookup.ok)
|
|
29
|
+
return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const key = await core.options.providers.resolveApiKey(lookup.model.provider, lookup.model.channel);
|
|
32
|
+
return key.apiKey !== undefined || !lookup.provider.requiresApiKey ? lookup.model : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function classifierModel(core, state) {
|
|
21
39
|
const ref = core.options.permissionClassifier?.model;
|
|
40
|
+
let model;
|
|
22
41
|
if (ref !== undefined && ref.trim() !== "") {
|
|
23
42
|
const lookup = core.options.providers.findModel(ref);
|
|
24
43
|
if (lookup.ok)
|
|
25
|
-
|
|
26
|
-
if (!warned
|
|
27
|
-
warned
|
|
28
|
-
core.log("warn", `permission.autoModel ${ref} not found;
|
|
44
|
+
model = lookup.model;
|
|
45
|
+
else if (!state.warned) {
|
|
46
|
+
state.warned = true;
|
|
47
|
+
core.log("warn", `permission.autoModel ${ref} not found; ignored`);
|
|
29
48
|
}
|
|
30
49
|
}
|
|
31
|
-
|
|
50
|
+
const session = core.model();
|
|
51
|
+
model ??= (await smallModel(core, session)) ?? session;
|
|
52
|
+
const picked = `${model.provider}/${model.id}`;
|
|
53
|
+
if (state.logged !== picked) {
|
|
54
|
+
state.logged = picked;
|
|
55
|
+
core.log("debug", `permission classifier model: ${picked}`);
|
|
56
|
+
}
|
|
57
|
+
return model;
|
|
32
58
|
}
|
|
33
59
|
/** 最近一条用户消息的文本(分类器输入的摘要来源)。 */
|
|
34
60
|
export function latestUserText(core) {
|
|
@@ -41,10 +67,10 @@ export function latestUserText(core) {
|
|
|
41
67
|
return content.map((block) => (block.type === "text" ? block.text : "")).join(" ");
|
|
42
68
|
}
|
|
43
69
|
export function createSessionClassifier(core) {
|
|
44
|
-
const
|
|
70
|
+
const state = { warned: false };
|
|
45
71
|
const options = core.options.permissionClassifier;
|
|
46
72
|
return new PermissionClassifier(async (prompt, signal) => {
|
|
47
|
-
const model = classifierModel(core,
|
|
73
|
+
const model = await classifierModel(core, state);
|
|
48
74
|
const context = {
|
|
49
75
|
messages: [
|
|
50
76
|
{ role: "system", sections: { preamble: prompt.system }, timestamp: Date.now() },
|
|
@@ -18,17 +18,26 @@
|
|
|
18
18
|
* (只统计,不写分支、不改请求);附档一 / 档二阈值。压缩调度本身仍只看 `estimate()`。
|
|
19
19
|
* [W3-C1b] 阈值 / 手动压缩与分支摘要优先走会话前缀续写(缓存控制器给前缀),溢出恢复不走
|
|
20
20
|
* (前缀本身已超窗口);续写失败回落独立请求并记 warning。
|
|
21
|
+
* [ME-B] D6 档一 / 档二边界:先干跑档一;裁完仍超预算且续写可用(同模型、前缀成立、缓存未冷)→
|
|
22
|
+
* 不裁、直接续写摘要(被裁内容本来就进摘要,续写比全价独立请求便宜一个数量级);裁完够 → 只裁;
|
|
23
|
+
* 缓存已冷 → 照旧先裁,摘要走独立请求。D16 软窗口:`compaction.contextBudget` 设了时档一 / 档二 /
|
|
24
|
+
* 熔断 / context_pressure 的窗口取 min(模型窗口, 它)。
|
|
21
25
|
*/
|
|
22
26
|
import { CompactionBreaker } from "../compaction/breaker.js";
|
|
23
27
|
import { type ContextEstimate } from "../compaction/estimate.js";
|
|
24
28
|
import type { SystemMessage } from "../ai/types.js";
|
|
25
|
-
import { type PrunePolicy } from "../compaction/prune-tier.js";
|
|
29
|
+
import { type PrunePlan, type PrunePolicy } from "../compaction/prune-tier.js";
|
|
26
30
|
import type { BranchSummaryEntry } from "../session/types.js";
|
|
27
31
|
import type { SessionCore } from "./session-core.js";
|
|
28
32
|
import type { CompactionResult, CompactionSettings, SessionContextStats } from "./types.js";
|
|
29
33
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
30
34
|
/** 保留区不超过 (窗口 − 预留) 的 40%,否则小窗口模型上摘要后仍然溢出。 */
|
|
31
35
|
export declare const KEEP_RECENT_MAX_RATIO = 0.4;
|
|
36
|
+
/**
|
|
37
|
+
* [ME-B] D16 软窗口:模型窗口与 `compaction.contextBudget` 取小;窗口未知仍 undefined(不压缩)。
|
|
38
|
+
* `compaction` 是会话选项里展开的 config 段(`contextBudget` 不在 CompactionSettings 类型上)。
|
|
39
|
+
*/
|
|
40
|
+
export declare function effectiveWindow(contextWindow: number | undefined, compaction: object | undefined): number | undefined;
|
|
32
41
|
export declare class CompactionController {
|
|
33
42
|
readonly breaker: CompactionBreaker;
|
|
34
43
|
settings: CompactionSettings;
|
|
@@ -38,6 +47,8 @@ export declare class CompactionController {
|
|
|
38
47
|
/** [W5-H1] config `compaction.prune / pruneExclude`(组装根把整段 config.compaction 展开传入)。 */
|
|
39
48
|
private readonly pruneConfig;
|
|
40
49
|
constructor(core: SessionCore, settings?: Partial<CompactionSettings>);
|
|
50
|
+
/** 档一 / 档二 / 熔断用的窗口(软窗口生效后的值)。 */
|
|
51
|
+
private window;
|
|
41
52
|
get isCompacting(): boolean;
|
|
42
53
|
/** 模型或开关变化后调用。 */
|
|
43
54
|
refresh(): void;
|
|
@@ -55,16 +66,24 @@ export declare class CompactionController {
|
|
|
55
66
|
private prefixTokens;
|
|
56
67
|
private budget;
|
|
57
68
|
private keepRecent;
|
|
58
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 摘要请求的公共选项;`continuation` 时附上会话前缀续写的材料(传入对象 = 调用方已算好的那份,
|
|
71
|
+
* 避免重算)。
|
|
72
|
+
*/
|
|
59
73
|
private summarizer;
|
|
60
74
|
/** 档一参数;无窗口时 undefined。 */
|
|
61
75
|
prunePolicy(): PrunePolicy | undefined;
|
|
76
|
+
/** 档一干跑;`need` = 清到目标还要省多少(undefined = 全部候选)。 */
|
|
77
|
+
prunePlan(policy: PrunePolicy, need: number | undefined): PrunePlan;
|
|
78
|
+
/** 档一落盘:写 context_edit 并重载消息。返回省下的 token 估算(0 = 未裁)。 */
|
|
79
|
+
applyPrune(plan: PrunePlan): number;
|
|
62
80
|
/**
|
|
63
|
-
*
|
|
81
|
+
* 阈值检查(档一 → 档二,[ME-B] D6)。失败只记录,不打断 run。
|
|
82
|
+
* 先干跑档一:裁完仍超预算、熔断允许且续写可用 → 跳过裁剪直接续写摘要;否则照旧先裁。
|
|
64
83
|
*/
|
|
65
|
-
prune(policy: PrunePolicy, need: number | undefined): number;
|
|
66
|
-
/** 阈值检查(档一 → 档二)。失败只记录,不打断 run。 */
|
|
67
84
|
checkThreshold(signal: AbortSignal): Promise<void>;
|
|
85
|
+
/** 熔断是否允许自动摘要(顺带更新「固定前缀超预算」)。 */
|
|
86
|
+
private summaryAllowed;
|
|
68
87
|
/** 回注块(todo、计划、已加载 Skill、最近文件、转录与 outputs 路径;不含文件正文)。 */
|
|
69
88
|
private postCompactBlock;
|
|
70
89
|
/**
|