@armadra/agent 0.7.2 → 0.7.4
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 +41 -0
- package/CHANGELOG.zh-CN.md +41 -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 +30 -5
- package/dist/agent/session-compaction.js +69 -29
- 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 +9 -3
- package/dist/ai/apis/google-generative-ai.js +4 -2
- 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 +1 -1
- package/dist/ai/apis/openai-responses-request.js +1 -1
- 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 +31 -0
- package/dist/ai/json-body.js +208 -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 +3142 -1805
- 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/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/compaction/prefix-estimate.d.ts +3 -1
- package/dist/compaction/prefix-estimate.js +7 -3
- 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 +15 -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-context.d.ts +4 -0
- package/dist/i18n/messages/report-context.js +4 -0
- package/dist/i18n/messages/report.d.ts +6 -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 +15 -5
- package/dist/modes/acp/acp-sessions.d.ts +9 -0
- package/dist/modes/acp/acp-sessions.js +25 -0
- package/dist/modes/context-report.d.ts +2 -0
- package/dist/modes/context-report.js +25 -8
- 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 +7 -3
- package/dist/session/manager.js +21 -48
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- 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 +3 -0
- package/dist/session/store.js +37 -31
- 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 +32 -0
- package/dist/tools/read-lines.js +147 -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 +5 -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 +3 -3
- package/docs/permissions.md +4 -1
- package/docs/providers.md +85 -22
- package/docs/rpc.md +22 -6
- package/docs/session-format.md +3 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fork 式子 Agent(docs/model-efficiency-plan.md §2.1,D1–D3)。[ME-A]
|
|
3
|
+
*
|
|
4
|
+
* - fork 点 = 发出本次 `task` 调用的那条 assistant **之前一条**条目(= 父会话上一次真实请求的转录);
|
|
5
|
+
* 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
|
|
6
|
+
* - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
|
|
7
|
+
* `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
|
|
8
|
+
* 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `FORK_MAX_CONTEXT_RATIO` ×
|
|
9
|
+
* (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
|
|
10
|
+
* - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
|
|
11
|
+
* `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
|
|
12
|
+
*/
|
|
13
|
+
import { DEFAULT_COMPACTION_SETTINGS } from "./session-compaction.js";
|
|
14
|
+
/** 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh。 */
|
|
15
|
+
export const FORK_MAX_CONTEXT_RATIO = 0.5;
|
|
16
|
+
const TASK_CUSTOM_TYPE = "ama.task";
|
|
17
|
+
/** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
|
|
18
|
+
export function requestedContext(request, agent) {
|
|
19
|
+
return request.context ?? agent.context ?? "fresh";
|
|
20
|
+
}
|
|
21
|
+
/** 父分支里发出 `parentToolCallId` 的 assistant 之前一条条目的 id;找不到(或它是首条)返回 undefined。 */
|
|
22
|
+
export function forkPoint(branch, parentToolCallId) {
|
|
23
|
+
const index = branch.findIndex((entry) => entry.type === "message" &&
|
|
24
|
+
entry.message.role === "assistant" &&
|
|
25
|
+
entry.message.content.some((block) => block.type === "toolCall" && block.id === parentToolCallId));
|
|
26
|
+
return index > 0 ? branch[index - 1]?.id : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
|
|
30
|
+
* `model` / `thinking` 是子会话按类型与参数解析出的值。
|
|
31
|
+
*/
|
|
32
|
+
export function forkPlan(parent, spec, model, thinking) {
|
|
33
|
+
const base = parent.childBase();
|
|
34
|
+
if (model.provider !== base.model.provider || model.id !== base.model.id)
|
|
35
|
+
return { fallback: `model ${model.provider}/${model.id} differs from the parent` };
|
|
36
|
+
if (thinking !== (base.thinkingLevel ?? "off"))
|
|
37
|
+
return { fallback: `thinking level ${thinking} differs from the parent` };
|
|
38
|
+
const last = parent.cache?.lastTurn;
|
|
39
|
+
if (last === undefined)
|
|
40
|
+
return { fallback: "the parent has not sent a request yet" };
|
|
41
|
+
const reserve = parent.options.compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
42
|
+
const window = model.contextWindow;
|
|
43
|
+
const limit = window === undefined ? Infinity : FORK_MAX_CONTEXT_RATIO * Math.max(0, window - reserve);
|
|
44
|
+
if (last.promptTokens > limit)
|
|
45
|
+
return { fallback: `parent context ${last.promptTokens} tokens exceeds ${Math.floor(limit)}` };
|
|
46
|
+
const forkedFrom = forkPoint(parent.manager.branch(), spec.request.parentToolCallId);
|
|
47
|
+
if (forkedFrom === undefined)
|
|
48
|
+
return { fallback: "the task call was not found in the parent transcript" };
|
|
49
|
+
const parentSession = parent.manager.file();
|
|
50
|
+
const manager = parent.manager.fork(forkedFrom, {
|
|
51
|
+
cwd: spec.cwd,
|
|
52
|
+
head: {
|
|
53
|
+
type: "custom",
|
|
54
|
+
customType: TASK_CUSTOM_TYPE,
|
|
55
|
+
data: {
|
|
56
|
+
taskId: spec.taskId,
|
|
57
|
+
agent: spec.agent.name,
|
|
58
|
+
parentToolCallId: spec.request.parentToolCallId,
|
|
59
|
+
description: spec.request.description,
|
|
60
|
+
parentSession,
|
|
61
|
+
context: "fork",
|
|
62
|
+
forkedFrom,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return { manager, forkedFrom };
|
|
67
|
+
}
|
|
68
|
+
/** fork 子会话的首条 user 消息(固定英文,发给模型)。 */
|
|
69
|
+
export function forkBrief(input) {
|
|
70
|
+
const lines = [
|
|
71
|
+
"<task>",
|
|
72
|
+
"You are a sub-agent forked from the conversation above at this point. The main agent cannot " +
|
|
73
|
+
"see your work, only your final reply; do not delegate further. Requests above were for the " +
|
|
74
|
+
"main agent: do only this task. Instructions in this block take precedence over earlier " +
|
|
75
|
+
"plans or reminders above.",
|
|
76
|
+
];
|
|
77
|
+
if (input.worktree !== undefined)
|
|
78
|
+
lines.push(`Working directory for this task: ${input.worktree.cwd}. Relative paths in the ` +
|
|
79
|
+
`conversation above refer to ${input.worktree.parentCwd}.`);
|
|
80
|
+
if (input.unavailable.length > 0)
|
|
81
|
+
lines.push(`Tools not available to you: ${input.unavailable.join(", ")}. Calls to them are rejected.`);
|
|
82
|
+
const role = input.role.trim();
|
|
83
|
+
if (role !== "")
|
|
84
|
+
lines.push(`<role>\n${role}\n</role>`);
|
|
85
|
+
lines.push(`<instructions>\n${input.prompt}\n</instructions>`, "Complete the task, then end with a concise report: what you did, key findings, files changed (if any).", "</task>");
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 宿主,经 `env.runners`);同一 `task(agent=…)` 入口;
|
|
8
8
|
* - 并发池(`subagents.maxConcurrent`,缺省 4)+ 排队上限(`maxPending`,缺省 16,超出报错不要重试);
|
|
9
9
|
* - 前台(等结果)/ 后台(立即返回 taskId,完成后 `<task-notification>` 走父会话 followUp 队列);
|
|
10
|
-
* - `taskId` 续聊:句柄保留
|
|
10
|
+
* - `taskId` 续聊:句柄保留 4 个(`subagents.retainSessions`,LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
|
|
11
11
|
* 文件 / 外部会话 id 重新接上;
|
|
12
12
|
* - 结果 > 50 KB 保留头尾并全文落 `outputs/`;轮数耗尽加说明;worktree 隔离;
|
|
13
13
|
* - 事件 `subagent_start / update / end`;父会话 `custom{ama.task}` 记任务快照(带 `status`),
|
|
@@ -26,7 +26,7 @@ import { type DirectReply } from "./subagent-direct.js";
|
|
|
26
26
|
import { type BackgroundReason } from "./subagent-background.js";
|
|
27
27
|
export declare const DEFAULT_SUBAGENT_CONCURRENCY = 4;
|
|
28
28
|
export declare const DEFAULT_MAX_PENDING = 16;
|
|
29
|
-
export declare const DEFAULT_RETAINED =
|
|
29
|
+
export declare const DEFAULT_RETAINED = 4;
|
|
30
30
|
export { SubagentPool, TASK_CUSTOM_TYPE, type AmaRunnerSpec, type TaskHandle };
|
|
31
31
|
export type { BackgroundReason };
|
|
32
32
|
export type AmaRunnerFactory = (spec: AmaRunnerSpec) => SubagentRunner;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 宿主,经 `env.runners`);同一 `task(agent=…)` 入口;
|
|
8
8
|
* - 并发池(`subagents.maxConcurrent`,缺省 4)+ 排队上限(`maxPending`,缺省 16,超出报错不要重试);
|
|
9
9
|
* - 前台(等结果)/ 后台(立即返回 taskId,完成后 `<task-notification>` 走父会话 followUp 队列);
|
|
10
|
-
* - `taskId` 续聊:句柄保留
|
|
10
|
+
* - `taskId` 续聊:句柄保留 4 个(`subagents.retainSessions`,LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
|
|
11
11
|
* 文件 / 外部会话 id 重新接上;
|
|
12
12
|
* - 结果 > 50 KB 保留头尾并全文落 `outputs/`;轮数耗尽加说明;worktree 隔离;
|
|
13
13
|
* - 事件 `subagent_start / update / end`;父会话 `custom{ama.task}` 记任务快照(带 `status`),
|
|
@@ -30,7 +30,7 @@ import { directMessage, drainDirect, liveOf } from "./subagent-direct.js";
|
|
|
30
30
|
import { TaskNotifier, WaitDetach, backgroundRecords, blockingTasks, foregroundWaiter, settleTasks, startedResult, waitForTask, } from "./subagent-background.js";
|
|
31
31
|
export const DEFAULT_SUBAGENT_CONCURRENCY = 4;
|
|
32
32
|
export const DEFAULT_MAX_PENDING = 16;
|
|
33
|
-
export const DEFAULT_RETAINED =
|
|
33
|
+
export const DEFAULT_RETAINED = 4;
|
|
34
34
|
export { SubagentPool, TASK_CUSTOM_TYPE };
|
|
35
35
|
const registries = new Map();
|
|
36
36
|
/** RPC `get_tasks` / `/tasks`:会话 id → 注册表视图(没有任务时 undefined)。 */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具可用性的固定英文文案(docs/model-efficiency-plan.md §1.4、D5):执行层拒绝与尾部提醒共用,
|
|
3
|
+
* 发给模型,两种界面语言下逐字节相同。[ME-C0]
|
|
4
|
+
* [ME-B] 两条提醒由 `ai/context.ts` 渲染(ai 不 import agent),定义移到那里、这里再导出。
|
|
5
|
+
*/
|
|
6
|
+
export { toolRemovedReminder, toolRestoredReminder } from "../ai/context.js";
|
|
7
|
+
/** 执行层拒绝:工具不在本会话可用(未注册,或在 `unavailableTools` 里)。 */
|
|
8
|
+
export declare const toolUnavailableText: (name: string) => string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具可用性的固定英文文案(docs/model-efficiency-plan.md §1.4、D5):执行层拒绝与尾部提醒共用,
|
|
3
|
+
* 发给模型,两种界面语言下逐字节相同。[ME-C0]
|
|
4
|
+
* [ME-B] 两条提醒由 `ai/context.ts` 渲染(ai 不 import agent),定义移到那里、这里再导出。
|
|
5
|
+
*/
|
|
6
|
+
export { toolRemovedReminder, toolRestoredReminder } from "../ai/context.js";
|
|
7
|
+
/** 执行层拒绝:工具不在本会话可用(未注册,或在 `unavailableTools` 里)。 */
|
|
8
|
+
export const toolUnavailableText = (name) => `Tool "${name}" is not available in this session.`;
|
|
@@ -27,6 +27,7 @@ import { formatSchemaErrors, validateSchema } from "./schema.js";
|
|
|
27
27
|
import { CODEMODE_TOOL } from "../tools/presets.js";
|
|
28
28
|
import { executionModeOf } from "../tools/registry.js";
|
|
29
29
|
import { truncateMiddle } from "../tools/truncate.js";
|
|
30
|
+
import { toolUnavailableText } from "./tool-availability.js";
|
|
30
31
|
export const ABORTED_TOOL_TEXT = "aborted by user";
|
|
31
32
|
export const DEFAULT_MAX_TOOL_RESULT_CHARS = 30_000;
|
|
32
33
|
export const DEFAULT_ABORT_GRACE_MS = 3000;
|
|
@@ -63,7 +64,7 @@ function notFoundText(name, options) {
|
|
|
63
64
|
if (options.getNestedTool?.(name) !== undefined && options.getTool(CODEMODE_TOOL) !== undefined) {
|
|
64
65
|
return `Tool ${name} is only callable inside a codemode script: tools.${name}({...})`;
|
|
65
66
|
}
|
|
66
|
-
return
|
|
67
|
+
return toolUnavailableText(name);
|
|
67
68
|
}
|
|
68
69
|
async function prepare(call, assistant, options, signal, parent) {
|
|
69
70
|
if (signal.aborted)
|
|
@@ -124,10 +125,14 @@ function resultText(content) {
|
|
|
124
125
|
return content;
|
|
125
126
|
return content.map((block) => (block.type === "text" ? block.text : "")).join("");
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
/** 结果字符上限:嵌套调用 1 MB,否则会话的 `maxToolResultChars`。 */
|
|
129
|
+
function resultLimit(options, nested) {
|
|
130
|
+
return nested
|
|
129
131
|
? NESTED_MAX_RESULT_CHARS
|
|
130
132
|
: (options.maxToolResultChars ?? DEFAULT_MAX_TOOL_RESULT_CHARS);
|
|
133
|
+
}
|
|
134
|
+
function truncateResult(call, result, options, nested = false) {
|
|
135
|
+
const limit = resultLimit(options, nested);
|
|
131
136
|
const text = resultText(result.content);
|
|
132
137
|
if (text.length <= limit)
|
|
133
138
|
return result;
|
|
@@ -174,7 +179,11 @@ async function execute(prepared, options, signal, emit, parent) {
|
|
|
174
179
|
});
|
|
175
180
|
let result;
|
|
176
181
|
try {
|
|
177
|
-
const
|
|
182
|
+
const created = options.createToolContext(call, signal, onUpdate);
|
|
183
|
+
// [ME-D] 工具按会话结果上限一次截到位(D11);宿主自己给了就不覆盖
|
|
184
|
+
const ctx = created.maxResultChars !== undefined
|
|
185
|
+
? created
|
|
186
|
+
: { ...created, maxResultChars: resultLimit(options, parent !== undefined) };
|
|
178
187
|
const run = tool.execute(input, ctx).then((value) => value, (error) => errorResult(signal.aborted
|
|
179
188
|
? ABORTED_TOOL_TEXT
|
|
180
189
|
: error instanceof Error
|
package/dist/agent/worktree.d.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 非 git 目录直接报错,不回落到共享目录;`.ama/worktrees/.gitignore` 写 `*`,父会话的 git 与
|
|
9
9
|
* grep / glob(读嵌套 .gitignore)都看不到这些目录;
|
|
10
10
|
* - worktree 不保证可运行:依赖(node_modules 等)不共享。
|
|
11
|
+
* - 并发:同一仓库的 `worktree add / remove` 与删分支会扫描、改写 `.git/worktrees/*`,并行的两个隔离
|
|
12
|
+
* 任务曾互相读到对方建了一半的管理目录(`无法读取 .git/worktrees/<id>/commondir`)。同进程内按仓库的
|
|
13
|
+
* 公共 git 目录串行;跨进程(两个 ama 同时开隔离任务)的同类瞬时错误短暂重试。
|
|
11
14
|
*/
|
|
12
15
|
export interface Worktree {
|
|
13
16
|
/** worktree 根目录。 */
|
|
@@ -29,6 +32,11 @@ export interface WorktreeOutcome {
|
|
|
29
32
|
}
|
|
30
33
|
export type GitRunner = (args: readonly string[], cwd: string) => Promise<string>;
|
|
31
34
|
export declare const runGit: GitRunner;
|
|
35
|
+
export declare function serialized<T>(key: string, run: () => Promise<T>): Promise<T>;
|
|
36
|
+
export declare function retryTransient<T>(run: () => Promise<T>, options?: {
|
|
37
|
+
attempts?: number;
|
|
38
|
+
delayMs?: number;
|
|
39
|
+
}): Promise<T>;
|
|
32
40
|
export declare function worktreeBranch(taskId: string): string;
|
|
33
41
|
/** 在父 cwd 所在仓库里为任务建 worktree;非 git 目录抛错。 */
|
|
34
42
|
export declare function createWorktree(cwd: string, taskId: string, git?: GitRunner): Promise<Worktree>;
|
package/dist/agent/worktree.js
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* - 非 git 目录直接报错,不回落到共享目录;`.ama/worktrees/.gitignore` 写 `*`,父会话的 git 与
|
|
9
9
|
* grep / glob(读嵌套 .gitignore)都看不到这些目录;
|
|
10
10
|
* - worktree 不保证可运行:依赖(node_modules 等)不共享。
|
|
11
|
+
* - 并发:同一仓库的 `worktree add / remove` 与删分支会扫描、改写 `.git/worktrees/*`,并行的两个隔离
|
|
12
|
+
* 任务曾互相读到对方建了一半的管理目录(`无法读取 .git/worktrees/<id>/commondir`)。同进程内按仓库的
|
|
13
|
+
* 公共 git 目录串行;跨进程(两个 ama 同时开隔离任务)的同类瞬时错误短暂重试。
|
|
11
14
|
*/
|
|
12
15
|
import { execFile } from "node:child_process";
|
|
13
16
|
import { existsSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { join, relative } from "node:path";
|
|
17
|
+
import { join, relative, resolve } from "node:path";
|
|
15
18
|
const TIMEOUT_MS = 30_000;
|
|
16
19
|
export const runGit = (args, cwd) => new Promise((resolve, reject) => {
|
|
17
20
|
execFile("git", [...args], { cwd, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
@@ -33,6 +36,48 @@ function realPath(path) {
|
|
|
33
36
|
return path;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
/** 每个仓库(按公共 git 目录)一条队列:worktree 的增删与删分支串行执行。 */
|
|
40
|
+
const queues = new Map();
|
|
41
|
+
export function serialized(key, run) {
|
|
42
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
43
|
+
const current = previous.then(run, run);
|
|
44
|
+
const tail = current.then(() => undefined, () => undefined);
|
|
45
|
+
queues.set(key, tail);
|
|
46
|
+
void tail.then(() => {
|
|
47
|
+
if (queues.get(key) === tail)
|
|
48
|
+
queues.delete(key);
|
|
49
|
+
});
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
/** 另一个进程正在增删 worktree 时 git 报的瞬时错误(读到半建的管理目录、锁文件已存在)。 */
|
|
53
|
+
const TRANSIENT = /commondir|gitdir|could not read|unable to read|无法读取|\.lock'?: File exists|Unable to create '.*\.lock'|无法创建 '.*\.lock'/i;
|
|
54
|
+
export async function retryTransient(run, options = {}) {
|
|
55
|
+
const attempts = options.attempts ?? 5;
|
|
56
|
+
const delayMs = options.delayMs ?? 50;
|
|
57
|
+
for (let n = 1;; n++) {
|
|
58
|
+
try {
|
|
59
|
+
return await run();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (n >= attempts || !TRANSIENT.test(error instanceof Error ? error.message : String(error)))
|
|
63
|
+
throw error;
|
|
64
|
+
await new Promise((r) => setTimeout(r, delayMs * n));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 仓库的公共 git 目录(所有 worktree 共享),作为串行队列的键。 */
|
|
69
|
+
async function commonDir(repo, git) {
|
|
70
|
+
try {
|
|
71
|
+
return realPath(resolve(repo, (await git(["rev-parse", "--git-common-dir"], repo)).trim()));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return repo;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** 串行 + 瞬时错误重试地跑一条会改动 `.git/worktrees` 的 git 命令。 */
|
|
78
|
+
function mutate(key, git, args, cwd) {
|
|
79
|
+
return serialized(key, () => retryTransient(() => git(args, cwd)));
|
|
80
|
+
}
|
|
36
81
|
export function worktreeBranch(taskId) {
|
|
37
82
|
return `ama/task-${taskId}`;
|
|
38
83
|
}
|
|
@@ -53,8 +98,9 @@ export async function createWorktree(cwd, taskId, git = runGit) {
|
|
|
53
98
|
writeFileSync(ignore, "*\n");
|
|
54
99
|
const path = join(root, taskId);
|
|
55
100
|
const branch = worktreeBranch(taskId);
|
|
101
|
+
const key = await commonDir(repo, git);
|
|
56
102
|
if (!existsSync(path))
|
|
57
|
-
await git
|
|
103
|
+
await mutate(key, git, ["worktree", "add", "-b", branch, path, base], repo);
|
|
58
104
|
const sub = relative(repo, realPath(cwd));
|
|
59
105
|
const childCwd = sub === "" || sub.startsWith("..") ? path : join(path, sub);
|
|
60
106
|
mkdirSync(childCwd, { recursive: true });
|
|
@@ -65,8 +111,9 @@ export async function finishWorktree(tree, git = runGit) {
|
|
|
65
111
|
const status = (await git(["status", "--porcelain"], tree.path)).trim();
|
|
66
112
|
const head = (await git(["rev-parse", "HEAD"], tree.path)).trim();
|
|
67
113
|
if (status === "" && head === tree.base) {
|
|
68
|
-
|
|
69
|
-
await git
|
|
114
|
+
const key = await commonDir(tree.repo, git);
|
|
115
|
+
await mutate(key, git, ["worktree", "remove", "--force", tree.path], tree.repo);
|
|
116
|
+
await mutate(key, git, ["branch", "-D", tree.branch], tree.repo).catch(() => "");
|
|
70
117
|
return { branch: tree.branch, changed: false };
|
|
71
118
|
}
|
|
72
119
|
const stat = (await git(["diff", "--stat", tree.base], tree.path).catch(() => "")).trim();
|
package/dist/agents/parse.js
CHANGED
|
@@ -88,8 +88,11 @@ export function parseAgentDefinition(text, filePath, source) {
|
|
|
88
88
|
const background = data["background"];
|
|
89
89
|
if (background !== undefined && typeof background !== "boolean")
|
|
90
90
|
return fail("background must be true or false");
|
|
91
|
+
const context = data["context"];
|
|
92
|
+
if (context !== undefined && context !== null && context !== "fork" && context !== "fresh")
|
|
93
|
+
return fail("context must be fork or fresh");
|
|
91
94
|
if (runner !== "ama") {
|
|
92
|
-
const ignored = ["tools", "disallowed-tools", "permission-mode"].filter((key) => data[key] !== undefined);
|
|
95
|
+
const ignored = ["tools", "disallowed-tools", "permission-mode", "context"].filter((key) => data[key] !== undefined);
|
|
93
96
|
if (ignored.length > 0)
|
|
94
97
|
warnings.push(`${filePath}: ${ignored.join(", ")} ignored for external runner ${runner} (it keeps its own policy)`);
|
|
95
98
|
}
|
|
@@ -113,5 +116,7 @@ export function parseAgentDefinition(text, filePath, source) {
|
|
|
113
116
|
agent.disallowedTools = disallowedTools;
|
|
114
117
|
if (typeof thinking === "string")
|
|
115
118
|
agent.thinking = thinking;
|
|
119
|
+
if (context === "fork" || context === "fresh")
|
|
120
|
+
agent.context = context;
|
|
116
121
|
return { agent, warnings };
|
|
117
122
|
}
|
|
@@ -171,6 +171,9 @@ export function applyRunnerEvent(record, event, sink) {
|
|
|
171
171
|
case "notice":
|
|
172
172
|
sink.log(event.level, `[task ${taskId}] ${event.text}`);
|
|
173
173
|
return;
|
|
174
|
+
case "context":
|
|
175
|
+
record.info.context = event.mode;
|
|
176
|
+
return;
|
|
174
177
|
case "turn_trace":
|
|
175
178
|
sink.appendTrace?.({
|
|
176
179
|
kind: "external_turn",
|
package/dist/agents/types.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface AgentDefinition {
|
|
|
31
31
|
*/
|
|
32
32
|
background?: boolean;
|
|
33
33
|
runner: AgentRunnerSpec;
|
|
34
|
+
/** [ME-C0] frontmatter `context:`:`fork` 继承父会话已完成的回合;缺省 `fresh`。 */
|
|
35
|
+
context?: "fork" | "fresh";
|
|
34
36
|
/** 正文:追加到子会话系统提示末尾的角色说明。 */
|
|
35
37
|
prompt: string;
|
|
36
38
|
source: AgentSource;
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* 任一出现过(含 0)即 `cacheReported: true`,之后的事件不会把它改回 false。
|
|
13
13
|
*/
|
|
14
14
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
15
|
-
import { authHeaders, describeErrorJson, idleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
|
|
15
|
+
import { authHeaders, describeErrorJson, idleTimeoutOf, streamIdleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
|
|
16
16
|
import { readSseEvents } from "../sse.js";
|
|
17
|
-
import {
|
|
17
|
+
import { clampRequestMaxTokens, postWithMaxTokensFallback } from "./max-tokens.js";
|
|
18
18
|
import { ANTHROPIC_VERSION, anthropicMessagesUrl, buildAnthropicRequest, detectAnthropicCompat, } from "./anthropic-request.js";
|
|
19
19
|
import { BlockTracker, STREAM_ENDED_MESSAGE, ProviderStopError, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
|
|
20
20
|
function num(value) {
|
|
@@ -160,20 +160,25 @@ async function run(stream, model, context, options) {
|
|
|
160
160
|
if (request.providerThinkingLevel !== undefined) {
|
|
161
161
|
tracker.output.providerThinkingLevel = request.providerThinkingLevel;
|
|
162
162
|
}
|
|
163
|
+
// 预算型思考的预算由 max_tokens 推导,收紧会让消息缓存失效:不主动收紧(D9)
|
|
164
|
+
const budgeted = obj(request.body["thinking"])?.["type"] === "enabled";
|
|
165
|
+
clampRequestMaxTokens(request.body, "max_tokens", model.contextWindow, budgeted);
|
|
163
166
|
const replaced = options.onPayload?.(request.body);
|
|
164
167
|
const body = replaced === undefined ? request.body : replaced;
|
|
165
168
|
const baseUrl = model.baseUrl ?? "https://api.anthropic.com";
|
|
166
|
-
const
|
|
169
|
+
const url = anthropicMessagesUrl(baseUrl);
|
|
170
|
+
const post = {
|
|
167
171
|
headers: buildHeaders(model, options, request.betas),
|
|
168
172
|
body,
|
|
169
173
|
signal: options.signal,
|
|
170
174
|
timeoutMs: options.timeoutMs,
|
|
171
175
|
idleTimeoutMs: idleTimeoutOf(options),
|
|
172
176
|
onResponse: options.onResponse,
|
|
173
|
-
}
|
|
177
|
+
};
|
|
178
|
+
const response = await postWithMaxTokensFallback(model, url, post, "max_tokens");
|
|
174
179
|
stream.push({ type: "start", partial: tracker.output });
|
|
175
180
|
const state = { blocks: new Map(), stop: undefined, sawMessageStop: false };
|
|
176
|
-
for await (const sse of readSseEvents(response.body, options.signal,
|
|
181
|
+
for await (const sse of readSseEvents(response.body, options.signal, streamIdleTimeoutOf(options))) {
|
|
177
182
|
if (sse.event === "ping" || sse.data === "")
|
|
178
183
|
continue;
|
|
179
184
|
let data;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* `ttl: "1h"`,仅 `supportsLongCacheRetention`——官方端点缺省开,中转缺省关、降为 short;最后做 TTL
|
|
8
8
|
* 顺序校验,5m 之后出现 1h 则全部降为 5m)按优先级取前
|
|
9
9
|
* `maxCacheBreakpoints` 个:① 最后一条 user 消息(含工具结果)的最后一个块 ② system 末块
|
|
10
|
-
* ③
|
|
10
|
+
* ③ 倒数第二条 user 消息的末块(= 上一次请求的写入点;并行工具结果多时最后一条离上次写入点可能超出
|
|
11
|
+
* 回看窗口)④ 最后一个工具定义(`supportsCacheControlOnTools`)。[ME-C] D7
|
|
11
12
|
*
|
|
12
13
|
* thinking:`adaptiveThinking` 模型发 `{type:"adaptive"}` + `output_config.effort`;
|
|
13
14
|
* 其余推理模型按预算发 `{type:"enabled", budget_tokens}`,预算计入 max_tokens(有工具时带交错思考 beta 头,
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* `ttl: "1h"`,仅 `supportsLongCacheRetention`——官方端点缺省开,中转缺省关、降为 short;最后做 TTL
|
|
8
8
|
* 顺序校验,5m 之后出现 1h 则全部降为 5m)按优先级取前
|
|
9
9
|
* `maxCacheBreakpoints` 个:① 最后一条 user 消息(含工具结果)的最后一个块 ② system 末块
|
|
10
|
-
* ③
|
|
10
|
+
* ③ 倒数第二条 user 消息的末块(= 上一次请求的写入点;并行工具结果多时最后一条离上次写入点可能超出
|
|
11
|
+
* 回看窗口)④ 最后一个工具定义(`supportsCacheControlOnTools`)。[ME-C] D7
|
|
11
12
|
*
|
|
12
13
|
* thinking:`adaptiveThinking` 模型发 `{type:"adaptive"}` + `output_config.effort`;
|
|
13
14
|
* 其余推理模型按预算发 `{type:"enabled", budget_tokens}`,预算计入 max_tokens(有工具时带交错思考 beta 头,
|
|
@@ -161,12 +162,15 @@ function convertTools(tools) {
|
|
|
161
162
|
input_schema: { ...tool.parameters, type: "object" },
|
|
162
163
|
}));
|
|
163
164
|
}
|
|
164
|
-
/**
|
|
165
|
-
function markLastUser(messages, cacheControl) {
|
|
165
|
+
/** 在从后数第 `skip + 1` 条 user 消息的最后一个块上打断点;返回是否打上。 */
|
|
166
|
+
function markLastUser(messages, cacheControl, skip = 0) {
|
|
167
|
+
let remaining = skip;
|
|
166
168
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
167
169
|
const message = messages[i];
|
|
168
170
|
if (!message || message["role"] !== "user")
|
|
169
171
|
continue;
|
|
172
|
+
if (remaining-- > 0)
|
|
173
|
+
continue;
|
|
170
174
|
const content = message["content"];
|
|
171
175
|
if (typeof content === "string") {
|
|
172
176
|
message["content"] = [{ type: "text", text: content, cache_control: cacheControl }];
|
|
@@ -240,6 +244,8 @@ export function buildAnthropicRequest(model, context, options) {
|
|
|
240
244
|
lastSystem["cache_control"] = cacheControl;
|
|
241
245
|
budget--;
|
|
242
246
|
}
|
|
247
|
+
if (cacheControl && budget > 0 && markLastUser(messages, cacheControl, 1))
|
|
248
|
+
budget--;
|
|
243
249
|
const lastTool = tools[tools.length - 1];
|
|
244
250
|
if (cacheControl && budget > 0 && lastTool && compat.supportsCacheControlOnTools) {
|
|
245
251
|
lastTool["cache_control"] = cacheControl;
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* - 错误体 `{error:{code,status,message}}` 重排为 `429 RESOURCE_EXHAUSTED: …`。
|
|
17
17
|
*/
|
|
18
18
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
19
|
-
import { authHeaders, HttpError, idleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
|
|
19
|
+
import { authHeaders, HttpError, idleTimeoutOf, streamIdleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
|
|
20
20
|
import { readSseEvents } from "../sse.js";
|
|
21
21
|
import { GOOGLE_BASE_URL, buildGoogleRequest, detectGoogleCompat } from "./google-request.js";
|
|
22
|
+
import { clampGoogleMaxTokens } from "./max-tokens.js";
|
|
22
23
|
import { BlockTracker, ProviderStopError, STREAM_ENDED_MESSAGE, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
|
|
23
24
|
function num(value) {
|
|
24
25
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
@@ -188,6 +189,7 @@ async function run(stream, model, context, options) {
|
|
|
188
189
|
if (request.providerThinkingLevel !== undefined) {
|
|
189
190
|
tracker.output.providerThinkingLevel = request.providerThinkingLevel;
|
|
190
191
|
}
|
|
192
|
+
clampGoogleMaxTokens(request.body, model.contextWindow);
|
|
191
193
|
const replaced = options.onPayload?.(request.body);
|
|
192
194
|
let response;
|
|
193
195
|
try {
|
|
@@ -210,7 +212,7 @@ async function run(stream, model, context, options) {
|
|
|
210
212
|
idSeed: Date.now().toString(36),
|
|
211
213
|
};
|
|
212
214
|
const body = response.body;
|
|
213
|
-
for await (const sse of readSseEvents(body, options.signal,
|
|
215
|
+
for await (const sse of readSseEvents(body, options.signal, streamIdleTimeoutOf(options))) {
|
|
214
216
|
const data = sse.data.trim();
|
|
215
217
|
if (data === "" || data === "[DONE]")
|
|
216
218
|
continue;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `max_tokens` 的主动收紧与被动修正(docs/model-efficiency-plan.md §1.9、D9)。[ME-C]
|
|
3
|
+
*
|
|
4
|
+
* - 主动收紧:窗口已知时 `max_tokens = min(请求值, max(MIN_OUTPUT_TOKENS, 窗口 − 估算输入 − 余量))`,
|
|
5
|
+
* 输入按请求体字符 / 4 估算(本模块自带,不依赖 compaction/)。预算型思考(`fixed`)不收紧:
|
|
6
|
+
* 预算由 max_tokens 推导,改它会让消息缓存失效。
|
|
7
|
+
* - 被动修正:端点以 400 拒收并说出上限(「max_tokens 范围」类文案)时,记入进程级 `maxTokensCaps`、
|
|
8
|
+
* 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
|
|
9
|
+
* 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
|
|
10
|
+
* 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
|
|
11
|
+
*/
|
|
12
|
+
import { type PostOptions } from "../http.js";
|
|
13
|
+
import type { Model } from "../types.js";
|
|
14
|
+
/** 主动收紧的下限:再小就不如让请求原样发出、由被动修正或溢出处理接手。 */
|
|
15
|
+
export declare const MIN_OUTPUT_TOKENS = 1024;
|
|
16
|
+
/** 窗口里给估算误差留的余量。 */
|
|
17
|
+
export declare const OUTPUT_HEADROOM_TOKENS = 2048;
|
|
18
|
+
/** 请求体里放 max_tokens 的字段名(三条协议各不相同)。 */
|
|
19
|
+
export type MaxTokensField = "max_tokens" | "max_completion_tokens" | "max_output_tokens";
|
|
20
|
+
/** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
|
|
21
|
+
export declare const maxTokensCaps: Map<string, number>;
|
|
22
|
+
type Json = Record<string, unknown>;
|
|
23
|
+
/** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
|
|
24
|
+
export declare function clampMaxTokens(requested: number, window: number | undefined, estimatedInput: number, fixed: boolean): number;
|
|
25
|
+
/** 请求体的输入 token 估算:JSON 字符数 / 4(CJK 偏低,由被动修正兜底)。 */
|
|
26
|
+
export declare function estimateInputTokens(body: unknown): number;
|
|
27
|
+
/**
|
|
28
|
+
* 就地收紧 `holder[field]`(缺省 holder = body;Google 的 `generationConfig.maxOutputTokens`
|
|
29
|
+
* 传 holder)。字段不是数字时不动。
|
|
30
|
+
*/
|
|
31
|
+
export declare function clampRequestMaxTokens(body: Json, field: string, window: number | undefined, fixed?: boolean, holder?: Json): void;
|
|
32
|
+
/** Google `generationConfig.maxOutputTokens`;带正的思考预算时不收紧(预算按回答上限留白算出)。 */
|
|
33
|
+
export declare function clampGoogleMaxTokens(body: Json, window: number | undefined): void;
|
|
34
|
+
/** 400 文案里的上限;`{ cap }` 可重发,`{ overflow: true }` 判溢出,undefined 不是本类错误。 */
|
|
35
|
+
export declare function parseMaxTokensRejection(error: unknown): {
|
|
36
|
+
cap: number;
|
|
37
|
+
} | {
|
|
38
|
+
overflow: true;
|
|
39
|
+
} | undefined;
|
|
40
|
+
/** `postWithCacheFallback` 之外再包一层:max_tokens 超上限的 400 以收紧值重发一次(只在 `start` 之前)。 */
|
|
41
|
+
export declare function postWithMaxTokensFallback(model: Pick<Model, "provider" | "id">, url: string, options: PostOptions, field: MaxTokensField): Promise<Response>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `max_tokens` 的主动收紧与被动修正(docs/model-efficiency-plan.md §1.9、D9)。[ME-C]
|
|
3
|
+
*
|
|
4
|
+
* - 主动收紧:窗口已知时 `max_tokens = min(请求值, max(MIN_OUTPUT_TOKENS, 窗口 − 估算输入 − 余量))`,
|
|
5
|
+
* 输入按请求体字符 / 4 估算(本模块自带,不依赖 compaction/)。预算型思考(`fixed`)不收紧:
|
|
6
|
+
* 预算由 max_tokens 推导,改它会让消息缓存失效。
|
|
7
|
+
* - 被动修正:端点以 400 拒收并说出上限(「max_tokens 范围」类文案)时,记入进程级 `maxTokensCaps`、
|
|
8
|
+
* 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
|
|
9
|
+
* 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
|
|
10
|
+
* 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
|
|
11
|
+
*/
|
|
12
|
+
import { HttpError, errorText } from "../http.js";
|
|
13
|
+
import { postWithCacheFallback } from "./cache-params.js";
|
|
14
|
+
/** 主动收紧的下限:再小就不如让请求原样发出、由被动修正或溢出处理接手。 */
|
|
15
|
+
export const MIN_OUTPUT_TOKENS = 1024;
|
|
16
|
+
/** 窗口里给估算误差留的余量。 */
|
|
17
|
+
export const OUTPUT_HEADROOM_TOKENS = 2048;
|
|
18
|
+
/** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
|
|
19
|
+
export const maxTokensCaps = new Map();
|
|
20
|
+
/** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
|
|
21
|
+
export function clampMaxTokens(requested, window, estimatedInput, fixed) {
|
|
22
|
+
if (fixed || window === undefined || !Number.isFinite(window) || window <= 0)
|
|
23
|
+
return requested;
|
|
24
|
+
const room = Math.floor(window - estimatedInput - OUTPUT_HEADROOM_TOKENS);
|
|
25
|
+
return Math.min(requested, Math.max(MIN_OUTPUT_TOKENS, room));
|
|
26
|
+
}
|
|
27
|
+
/** 请求体的输入 token 估算:JSON 字符数 / 4(CJK 偏低,由被动修正兜底)。 */
|
|
28
|
+
export function estimateInputTokens(body) {
|
|
29
|
+
try {
|
|
30
|
+
return Math.ceil(JSON.stringify(body).length / 4);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 就地收紧 `holder[field]`(缺省 holder = body;Google 的 `generationConfig.maxOutputTokens`
|
|
38
|
+
* 传 holder)。字段不是数字时不动。
|
|
39
|
+
*/
|
|
40
|
+
export function clampRequestMaxTokens(body, field, window, fixed = false, holder = body) {
|
|
41
|
+
const requested = holder[field];
|
|
42
|
+
if (typeof requested !== "number" || fixed || window === undefined)
|
|
43
|
+
return;
|
|
44
|
+
holder[field] = clampMaxTokens(requested, window, estimateInputTokens(body), false);
|
|
45
|
+
}
|
|
46
|
+
/** Google `generationConfig.maxOutputTokens`;带正的思考预算时不收紧(预算按回答上限留白算出)。 */
|
|
47
|
+
export function clampGoogleMaxTokens(body, window) {
|
|
48
|
+
const config = body["generationConfig"];
|
|
49
|
+
if (typeof config !== "object" || config === null)
|
|
50
|
+
return;
|
|
51
|
+
const thinking = config["thinkingConfig"];
|
|
52
|
+
const budget = typeof thinking === "object" && thinking !== null
|
|
53
|
+
? thinking["thinkingBudget"]
|
|
54
|
+
: undefined;
|
|
55
|
+
const fixed = typeof budget === "number" && budget > 0;
|
|
56
|
+
clampRequestMaxTokens(body, "maxOutputTokens", window, fixed, config);
|
|
57
|
+
}
|
|
58
|
+
const NUM = "`?(\\d[\\d,_]*)`?";
|
|
59
|
+
const toInt = (text) => Number((text ?? "").replace(/[,_]/g, ""));
|
|
60
|
+
const RANGE = new RegExp(`range of \`?max_\\w*tokens\`?\\s*(?:should be|is|must be)?\\s*[[(]\\s*\\d+\\s*,\\s*${NUM}\\s*[\\])]`, "i");
|
|
61
|
+
const LIMIT = new RegExp(`max_\\w*tokens\`?[^\\n]{0,200}?(?:must be|should be|less than or equal to|at most|<=)\\s*${NUM}`, "i");
|
|
62
|
+
const CONTEXT_LIMIT = /input length and `?max_tokens`? exceed context limit:?\s*(\d+)\s*\+\s*(\d+)\s*>\s*(\d+)/i;
|
|
63
|
+
function classify(error) {
|
|
64
|
+
if (error instanceof HttpError && error.status !== 400 && error.status !== 422)
|
|
65
|
+
return undefined;
|
|
66
|
+
const text = errorText(error);
|
|
67
|
+
const context = CONTEXT_LIMIT.exec(text);
|
|
68
|
+
if (context) {
|
|
69
|
+
const cap = toInt(context[3]) - toInt(context[1]);
|
|
70
|
+
return cap < MIN_OUTPUT_TOKENS ? { overflow: true } : { cap, transient: true };
|
|
71
|
+
}
|
|
72
|
+
const match = RANGE.exec(text) ?? LIMIT.exec(text);
|
|
73
|
+
if (!match)
|
|
74
|
+
return undefined;
|
|
75
|
+
const cap = toInt(match[1]);
|
|
76
|
+
return Number.isFinite(cap) && cap >= 1 ? { cap, transient: false } : undefined;
|
|
77
|
+
}
|
|
78
|
+
/** 400 文案里的上限;`{ cap }` 可重发,`{ overflow: true }` 判溢出,undefined 不是本类错误。 */
|
|
79
|
+
export function parseMaxTokensRejection(error) {
|
|
80
|
+
const rejection = classify(error);
|
|
81
|
+
return rejection === undefined || "overflow" in rejection ? rejection : { cap: rejection.cap };
|
|
82
|
+
}
|
|
83
|
+
/** body[field] 超过 cap 时返回改小后的浅拷贝,否则原样返回。 */
|
|
84
|
+
function withCap(body, field, cap) {
|
|
85
|
+
if (cap === undefined || typeof body !== "object" || body === null)
|
|
86
|
+
return body;
|
|
87
|
+
const value = body[field];
|
|
88
|
+
return typeof value === "number" && value > cap ? { ...body, [field]: cap } : body;
|
|
89
|
+
}
|
|
90
|
+
/** Anthropic 预算型思考:budget_tokens 必须小于 max_tokens,上限压不下时不重发。 */
|
|
91
|
+
function budgetFits(body, cap) {
|
|
92
|
+
const thinking = body["thinking"];
|
|
93
|
+
if (typeof thinking !== "object" || thinking === null)
|
|
94
|
+
return true;
|
|
95
|
+
const budget = thinking["budget_tokens"];
|
|
96
|
+
return typeof budget !== "number" || budget < cap;
|
|
97
|
+
}
|
|
98
|
+
/** `postWithCacheFallback` 之外再包一层:max_tokens 超上限的 400 以收紧值重发一次(只在 `start` 之前)。 */
|
|
99
|
+
export async function postWithMaxTokensFallback(model, url, options, field) {
|
|
100
|
+
const key = `${model.provider}/${model.id}`;
|
|
101
|
+
const body = withCap(options.body, field, maxTokensCaps.get(key));
|
|
102
|
+
try {
|
|
103
|
+
return await postWithCacheFallback(model, url, { ...options, body });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (options.signal.aborted || typeof body !== "object" || body === null)
|
|
107
|
+
throw error;
|
|
108
|
+
const rejection = classify(error);
|
|
109
|
+
const sent = body[field];
|
|
110
|
+
if (rejection === undefined || !("cap" in rejection) || typeof sent !== "number")
|
|
111
|
+
throw error;
|
|
112
|
+
if (rejection.cap >= sent || !budgetFits(body, rejection.cap))
|
|
113
|
+
throw error;
|
|
114
|
+
if (!rejection.transient)
|
|
115
|
+
maxTokensCaps.set(key, rejection.cap);
|
|
116
|
+
return postWithCacheFallback(model, url, {
|
|
117
|
+
...options,
|
|
118
|
+
body: withCap(body, field, rejection.cap),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|