@armadra/agent 0.7.4 → 0.7.6
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 +32 -0
- package/CHANGELOG.zh-CN.md +32 -0
- package/dist/agent/session-classifier.d.ts +2 -0
- package/dist/agent/session-classifier.js +24 -3
- package/dist/agent/session-extensions.d.ts +1 -0
- package/dist/agent/session-extensions.js +3 -0
- package/dist/agent/session-subagent.js +9 -2
- package/dist/agent/subagent-background.d.ts +5 -1
- package/dist/agent/subagent-background.js +13 -1
- package/dist/agent/subagent-fork.d.ts +10 -5
- package/dist/agent/subagent-fork.js +22 -6
- package/dist/agent/subagent-registry.d.ts +1 -1
- package/dist/agent/subagent-registry.js +1 -1
- package/dist/agents/catalog.d.ts +1 -0
- package/dist/ai/apis/anthropic-request.js +6 -1
- package/dist/ai/apis/cache-params.js +2 -1
- package/dist/ai/apis/google-request.js +2 -1
- package/dist/ai/apis/max-tokens.d.ts +4 -0
- package/dist/ai/apis/max-tokens.js +12 -1
- package/dist/ai/apis/openai-request.js +3 -1
- package/dist/ai/apis/openai-responses-request.js +2 -1
- package/dist/ai/json-body.d.ts +33 -1
- package/dist/ai/json-body.js +96 -18
- package/dist/ai/providers/enrich.d.ts +6 -2
- package/dist/ai/providers/enrich.js +13 -2
- package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
- package/dist/ai/providers/max-tokens-cache.js +95 -0
- package/dist/ai/providers/registry.js +3 -3
- package/dist/bundle/ama.cjs +982 -442
- package/dist/checkpoints/shadow-git.d.ts +5 -1
- package/dist/checkpoints/shadow-git.js +11 -2
- package/dist/cli/bootstrap.js +13 -5
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-store.d.ts +6 -2
- package/dist/cli/compose-store.js +8 -4
- package/dist/cli/compose.js +2 -2
- package/dist/cli/deps.d.ts +1 -1
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema-w5.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +5 -0
- package/dist/i18n/catalog.d.ts +2 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/interactive.d.ts +2 -0
- package/dist/i18n/messages/interactive.js +2 -0
- package/dist/i18n/messages/session.js +2 -2
- package/dist/modes/acp/acp-server.js +17 -15
- package/dist/modes/acp/acp-sessions.d.ts +14 -1
- package/dist/modes/acp/acp-sessions.js +44 -1
- package/dist/modes/interactive/agent-ui.js +7 -0
- package/dist/session/manager.d.ts +18 -0
- package/dist/session/manager.js +75 -14
- package/dist/session/offload.d.ts +59 -0
- package/dist/session/offload.js +218 -0
- package/dist/session/store.d.ts +12 -4
- package/dist/session/store.js +18 -8
- package/dist/tools/background-jobs.d.ts +4 -4
- package/dist/tools/background-jobs.js +4 -4
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +4 -3
- package/dist/tools/read-lines.d.ts +39 -0
- package/dist/tools/read-lines.js +163 -86
- package/dist/tools/read.js +3 -3
- package/dist/tools/task-ctl.js +2 -1
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +27 -1
- package/docs/acp.md +6 -3
- package/docs/agents.md +14 -3
- package/docs/en/acp.md +2 -2
- package/docs/en/permissions.md +1 -1
- package/docs/en/providers.md +4 -4
- package/docs/en/sessions.md +2 -1
- package/docs/permissions.md +1 -1
- package/docs/providers.md +9 -5
- package/docs/session-format.md +11 -0
- package/docs/sessions.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.6 (2026-10-10)
|
|
9
|
+
|
|
10
|
+
### Model efficiency
|
|
11
|
+
|
|
12
|
+
- **The auto-mode classifier picks a small model on relays and custom providers too**: when the session model inherits an official catalog entry by id (e.g. `packy/deepseek-v4-pro`), the classifier uses the same vendor's catalog `small` model if the provider's model table lists it (`packy/deepseek-v4-flash`); only listed models are used, never synthesized ones. `permission.autoModel` still overrides (#153).
|
|
13
|
+
- **Fork sub-agents are steered back to their task when `task` / `task_ctl` is rejected**: the rejection now says the caller is a forked sub-agent that cannot start sub-agents, asks it to do the sub-task itself, and quotes the first line of its `<task>` instructions (fresh sub-agents keep the old text; the tool list and the first request are unchanged). DeepSeek retest: one run made no `task` call, the other made one and then read the file and reported (#191).
|
|
14
|
+
- **Learned `max_tokens` limits persist across processes**: when an endpoint rejects with a 400 stating its output limit, the limit is now also written to `<dataDir>/models/max-tokens-caps.json` and loaded back on startup, so later processes no longer get the first request rejected once per model (packy `kimi-k2.5`: second process 0 rejections, previously 1). Entries expire after 30 days; delete the file to re-probe.
|
|
15
|
+
- **Warning for a wrong `models[].catalog` reference**: `catalog: "provider/id"` that matches no catalog entry now produces a registry warning (startup header count, `ama models list`, `AMA_LOG`) instead of silently inheriting nothing.
|
|
16
|
+
|
|
17
|
+
### Sessions
|
|
18
|
+
|
|
19
|
+
- **Unreadable session images are reported**: when a downgraded image cannot be read back because the session file was changed by another process, the warning now reaches the session log (`ama: [warn] cannot read session entry …` on stderr, filtered by `AMA_LOG`; a notice in the TUI) instead of being dropped. Warnings raised while the file is being opened are buffered and shown once the session is ready (at most 16, the rest counted); the source session of `--fork` reports through the same log.
|
|
20
|
+
- **`subagents.forkMaxContextRatio`**: the share of (window − `compaction.reserveTokens`) above which a fork sub-agent falls back to fresh is now configurable (0.05–0.95, default 0.5, user level only). Built-in types, `general` included, stay fresh by default; docs/agents.md explains the per-turn cost of re-reading the parent context.
|
|
21
|
+
- **Fork sub-agents are told `task` / `task_ctl` are unavailable**: the `<task>` message now lists them and asks not to call them (they stay in the tool list and are still rejected by depth). In two DeepSeek retests the sub-agent still made one rejected `task` call each time.
|
|
22
|
+
- **Background fork tasks report their mode right away**: the `running` result of a background `task` now carries `details.context` (`fork` / `fresh`) once the mode is known; a queued task or one waiting for its worktree still gets it later in `TaskInfo.context`.
|
|
23
|
+
- **Background `bash` job output fits the session result limit in one cut**: `{job, action: "wait" | "output" | "stop"}` now tail-truncates to the same byte limit as foreground `bash` (50 KB or `maxToolResultChars` − 512, whichever is smaller) instead of a fixed 2000 lines / 50 KB that the session then cut again in the middle; the note still gives the shown line count and `Full output: <path>`.
|
|
24
|
+
|
|
25
|
+
### Checkpoints
|
|
26
|
+
|
|
27
|
+
- **A slow first shadow snapshot no longer downgrades the session**: the first snapshot (which builds the index) is no longer timed, and the session downgrades to `tools` only after two snapshots in a row exceed 3 seconds (one slow snapshot followed by a normal one keeps shadow-git). The one-time notice now says so (#194).
|
|
28
|
+
|
|
29
|
+
## 0.7.5 (2026-10-10)
|
|
30
|
+
|
|
31
|
+
### Memory footprint
|
|
32
|
+
|
|
33
|
+
- **`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).
|
|
34
|
+
- **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).
|
|
35
|
+
- **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: ""`.
|
|
36
|
+
- **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.
|
|
37
|
+
- **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`.
|
|
38
|
+
- **`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.
|
|
39
|
+
|
|
8
40
|
## 0.7.4 (2026-10-10)
|
|
9
41
|
|
|
10
42
|
- **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.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.7.6(2026-10-10)
|
|
8
|
+
|
|
9
|
+
### 模型调用效率
|
|
10
|
+
|
|
11
|
+
- **中转 / 自定义供应商的 auto 分类器也用小模型**:会话模型按 id 继承了官方目录条目时(如 `packy/deepseek-v4-pro`),若该供应商模型表里列有同厂商目录的 `small` 模型(`packy/deepseek-v4-flash`)就用它分类;只认模型表里列出的条目,不用合成模型。`permission.autoModel` 仍优先(#153)。
|
|
12
|
+
- **fork 子 Agent 调 `task` / `task_ctl` 被拒后回到子任务**:拒绝文案说明它是 fork 出的子 Agent、不能再派生,要求自己完成子任务并附 `<task>` 指令首行(fresh 子 Agent 文案不变;工具表与首请求不变)。DeepSeek 复测:一次没调 `task`,另一次调了一次被拒后读文件并给出报告(#191)。
|
|
13
|
+
- **学到的 `max_tokens` 上限跨进程保留**:端点以 400 说出输出上限时,上限同时写入 `<dataDir>/models/max-tokens-caps.json`,启动时载回,之后的进程不再每个模型首个请求被拒一次(packy `kimi-k2.5`:第二个进程 0 次 400,原为 1 次)。条目 30 天过期,删掉该文件即重测。
|
|
14
|
+
- **`models[].catalog` 写错时给 warning**:`catalog: "provider/id"` 不命中任何目录条目时,注册表记一条 warning(启动头部计数、`ama models list`、`AMA_LOG`),不再静默不继承。
|
|
15
|
+
|
|
16
|
+
### 会话
|
|
17
|
+
|
|
18
|
+
- **会话图片读不回时有提示**:降级图片因会话文件被其它进程改写而读不回时,告警进会话日志(stderr 一行 `ama: [warn] cannot read session entry …`,受 `AMA_LOG` 过滤;TUI 显示在通知区),不再被丢弃。打开文件期间的告警先缓冲、会话建好时补出(最多 16 条,其余计数);`--fork` 的源会话走同一日志。
|
|
19
|
+
- **`subagents.forkMaxContextRatio`**:fork 子 Agent 回落为 fresh 的比例(父上一次请求占(窗口 − `compaction.reserveTokens`)的份额)可配置,0.05–0.95,缺省 0.5,只认用户级。内置类型(包括 `general`)仍缺省 fresh;docs/agents.md 说明了每回合重读父上下文的成本。
|
|
20
|
+
- **fork 子 Agent 被告知 `task` / `task_ctl` 不可用**:`<task>` 消息列出这两个工具并要求不要调用(它们仍在工具表里、按深度拒绝)。两次 DeepSeek 复测里子 Agent 仍各调了一次被拒的 `task`。
|
|
21
|
+
- **后台 fork 任务立即给出模式**:后台 `task` 的 `running` 结果在模式已知时带 `details.context`(`fork` / `fresh`);排队或等待 worktree 的任务仍在之后的 `TaskInfo.context` 里给出。
|
|
22
|
+
- **后台 `bash` 任务输出按会话结果上限一次截到位**:`{job, action: "wait" | "output" | "stop"}` 改为按与前台 `bash` 相同的字节上限(50 KB 与 `maxToolResultChars` − 512 取小)尾截断,不再固定 2000 行 / 50 KB 再被会话层截掉中段;说明行照旧写显示行数与 `Full output: <路径>`。
|
|
23
|
+
|
|
24
|
+
### 检查点
|
|
25
|
+
|
|
26
|
+
- **首次影子快照偏慢不再让会话降级**:首次快照(建索引)不计时,之后连续两次快照超过 3 秒才降级为 `tools`(单次偏慢、下一次正常则继续用影子 git);降级仍只提示一次,文案改为说明是连续两次(#194)。
|
|
27
|
+
|
|
28
|
+
## 0.7.5(2026-10-10)
|
|
29
|
+
|
|
30
|
+
### 内存占用
|
|
31
|
+
|
|
32
|
+
- **`read` 读大文件不再阻塞事件循环**:超过 1 MiB 的文本走的字节窗口改为异步按 64 KiB 分块读,每块一次 `await`,扫描 256 MB 文件期间 TUI / RPC / ACP 仍能响应(事件循环最长停顿约 170–320 ms → 20 ms 以内)。输出逐字节不变(同步与异步共用同一个行解析器),峰值内存不变(仍只有一块缓冲)。
|
|
33
|
+
- **带图请求体不再复制每张图**:图片数据从转录直接写入请求体,不再每个请求先拼出一份新的 data URL 字符串;转义检查每张图只做一次;请求体按每块最多 256 Ki 码元编码,不再一次编码一整张图。mock 300 步带 15 张图:峰值 RSS 约 630–690 → 390–400 MB,heapUsed 峰值约 270 → 85 MB。请求字节不变;SDK 的 `onPayload` 回调里大图变为带 `toJSON` 的对象(`JSON.stringify` 结果相同)。
|
|
34
|
+
- **降级后释放旧图**:活动分支上的消息被 `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` 为空串。
|
|
35
|
+
- **ACP 关掉的启动会话也能回收**:`Runtime.session` 与 `Runtime.sessionManager` 改为跟随当前会话(原先固定指向启动时建的会话),`session/close` 之后它的转录与图片和其它会话一样被回收(8 会话 × 4 轮全部关闭后 external 16.3 → 7.0 MB)。SDK 代码在切换会话后读 `runtime.session` 得到的是新会话。
|
|
36
|
+
- **ACP `session/prompt` 的 usage 只算本回合**:等过后台子 Agent 通知回合的提示,`usage` 不再计入那一回合的 token。
|
|
37
|
+
- **通知回合在跑时 `session/close`**:原先以 -32603 失败且会话留在内存;现在先停在跑的后台子 Agent、中断在跑的回合(含通知回合)、已排着的通知不再开回合,然后释放会话,之后不再有 `session/update`。stdin 关闭时同样这样收尾。
|
|
38
|
+
|
|
7
39
|
## 0.7.4(2026-10-10)
|
|
8
40
|
|
|
9
41
|
- **并行 worktree 隔离**:两个隔离任务同时开始时,可能报 `无法读取 .git/worktrees/<id>/commondir`——`git worktree add` 读到了另一个任务建了一半的管理目录。现在同一仓库的 worktree 增删与删分支按顺序执行,另一个 ama 进程造成的同类瞬时错误短暂重试。
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
|
|
5
5
|
* 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
|
|
6
6
|
* `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
|
|
7
|
+
* - [#153] 中转 / 自定义供应商(没有目录文件):会话模型按 id 继承的目录条目 → 原厂 `small` →
|
|
8
|
+
* 本供应商模型表(config `models[]`、发现缓存)里**已列出**的同一条目;不走合成路径。
|
|
7
9
|
* - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
|
|
8
10
|
* 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
|
|
9
11
|
* `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
* - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
|
|
5
5
|
* 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
|
|
6
6
|
* `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
|
|
7
|
+
* - [#153] 中转 / 自定义供应商(没有目录文件):会话模型按 id 继承的目录条目 → 原厂 `small` →
|
|
8
|
+
* 本供应商模型表(config `models[]`、发现缓存)里**已列出**的同一条目;不走合成路径。
|
|
7
9
|
* - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
|
|
8
10
|
* 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
|
|
9
11
|
* `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
|
|
10
12
|
* - 用量记一条 `usage{kind:"permission_classify"}` 条目:计入 `/session` 费用,不进上下文。
|
|
11
13
|
*/
|
|
12
14
|
import { CLASSIFIER_MAX_TOKENS, PermissionClassifier, } from "../permissions/classifier.js";
|
|
13
|
-
import { catalogSmall } from "../ai/providers/catalog.js";
|
|
15
|
+
import { catalogByAlias, catalogSmall } from "../ai/providers/catalog.js";
|
|
14
16
|
export const CLASSIFY_USAGE_KIND = "permission_classify";
|
|
15
17
|
function textOf(message) {
|
|
16
18
|
return message.content
|
|
@@ -19,9 +21,28 @@ function textOf(message) {
|
|
|
19
21
|
.join("")
|
|
20
22
|
.trim();
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
24
|
+
/** [#153] 会话模型继承的目录条目所属厂商的 `small`,在本供应商模型表里列出的条目 id。 */
|
|
25
|
+
function inferredSmall(core, session) {
|
|
26
|
+
const hit = catalogByAlias(session.id);
|
|
27
|
+
if (hit === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const origin = hit.ref.slice(0, hit.ref.indexOf("/"));
|
|
30
|
+
const small = catalogSmall(origin);
|
|
31
|
+
const target = `${origin}/${small}`;
|
|
32
|
+
if (small === undefined || hit.ref === target)
|
|
33
|
+
return undefined;
|
|
34
|
+
const listed = core.options.providers.get(session.provider)?.models ?? [];
|
|
35
|
+
return listed.find((m) => {
|
|
36
|
+
if (m.id === small)
|
|
37
|
+
return true;
|
|
38
|
+
const entry = catalogByAlias(m.id);
|
|
39
|
+
return entry?.ref === target && entry.tier === undefined;
|
|
40
|
+
})?.id;
|
|
41
|
+
}
|
|
42
|
+
/** 分类用的小模型:会话供应商的目录 small,或 [#153] 推断出的同厂商小模型;能找到且 key 可用(或无需 key)才用。 */
|
|
23
43
|
async function smallModel(core, session) {
|
|
24
|
-
const
|
|
44
|
+
const own = catalogSmall(session.provider);
|
|
45
|
+
const small = own === undefined ? inferredSmall(core, session) : own;
|
|
25
46
|
if (small === undefined || small === session.id)
|
|
26
47
|
return undefined;
|
|
27
48
|
const lookup = core.options.providers.findModel(`${session.provider}/${small}`);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 工厂在会话构造中途调用:只可读 `core.options / depth / cwd / manager`,不要 emit 或请求模型。
|
|
21
21
|
* - 钩子抛错不影响会话:同步钩子的异常记 warn 日志后忽略;`beforePrompts` / `onAgentSettled`
|
|
22
22
|
* 的异常同样记日志、按「无追加」处理。
|
|
23
|
+
* - 会话级诊断(#183):构造时顺带把 `core.manager` 的告警接到 `core.log`(每个会话实例一次)。
|
|
23
24
|
* - `onAgentSettled` 不要等待人工输入(计划审批等):需要等待的流程自行异步进行,之后用
|
|
24
25
|
* `prompt / followUp` 开新回合。
|
|
25
26
|
*/
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 工厂在会话构造中途调用:只可读 `core.options / depth / cwd / manager`,不要 emit 或请求模型。
|
|
21
21
|
* - 钩子抛错不影响会话:同步钩子的异常记 warn 日志后忽略;`beforePrompts` / `onAgentSettled`
|
|
22
22
|
* 的异常同样记日志、按「无追加」处理。
|
|
23
|
+
* - 会话级诊断(#183):构造时顺带把 `core.manager` 的告警接到 `core.log`(每个会话实例一次)。
|
|
23
24
|
* - `onAgentSettled` 不要等待人工输入(计划审批等):需要等待的流程自行异步进行,之后用
|
|
24
25
|
* `prompt / followUp` 开新回合。
|
|
25
26
|
*/
|
|
@@ -36,6 +37,8 @@ export class SessionExtensions {
|
|
|
36
37
|
list.push(extension);
|
|
37
38
|
}
|
|
38
39
|
this.list = list;
|
|
40
|
+
// 会话级诊断(#183):manager 的告警(图片读不回)走会话日志;动态读 options.log,TUI 换掉也生效
|
|
41
|
+
core.manager.setWarn((message) => core.log("warn", message));
|
|
39
42
|
}
|
|
40
43
|
get size() {
|
|
41
44
|
return this.list.length;
|
|
@@ -137,7 +137,9 @@ async function startAmaChild(parent, spec, createChild, run) {
|
|
|
137
137
|
}
|
|
138
138
|
const thinkingLevel = spec.request.thinkingLevel ?? spec.agent.thinking ?? base.thinkingLevel ?? "off";
|
|
139
139
|
const wanted = spec.resumeFile === undefined && requestedContext(spec.request, spec.agent) === "fork";
|
|
140
|
-
|
|
140
|
+
// #149:比例来自 `subagents.forkMaxContextRatio`(注册表环境里的整段配置)
|
|
141
|
+
const ratio = subagentRegistryFor(parent).env.modelConfig?.subagents?.forkMaxContextRatio;
|
|
142
|
+
const plan = wanted ? forkPlan(parent, spec, model, thinkingLevel, ratio) : undefined;
|
|
141
143
|
if (plan !== undefined && "fallback" in plan)
|
|
142
144
|
parent.log("info", `task ${spec.taskId}: fork falls back to fresh (${plan.fallback})`);
|
|
143
145
|
const forked = plan !== undefined && "manager" in plan ? plan.manager : undefined;
|
|
@@ -149,6 +151,11 @@ async function startAmaChild(parent, spec, createChild, run) {
|
|
|
149
151
|
// fork:工具表 = 父活动集(前缀不变),类型限制的工具在执行层拒绝
|
|
150
152
|
const inherited = base.activeTools ?? (parent.options.tools ?? []).map((tool) => tool.name);
|
|
151
153
|
const unavailable = inherited.filter((name) => !allowed.includes(name) && !PARENT_ONLY_TOOLS.includes(name));
|
|
154
|
+
// #150:<task> 里也列出按深度拒绝的 task / task_ctl(执行层的拒绝文案不变)
|
|
155
|
+
const briefUnavailable = [
|
|
156
|
+
...unavailable,
|
|
157
|
+
...PARENT_ONLY_TOOLS.filter((name) => inherited.includes(name)),
|
|
158
|
+
];
|
|
152
159
|
const options = {
|
|
153
160
|
...parent.options,
|
|
154
161
|
...base,
|
|
@@ -252,7 +259,7 @@ async function startAmaChild(parent, spec, createChild, run) {
|
|
|
252
259
|
: forkBrief({
|
|
253
260
|
prompt: run.prompt,
|
|
254
261
|
role: spec.agent.prompt,
|
|
255
|
-
unavailable,
|
|
262
|
+
unavailable: briefUnavailable,
|
|
256
263
|
...(spec.isolated ? { worktree: { cwd: spec.cwd, parentCwd: parent.cwd } } : {}),
|
|
257
264
|
});
|
|
258
265
|
let current = runOnce(first, spec.origin);
|
|
@@ -18,7 +18,11 @@ export type BackgroundReason = "user" | "timeout" | "host";
|
|
|
18
18
|
export type BackgroundSetting = "auto" | "always" | "never";
|
|
19
19
|
/** `auto`:交互 / RPC / ACP 缺省后台,`-p`(无人值守)缺省前台。 */
|
|
20
20
|
export declare function resolveTaskBackground(setting: BackgroundSetting | undefined, unattended: boolean): boolean;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* 直接后台的工具结果。`settle`(任务的运行 promise)给出时先等一个宏任务:ama 子会话在第一个
|
|
23
|
+
* await 之前就发出 `context` 事件,池有空位时此刻模式已知;排队或 worktree 隔离未就绪时不带。
|
|
24
|
+
*/
|
|
25
|
+
export declare function startedResult(record: TaskRecord, outputFile?: string, settle?: Promise<unknown>): Promise<SubagentResult>;
|
|
22
26
|
export declare function backgroundedResult(record: TaskRecord, reason: BackgroundReason, afterMs: number, outputFile?: string): SubagentResult;
|
|
23
27
|
/** 前台任务的等待者:`resolve` 让工具调用先返回,`detachParent` 解绑父 signal 与进度回调。 */
|
|
24
28
|
export declare function foregroundWaiter(detachParent: () => void): {
|
|
@@ -32,9 +32,21 @@ function runningResult(record, text, outputFile) {
|
|
|
32
32
|
};
|
|
33
33
|
if (outputFile !== undefined)
|
|
34
34
|
result.outputFile = outputFile;
|
|
35
|
+
// #156:fork / fresh 已定时带上实际模式(与完成结果的 details.context 同源)
|
|
36
|
+
if (record.info.context !== undefined)
|
|
37
|
+
result.context = record.info.context;
|
|
35
38
|
return result;
|
|
36
39
|
}
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* 直接后台的工具结果。`settle`(任务的运行 promise)给出时先等一个宏任务:ama 子会话在第一个
|
|
42
|
+
* await 之前就发出 `context` 事件,池有空位时此刻模式已知;排队或 worktree 隔离未就绪时不带。
|
|
43
|
+
*/
|
|
44
|
+
export async function startedResult(record, outputFile, settle) {
|
|
45
|
+
if (settle !== undefined)
|
|
46
|
+
await Promise.race([
|
|
47
|
+
settle.catch(() => undefined),
|
|
48
|
+
new Promise((resolve) => setImmediate(resolve)),
|
|
49
|
+
]);
|
|
38
50
|
return runningResult(record, startedText(record.info.taskId, record.agent.name), outputFile);
|
|
39
51
|
}
|
|
40
52
|
export function backgroundedResult(record, reason, afterMs, outputFile) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
|
|
6
6
|
* - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
|
|
7
7
|
* `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
|
|
8
|
-
* 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `
|
|
8
|
+
* 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `subagents.forkMaxContextRatio`(缺省 0.5)×
|
|
9
9
|
* (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
|
|
10
10
|
* - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
|
|
11
11
|
* `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
|
|
@@ -16,8 +16,13 @@ import type { SessionManager } from "../session/manager.js";
|
|
|
16
16
|
import type { SessionEntry } from "../session/types.js";
|
|
17
17
|
import type { SubagentRequest } from "../tools/types.js";
|
|
18
18
|
import type { AgentSessionOptions } from "./session-core.js";
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh;
|
|
21
|
+
* 缺省值,可由 `subagents.forkMaxContextRatio` 覆盖(#149)。
|
|
22
|
+
*/
|
|
20
23
|
export declare const FORK_MAX_CONTEXT_RATIO = 0.5;
|
|
24
|
+
/** 配置的比例:非有限数或不在 (0, 1) 内时用缺省。 */
|
|
25
|
+
export declare function forkRatio(ratio: number | undefined): number;
|
|
21
26
|
export type ContextMode = "fork" | "fresh";
|
|
22
27
|
/** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
|
|
23
28
|
export declare function requestedContext(request: Pick<SubagentRequest, "context">, agent: Pick<AgentDefinition, "context">): ContextMode;
|
|
@@ -36,7 +41,7 @@ export interface ForkParent {
|
|
|
36
41
|
export interface ForkSpec {
|
|
37
42
|
taskId: string;
|
|
38
43
|
agent: Pick<AgentDefinition, "name">;
|
|
39
|
-
request: Pick<SubagentRequest, "parentToolCallId" | "description">;
|
|
44
|
+
request: Pick<SubagentRequest, "parentToolCallId" | "description" | "prompt">;
|
|
40
45
|
/** 子会话 cwd(隔离时是 worktree)。 */
|
|
41
46
|
cwd: string;
|
|
42
47
|
}
|
|
@@ -48,9 +53,9 @@ export type ForkPlan = {
|
|
|
48
53
|
};
|
|
49
54
|
/**
|
|
50
55
|
* D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
|
|
51
|
-
* `model` / `thinking`
|
|
56
|
+
* `model` / `thinking` 是子会话按类型与参数解析出的值;`ratio` 是 `subagents.forkMaxContextRatio`。
|
|
52
57
|
*/
|
|
53
|
-
export declare function forkPlan(parent: ForkParent, spec: ForkSpec, model: Model, thinking: ModelThinkingLevel): ForkPlan;
|
|
58
|
+
export declare function forkPlan(parent: ForkParent, spec: ForkSpec, model: Model, thinking: ModelThinkingLevel, ratio?: number): ForkPlan;
|
|
54
59
|
export interface ForkBriefInput {
|
|
55
60
|
prompt: string;
|
|
56
61
|
/** 类型正文(不含通用前言:这里的首句已说明子 Agent 身份)。 */
|
|
@@ -5,14 +5,24 @@
|
|
|
5
5
|
* 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
|
|
6
6
|
* - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
|
|
7
7
|
* `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
|
|
8
|
-
* 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `
|
|
8
|
+
* 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `subagents.forkMaxContextRatio`(缺省 0.5)×
|
|
9
9
|
* (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
|
|
10
10
|
* - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
|
|
11
11
|
* `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
|
|
12
12
|
*/
|
|
13
|
+
import { FORK_CUSTOM_TYPE, taskLine } from "../tools/task.js";
|
|
13
14
|
import { DEFAULT_COMPACTION_SETTINGS } from "./session-compaction.js";
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh;
|
|
17
|
+
* 缺省值,可由 `subagents.forkMaxContextRatio` 覆盖(#149)。
|
|
18
|
+
*/
|
|
15
19
|
export const FORK_MAX_CONTEXT_RATIO = 0.5;
|
|
20
|
+
/** 配置的比例:非有限数或不在 (0, 1) 内时用缺省。 */
|
|
21
|
+
export function forkRatio(ratio) {
|
|
22
|
+
return ratio !== undefined && Number.isFinite(ratio) && ratio > 0 && ratio < 1
|
|
23
|
+
? ratio
|
|
24
|
+
: FORK_MAX_CONTEXT_RATIO;
|
|
25
|
+
}
|
|
16
26
|
const TASK_CUSTOM_TYPE = "ama.task";
|
|
17
27
|
/** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
|
|
18
28
|
export function requestedContext(request, agent) {
|
|
@@ -27,9 +37,9 @@ export function forkPoint(branch, parentToolCallId) {
|
|
|
27
37
|
}
|
|
28
38
|
/**
|
|
29
39
|
* D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
|
|
30
|
-
* `model` / `thinking`
|
|
40
|
+
* `model` / `thinking` 是子会话按类型与参数解析出的值;`ratio` 是 `subagents.forkMaxContextRatio`。
|
|
31
41
|
*/
|
|
32
|
-
export function forkPlan(parent, spec, model, thinking) {
|
|
42
|
+
export function forkPlan(parent, spec, model, thinking, ratio) {
|
|
33
43
|
const base = parent.childBase();
|
|
34
44
|
if (model.provider !== base.model.provider || model.id !== base.model.id)
|
|
35
45
|
return { fallback: `model ${model.provider}/${model.id} differs from the parent` };
|
|
@@ -40,7 +50,7 @@ export function forkPlan(parent, spec, model, thinking) {
|
|
|
40
50
|
return { fallback: "the parent has not sent a request yet" };
|
|
41
51
|
const reserve = parent.options.compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
42
52
|
const window = model.contextWindow;
|
|
43
|
-
const limit = window === undefined ? Infinity :
|
|
53
|
+
const limit = window === undefined ? Infinity : forkRatio(ratio) * Math.max(0, window - reserve);
|
|
44
54
|
if (last.promptTokens > limit)
|
|
45
55
|
return { fallback: `parent context ${last.promptTokens} tokens exceeds ${Math.floor(limit)}` };
|
|
46
56
|
const forkedFrom = forkPoint(parent.manager.branch(), spec.request.parentToolCallId);
|
|
@@ -63,6 +73,12 @@ export function forkPlan(parent, spec, model, thinking) {
|
|
|
63
73
|
},
|
|
64
74
|
},
|
|
65
75
|
});
|
|
76
|
+
// #191:fork 标记(不进上下文);复制来的父 `ama.task` 快照可能更晚,故不复用那个类型
|
|
77
|
+
manager.append({
|
|
78
|
+
type: "custom",
|
|
79
|
+
customType: FORK_CUSTOM_TYPE,
|
|
80
|
+
data: { taskLine: taskLine(spec.request.prompt) },
|
|
81
|
+
});
|
|
66
82
|
return { manager, forkedFrom };
|
|
67
83
|
}
|
|
68
84
|
/** fork 子会话的首条 user 消息(固定英文,发给模型)。 */
|
|
@@ -72,7 +88,7 @@ export function forkBrief(input) {
|
|
|
72
88
|
"You are a sub-agent forked from the conversation above at this point. The main agent cannot " +
|
|
73
89
|
"see your work, only your final reply; do not delegate further. Requests above were for the " +
|
|
74
90
|
"main agent: do only this task. Instructions in this block take precedence over earlier " +
|
|
75
|
-
"plans or reminders above.",
|
|
91
|
+
"plans or reminders above. Do not call task or task_ctl.",
|
|
76
92
|
];
|
|
77
93
|
if (input.worktree !== undefined)
|
|
78
94
|
lines.push(`Working directory for this task: ${input.worktree.cwd}. Relative paths in the ` +
|
|
@@ -62,7 +62,7 @@ export declare function registryOf(sessionId: string): SubagentRegistry | undefi
|
|
|
62
62
|
export declare function subagentRegistryFor(host: RegistryHost, env?: SubagentEnvironment): SubagentRegistry;
|
|
63
63
|
export declare class SubagentRegistry implements TaskControl {
|
|
64
64
|
private readonly host;
|
|
65
|
-
|
|
65
|
+
readonly env: SubagentEnvironment;
|
|
66
66
|
readonly catalog: AgentCatalog;
|
|
67
67
|
private readonly pool;
|
|
68
68
|
private readonly tasks;
|
|
@@ -245,7 +245,7 @@ export class SubagentRegistry {
|
|
|
245
245
|
const file = this.outputFileFor(record.info.taskId);
|
|
246
246
|
if (background) {
|
|
247
247
|
void running.then((result) => this.notifier.notify(record, result));
|
|
248
|
-
return
|
|
248
|
+
return startedResult(record, file, running);
|
|
249
249
|
}
|
|
250
250
|
// [W7-B1] 前台:转后台(background())时等待者先行 resolve,工具调用立即返回
|
|
251
251
|
const { waiter, promise } = foregroundWaiter(() => {
|
package/dist/agents/catalog.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 除非 `supportsTemperatureWithThinking`。`toolChoice: "none"`(有工具时)→ `tool_choice:{type:"none"}`。
|
|
18
18
|
*/
|
|
19
19
|
import { contentText, normalizeContext, sanitizeText } from "../context.js";
|
|
20
|
+
import { largeString } from "../json-body.js";
|
|
20
21
|
import { budgetedMaxTokens, clampThinkingLevel, mappedThinkingValue, thinkingBudget, } from "../thinking.js";
|
|
21
22
|
import { detectAnthropicCompat } from "./anthropic-compat.js";
|
|
22
23
|
import { effectiveRetention, resolveCacheRetention, resolvePromptCacheCompat, } from "./cache-params.js";
|
|
@@ -76,7 +77,11 @@ function convertBlocks(content) {
|
|
|
76
77
|
else {
|
|
77
78
|
blocks.push({
|
|
78
79
|
type: "image",
|
|
79
|
-
source: {
|
|
80
|
+
source: {
|
|
81
|
+
type: "base64",
|
|
82
|
+
media_type: block.mimeType,
|
|
83
|
+
data: largeString("", block, block.data),
|
|
84
|
+
},
|
|
80
85
|
});
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 并提示一次(实测:某中转的 Responses 对 DeepSeek 拒收 prompt_cache_retention / _options)。
|
|
15
15
|
*/
|
|
16
16
|
import { HttpError, postJson } from "../http.js";
|
|
17
|
+
import { LargeString } from "../json-body.js";
|
|
17
18
|
import { msg } from "../../i18n/index.js";
|
|
18
19
|
const DEFAULT_BASE_URLS = {
|
|
19
20
|
"anthropic-messages": "https://api.anthropic.com",
|
|
@@ -133,7 +134,7 @@ export function isCacheParamRejection(error) {
|
|
|
133
134
|
function stripInto(value, removed) {
|
|
134
135
|
if (Array.isArray(value))
|
|
135
136
|
return value.map((item) => stripInto(item, removed));
|
|
136
|
-
if (typeof value !== "object" || value === null)
|
|
137
|
+
if (typeof value !== "object" || value === null || value instanceof LargeString)
|
|
137
138
|
return value;
|
|
138
139
|
const out = {};
|
|
139
140
|
for (const [key, inner] of Object.entries(value)) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - `toolChoice: "none"`(有工具时)→ `toolConfig.functionCallingConfig.mode: "NONE"`。
|
|
14
14
|
*/
|
|
15
15
|
import { contentText, normalizeContext, sanitizeText } from "../context.js";
|
|
16
|
+
import { largeString } from "../json-body.js";
|
|
16
17
|
import { DEFAULT_THINKING_BUDGETS, clampBudgetToAnswerRoom, clampThinkingLevel, discreteThinkingLevel, } from "../thinking.js";
|
|
17
18
|
export const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
18
19
|
const GOOGLE_COMPAT_KEYS = [
|
|
@@ -63,7 +64,7 @@ function validSignature(signature) {
|
|
|
63
64
|
BASE64.test(signature));
|
|
64
65
|
}
|
|
65
66
|
function inlineData(block) {
|
|
66
|
-
return { inlineData: { mimeType: block.mimeType, data: block.data } };
|
|
67
|
+
return { inlineData: { mimeType: block.mimeType, data: largeString("", block, block.data) } };
|
|
67
68
|
}
|
|
68
69
|
function convertUser(message) {
|
|
69
70
|
if (typeof message.content === "string") {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
|
|
9
9
|
* 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
|
|
10
10
|
* 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
|
|
11
|
+
* - 跨进程:学到的上限经 `onMaxTokensCap` 写进 `<dataDir>/models/max-tokens-caps.json`(30 天),
|
|
12
|
+
* 组装注册表时载回(providers/max-tokens-cache.ts,#152)。
|
|
11
13
|
*/
|
|
12
14
|
import { type PostOptions } from "../http.js";
|
|
13
15
|
import type { Model } from "../types.js";
|
|
@@ -19,6 +21,8 @@ export declare const OUTPUT_HEADROOM_TOKENS = 2048;
|
|
|
19
21
|
export type MaxTokensField = "max_tokens" | "max_completion_tokens" | "max_output_tokens";
|
|
20
22
|
/** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
|
|
21
23
|
export declare const maxTokensCaps: Map<string, number>;
|
|
24
|
+
/** 被动修正学到新上限时通知(数据目录持久化用,max-tokens-cache.ts);返回取消函数。 */
|
|
25
|
+
export declare function onMaxTokensCap(listener: (key: string, cap: number) => void): () => void;
|
|
22
26
|
type Json = Record<string, unknown>;
|
|
23
27
|
/** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
|
|
24
28
|
export declare function clampMaxTokens(requested: number, window: number | undefined, estimatedInput: number, fixed: boolean): number;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
|
|
9
9
|
* 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
|
|
10
10
|
* 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
|
|
11
|
+
* - 跨进程:学到的上限经 `onMaxTokensCap` 写进 `<dataDir>/models/max-tokens-caps.json`(30 天),
|
|
12
|
+
* 组装注册表时载回(providers/max-tokens-cache.ts,#152)。
|
|
11
13
|
*/
|
|
12
14
|
import { HttpError, errorText } from "../http.js";
|
|
13
15
|
import { postWithCacheFallback } from "./cache-params.js";
|
|
@@ -17,6 +19,12 @@ export const MIN_OUTPUT_TOKENS = 1024;
|
|
|
17
19
|
export const OUTPUT_HEADROOM_TOKENS = 2048;
|
|
18
20
|
/** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
|
|
19
21
|
export const maxTokensCaps = new Map();
|
|
22
|
+
const capListeners = new Set();
|
|
23
|
+
/** 被动修正学到新上限时通知(数据目录持久化用,max-tokens-cache.ts);返回取消函数。 */
|
|
24
|
+
export function onMaxTokensCap(listener) {
|
|
25
|
+
capListeners.add(listener);
|
|
26
|
+
return () => capListeners.delete(listener);
|
|
27
|
+
}
|
|
20
28
|
/** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
|
|
21
29
|
export function clampMaxTokens(requested, window, estimatedInput, fixed) {
|
|
22
30
|
if (fixed || window === undefined || !Number.isFinite(window) || window <= 0)
|
|
@@ -111,8 +119,11 @@ export async function postWithMaxTokensFallback(model, url, options, field) {
|
|
|
111
119
|
throw error;
|
|
112
120
|
if (rejection.cap >= sent || !budgetFits(body, rejection.cap))
|
|
113
121
|
throw error;
|
|
114
|
-
if (!rejection.transient)
|
|
122
|
+
if (!rejection.transient) {
|
|
115
123
|
maxTokensCaps.set(key, rejection.cap);
|
|
124
|
+
for (const listener of capListeners)
|
|
125
|
+
listener(key, rejection.cap);
|
|
126
|
+
}
|
|
116
127
|
return postWithCacheFallback(model, url, {
|
|
117
128
|
...options,
|
|
118
129
|
body: withCap(body, field, rejection.cap),
|
|
@@ -19,12 +19,14 @@ import { contentText, normalizeContext, normalizeContextInline, sanitizeText, }
|
|
|
19
19
|
import { DEFAULT_THINKING_BUDGETS, clampBudgetToAnswerRoom, clampThinkingLevel, mappedThinkingValue, } from "../thinking.js";
|
|
20
20
|
import { cacheControlFor } from "./anthropic-request.js";
|
|
21
21
|
import { effectiveRetention, resolveCacheRetention, resolvePromptCacheCompat, } from "./cache-params.js";
|
|
22
|
+
import { largeString } from "../json-body.js";
|
|
22
23
|
import { detectCompat } from "./openai-compat.js";
|
|
23
24
|
/** 思考文本回放用的字段名;thinkingSignature 记录来源字段,回放时原样放回。 */
|
|
24
25
|
export const REASONING_FIELDS = ["reasoning_content", "reasoning", "reasoning_text"];
|
|
25
26
|
export const ASSISTANT_BRIDGE_TEXT = "Tool results received.";
|
|
26
27
|
function imagePart(block) {
|
|
27
|
-
|
|
28
|
+
const url = largeString(`data:${block.mimeType};base64,`, block, block.data);
|
|
29
|
+
return { type: "image_url", image_url: { url } };
|
|
28
30
|
}
|
|
29
31
|
function convertUser(message) {
|
|
30
32
|
if (typeof message.content === "string") {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* - [W6-O] compat `chatgptBackend`(ChatGPT 订阅后端):最后经 chatgpt-backend.ts 按后端白名单改写。
|
|
22
22
|
*/
|
|
23
23
|
import { contentText, normalizeContext, sanitizeText } from "../context.js";
|
|
24
|
+
import { largeString } from "../json-body.js";
|
|
24
25
|
import { clampThinkingLevel, mappedThinkingValue } from "../thinking.js";
|
|
25
26
|
import { resolveCacheRetention, resolvePromptCacheCompat } from "./cache-params.js";
|
|
26
27
|
import { transformChatGptBody } from "./chatgpt-backend.js";
|
|
@@ -110,7 +111,7 @@ function imagePart(block) {
|
|
|
110
111
|
return {
|
|
111
112
|
type: "input_image",
|
|
112
113
|
detail: "auto",
|
|
113
|
-
image_url: `data:${block.mimeType};base64
|
|
114
|
+
image_url: largeString(`data:${block.mimeType};base64,`, block, block.data),
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
117
|
function convertUser(message) {
|
package/dist/ai/json-body.d.ts
CHANGED
|
@@ -11,9 +11,36 @@
|
|
|
11
11
|
* 键、`[`)与已验证无需转义的大字符串(需要转义的大字符串整个交给原生);`toJSON`、`undefined` /
|
|
12
12
|
* 函数 / symbol 的跳过与 `null`、数字格式、转义全部由原生处理(带 `toJSON` 的值经单键容器序列化,
|
|
13
13
|
* 保证 `toJSON(key)` 收到与原生相同的键)。
|
|
14
|
+
*
|
|
15
|
+
* 图片由转换器包成 {@link LargeString}(`prefix + text`):片段路径直接写转录里的原字符串,不经模板串
|
|
16
|
+
* 拼接后的扁平化;无需转义的结论与字节数按 ImageBlock 缓存,同一张图每进程只扫一次。流式发送时大片段
|
|
17
|
+
* 按 {@link STREAM_CHUNK_CHARS} 切块编码。
|
|
14
18
|
*/
|
|
15
19
|
/** 达到这个长度(UTF-16 码元)的字符串才走片段路径。 */
|
|
16
20
|
export declare const LARGE_STRING_BYTES: number;
|
|
21
|
+
/** `isPlain` 的调用次数(测试用:同一块图片第二次序列化不应再扫)。 */
|
|
22
|
+
export declare const plainScans: {
|
|
23
|
+
count: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 请求体里的大字符串叶子 `prefix + text`(data URL 或裸 base64),用来代替模板串拼接:拼接得到的
|
|
27
|
+
* cons string 在每次序列化时都会被扁平化成一份新的整图字符串。片段路径直接写 `prefix` 与 `text`
|
|
28
|
+
* (`text` 是转录里的原字符串,不拷贝);`text` 无需转义的结论按 `key`(转录里身份稳定的 ImageBlock)
|
|
29
|
+
* 缓存,同一块每进程只扫一次。约定:同一 `key` 的 `text` 不变。其它遍历者看到的是一个带 `toJSON`
|
|
30
|
+
* 的对象,原生 `JSON.stringify` 得到相同字符串。由 {@link largeString} 构造。
|
|
31
|
+
*/
|
|
32
|
+
export declare class LargeString {
|
|
33
|
+
readonly prefix: string;
|
|
34
|
+
readonly key: object;
|
|
35
|
+
readonly text: string;
|
|
36
|
+
constructor(prefix: string, key: object, text: string);
|
|
37
|
+
toJSON(): string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `prefix + text` 作为请求体的值:`text` 够大且 `prefix` 自身无需转义时给 {@link LargeString},
|
|
41
|
+
* 否则就是拼好的字符串(小图与以前完全相同)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function largeString(prefix: string, key: object, text: string): string | LargeString;
|
|
17
44
|
/** 与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同;大字符串不经中间字符串。 */
|
|
18
45
|
export declare function serializeJsonBody(body: unknown): Buffer;
|
|
19
46
|
/** `postJson` 交给 fetch 的请求体。 */
|
|
@@ -22,10 +49,15 @@ export interface JsonFetchBody {
|
|
|
22
49
|
/** 流式请求体的字节数:调用方须作为 `content-length` 头发出(不走 chunked)。 */
|
|
23
50
|
contentLength?: number;
|
|
24
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 流式请求体每块最多编码的码元数:大片段(一整张图)按游标切片(V8 切片串不拷贝)逐块编码,
|
|
54
|
+
* 发送期间只多出一块的 UTF-8 字节,而不是一整张图。
|
|
55
|
+
*/
|
|
56
|
+
export declare const STREAM_CHUNK_CHARS: number;
|
|
25
57
|
/**
|
|
26
58
|
* 给 fetch 的请求体。不含大字符串时就是 `JSON.stringify(body)`(与以前完全相同);含大字符串时是
|
|
27
59
|
* 逐片段编码的流,并给出总字节数。不交给 fetch 一个 Buffer:Node 的 fetch 会把 BufferSource
|
|
28
60
|
* 请求体再复制两份(实测 mock 300 步带图峰值反而从约 1.0 GB 升到 1.85 GB),流则按片段边读边发,
|
|
29
|
-
*
|
|
61
|
+
* 同一时刻只多出一块(≤ {@link STREAM_CHUNK_CHARS} 码元)的 UTF-8 字节。字节序列与 `serializeJsonBody` 相同。
|
|
30
62
|
*/
|
|
31
63
|
export declare function jsonFetchBody(body: unknown): JsonFetchBody;
|