@armadra/agent 0.7.0 → 0.7.2
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 +65 -0
- package/CHANGELOG.zh-CN.md +41 -0
- package/dist/agent/session-cache.d.ts +4 -2
- package/dist/agent/session-cache.js +5 -3
- package/dist/agent/session-compaction.d.ts +13 -1
- package/dist/agent/session-compaction.js +36 -0
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-state.d.ts +9 -3
- package/dist/agent/session-state.js +8 -6
- package/dist/agent/session.js +3 -3
- package/dist/agent/types-w5.d.ts +3 -0
- package/dist/agent/types.d.ts +18 -0
- package/dist/agents/task-record.d.ts +4 -0
- package/dist/agents/task-record.js +18 -0
- package/dist/ai/context.d.ts +7 -2
- package/dist/ai/context.js +45 -7
- package/dist/ai/providers/catalog-data.js +1 -1
- package/dist/auth/oauth/token-store.d.ts +2 -0
- package/dist/auth/oauth/token-store.js +7 -2
- package/dist/bundle/ama.cjs +1684 -490
- package/dist/checkpoints/shadow-git.js +2 -1
- package/dist/compaction/breakdown.d.ts +51 -0
- package/dist/compaction/breakdown.js +159 -0
- package/dist/compaction/estimate.d.ts +6 -0
- package/dist/compaction/estimate.js +1 -1
- package/dist/compaction/prefix-estimate.d.ts +23 -0
- package/dist/compaction/prefix-estimate.js +34 -0
- package/dist/config/auth-file.js +10 -2
- package/dist/config/fs-retry.d.ts +18 -0
- package/dist/config/fs-retry.js +33 -0
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/drivers/native/codex-app-server.js +6 -0
- package/dist/drivers/runner.js +17 -0
- package/dist/drivers/store.d.ts +3 -0
- package/dist/drivers/store.js +4 -0
- package/dist/i18n/catalog.d.ts +53 -0
- package/dist/i18n/messages/agents.d.ts +3 -0
- package/dist/i18n/messages/agents.js +3 -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 +6 -0
- package/dist/i18n/messages/interactive.js +6 -0
- package/dist/i18n/messages/panels.d.ts +3 -0
- package/dist/i18n/messages/panels.js +5 -2
- package/dist/i18n/messages/report-context.d.ts +94 -0
- package/dist/i18n/messages/report-context.js +95 -0
- package/dist/i18n/messages/report.d.ts +96 -0
- package/dist/i18n/messages/report.js +13 -2
- package/dist/i18n/messages/settings.d.ts +2 -0
- package/dist/i18n/messages/settings.js +2 -0
- package/dist/i18n/messages/subcommands.js +2 -2
- package/dist/modes/acp/acp-events.d.ts +17 -4
- package/dist/modes/acp/acp-events.js +32 -6
- package/dist/modes/commands-core.js +4 -0
- package/dist/modes/context-report.d.ts +35 -0
- package/dist/modes/context-report.js +172 -0
- package/dist/modes/interactive/agent-bar.d.ts +2 -0
- package/dist/modes/interactive/agent-bar.js +6 -0
- package/dist/modes/interactive/commands.d.ts +2 -1
- package/dist/modes/interactive/commands.js +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +6 -1
- package/dist/modes/interactive/panels.d.ts +2 -1
- package/dist/modes/interactive/panels.js +13 -5
- package/dist/modes/interactive/program-status-ui.d.ts +72 -0
- package/dist/modes/interactive/program-status-ui.js +229 -0
- package/dist/modes/interactive/session-events.d.ts +4 -0
- package/dist/modes/interactive/session-events.js +24 -2
- package/dist/modes/interactive/status-bar.d.ts +21 -3
- package/dist/modes/interactive/status-bar.js +64 -25
- package/dist/modes/interactive/status-ctx.d.ts +55 -0
- package/dist/modes/interactive/status-ctx.js +125 -0
- package/dist/modes/interactive/status-line.d.ts +2 -1
- package/dist/modes/interactive/status-line.js +3 -2
- package/dist/modes/interactive/tasks-report.js +4 -0
- package/dist/modes/print/print-mode.js +12 -0
- package/dist/modes/session-report.d.ts +10 -2
- package/dist/modes/session-report.js +26 -2
- package/dist/sdk.d.ts +1 -1
- package/dist/tools/types.d.ts +6 -0
- package/dist/tui/components/meter.d.ts +2 -0
- package/dist/tui/components/meter.js +1 -1
- package/dist/tui/program-status.d.ts +90 -0
- package/dist/tui/program-status.js +259 -0
- package/docs/acp.md +4 -4
- package/docs/agents.md +1 -1
- package/docs/en/acp.md +4 -4
- package/docs/en/providers.md +5 -3
- package/docs/en/rpc.md +3 -0
- package/docs/en/sessions.md +24 -1
- package/docs/en/tui.md +79 -22
- package/docs/providers.md +8 -5
- package/docs/rpc.md +1 -0
- package/docs/session-format.md +3 -3
- package/docs/sessions.md +23 -1
- package/docs/tui-design.md +2 -2
- package/docs/tui.md +79 -22
- package/package.json +1 -1
|
@@ -204,8 +204,9 @@ export class ShadowRepo {
|
|
|
204
204
|
/** 新回合快照:返回提交 id 与是否该降级。 */
|
|
205
205
|
snapshot(parent, message) {
|
|
206
206
|
return exclusive(this.dir, async () => {
|
|
207
|
-
const started = this.now();
|
|
208
207
|
await this.init();
|
|
208
|
+
// 计时不含一次性的建仓与本地配置(十来个 git 子进程,Windows 上可达数秒),只量快照本身
|
|
209
|
+
const started = this.now();
|
|
209
210
|
await this.writeExcludes();
|
|
210
211
|
if (!this.counted) {
|
|
211
212
|
const out = await this.run([
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文分类明细(`/context`):把投影消息按类别拆开估算。纯函数,只读,不改请求体。
|
|
3
|
+
*
|
|
4
|
+
* 口径与 `estimateMessagesTokens` 一致:每条消息先按 `estimateMessageTokens` 求总量,再把这个总量
|
|
5
|
+
* 分到消息内各部分(文本部分取 `wide + floor(narrow / 4)`,余下的 0–3 个 token 按最大余数补给
|
|
6
|
+
* 字符余数最大的部分;图片每张 `IMAGE_TOKENS`),所以各类别之和恰好等于整组消息的估算。
|
|
7
|
+
*
|
|
8
|
+
* 系统提示与工具声明来自活动分支上的 `system` 消息(首条全量、之后是补丁,补丁也计入——与估算口径
|
|
9
|
+
* 相同):按节名、按工具名汇总;工具表数组的括号与逗号计入工具声明但不归到具体工具。首次请求前
|
|
10
|
+
* 分支上还没有 `system` 消息,这两类为 0(`hasSystem: false`)。
|
|
11
|
+
*
|
|
12
|
+
* 只产出数字与名字(节名、工具名、custom 类型),不带任何正文。
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentMessage } from "../session/types.js";
|
|
15
|
+
export declare const BREAKDOWN_CATEGORIES: readonly ["system", "tools", "user", "assistant", "reasoning", "toolCalls", "toolResults", "images", "summaries", "custom"];
|
|
16
|
+
export type BreakdownCategory = (typeof BREAKDOWN_CATEGORIES)[number];
|
|
17
|
+
export interface BreakdownPart {
|
|
18
|
+
/** 节名 / 工具名 / `compaction`·`branch` / custom 类型 / 图片来源角色。 */
|
|
19
|
+
name: string;
|
|
20
|
+
tokens: number;
|
|
21
|
+
}
|
|
22
|
+
export interface BreakdownEntry {
|
|
23
|
+
category: BreakdownCategory;
|
|
24
|
+
tokens: number;
|
|
25
|
+
/** 计入本类的块数(消息、块、节、图片)。 */
|
|
26
|
+
count: number;
|
|
27
|
+
/** 按名字汇总,从大到小;没有名字的部分(如工具表的括号)只进 `tokens`。 */
|
|
28
|
+
parts: BreakdownPart[];
|
|
29
|
+
}
|
|
30
|
+
export interface ToolResultSize {
|
|
31
|
+
/** 在投影里第几个工具结果(从 1 开始)。 */
|
|
32
|
+
ordinal: number;
|
|
33
|
+
toolName: string;
|
|
34
|
+
tokens: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ContextBreakdown {
|
|
37
|
+
/** 固定顺序(`BREAKDOWN_CATEGORIES`)。 */
|
|
38
|
+
entries: BreakdownEntry[];
|
|
39
|
+
/** = `estimateMessagesTokens(messages)`。 */
|
|
40
|
+
total: number;
|
|
41
|
+
/** 系统提示 + 工具声明。 */
|
|
42
|
+
prefixTokens: number;
|
|
43
|
+
/** 分支上已有 `system` 消息(首次请求后)。 */
|
|
44
|
+
hasSystem: boolean;
|
|
45
|
+
/** 每个工具结果的大小,按出现顺序。 */
|
|
46
|
+
toolResults: ToolResultSize[];
|
|
47
|
+
}
|
|
48
|
+
/** 消息 → 分类明细(各类别之和 = `estimateMessagesTokens(messages)`)。 */
|
|
49
|
+
export declare function breakdownMessages(messages: readonly AgentMessage[]): ContextBreakdown;
|
|
50
|
+
/** 最大的 n 个工具结果(大小相同按出现顺序)。 */
|
|
51
|
+
export declare function largestToolResults(breakdown: ContextBreakdown, n: number): ToolResultSize[];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文分类明细(`/context`):把投影消息按类别拆开估算。纯函数,只读,不改请求体。
|
|
3
|
+
*
|
|
4
|
+
* 口径与 `estimateMessagesTokens` 一致:每条消息先按 `estimateMessageTokens` 求总量,再把这个总量
|
|
5
|
+
* 分到消息内各部分(文本部分取 `wide + floor(narrow / 4)`,余下的 0–3 个 token 按最大余数补给
|
|
6
|
+
* 字符余数最大的部分;图片每张 `IMAGE_TOKENS`),所以各类别之和恰好等于整组消息的估算。
|
|
7
|
+
*
|
|
8
|
+
* 系统提示与工具声明来自活动分支上的 `system` 消息(首条全量、之后是补丁,补丁也计入——与估算口径
|
|
9
|
+
* 相同):按节名、按工具名汇总;工具表数组的括号与逗号计入工具声明但不归到具体工具。首次请求前
|
|
10
|
+
* 分支上还没有 `system` 消息,这两类为 0(`hasSystem: false`)。
|
|
11
|
+
*
|
|
12
|
+
* 只产出数字与名字(节名、工具名、custom 类型),不带任何正文。
|
|
13
|
+
*/
|
|
14
|
+
import { countText, estimateMessageTokens, IMAGE_TOKENS } from "./estimate.js";
|
|
15
|
+
/** 与 estimate.ts 的字符口径相同(非宽字符每 4 个 1 token)。 */
|
|
16
|
+
const CHARS_PER_TOKEN = 4;
|
|
17
|
+
export const BREAKDOWN_CATEGORIES = [
|
|
18
|
+
"system",
|
|
19
|
+
"tools",
|
|
20
|
+
"user",
|
|
21
|
+
"assistant",
|
|
22
|
+
"reasoning",
|
|
23
|
+
"toolCalls",
|
|
24
|
+
"toolResults",
|
|
25
|
+
"images",
|
|
26
|
+
"summaries",
|
|
27
|
+
"custom",
|
|
28
|
+
];
|
|
29
|
+
function textPiece(category, name, text) {
|
|
30
|
+
const count = countText(text, { wide: 0, narrow: 0 });
|
|
31
|
+
return { category, name, wide: count.wide, narrow: count.narrow, fixed: 0 };
|
|
32
|
+
}
|
|
33
|
+
function imagePiece(name) {
|
|
34
|
+
return { category: "images", name, wide: 0, narrow: 0, fixed: IMAGE_TOKENS };
|
|
35
|
+
}
|
|
36
|
+
function contentPieces(content, category, name, imageSource) {
|
|
37
|
+
if (typeof content === "string")
|
|
38
|
+
return [textPiece(category, name, content)];
|
|
39
|
+
return content.map((block) => block.type === "text" ? textPiece(category, name, block.text) : imagePiece(imageSource));
|
|
40
|
+
}
|
|
41
|
+
/** 一条消息拆成的部分(字符合计与 `estimateMessageTokens` 的计数完全相同)。 */
|
|
42
|
+
function messagePieces(message) {
|
|
43
|
+
switch (message.role) {
|
|
44
|
+
case "system": {
|
|
45
|
+
const pieces = Object.entries(message.sections).map(([name, text]) => textPiece("system", name, text ?? ""));
|
|
46
|
+
const tools = message.toolsAdded;
|
|
47
|
+
if (tools !== undefined) {
|
|
48
|
+
for (const tool of tools)
|
|
49
|
+
pieces.push(textPiece("tools", tool.name, JSON.stringify(tool)));
|
|
50
|
+
// JSON.stringify(数组) 比各元素多出 `[`、`]` 与元素间的逗号
|
|
51
|
+
pieces.push({
|
|
52
|
+
category: "tools",
|
|
53
|
+
name: undefined,
|
|
54
|
+
wide: 0,
|
|
55
|
+
narrow: 2 + Math.max(0, tools.length - 1),
|
|
56
|
+
fixed: 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return pieces;
|
|
60
|
+
}
|
|
61
|
+
case "user":
|
|
62
|
+
return contentPieces(message.content, "user", undefined, "user");
|
|
63
|
+
case "toolResult":
|
|
64
|
+
return contentPieces(message.content, "toolResults", message.toolName, "toolResult");
|
|
65
|
+
case "custom":
|
|
66
|
+
return contentPieces(message.content, "custom", message.customType, "custom");
|
|
67
|
+
case "assistant":
|
|
68
|
+
return message.content.map((block) => block.type === "text"
|
|
69
|
+
? textPiece("assistant", undefined, block.text)
|
|
70
|
+
: block.type === "thinking"
|
|
71
|
+
? textPiece("reasoning", undefined, block.thinking)
|
|
72
|
+
: textPiece("toolCalls", block.name, block.name + JSON.stringify(block.arguments)));
|
|
73
|
+
case "compactionSummary":
|
|
74
|
+
return [textPiece("summaries", "compaction", message.summary)];
|
|
75
|
+
case "branchSummary":
|
|
76
|
+
return [textPiece("summaries", "branch", message.summary)];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** 把消息总量分到各部分(最大余数法),返回与 pieces 对齐的 token 数。 */
|
|
80
|
+
function allocate(pieces, total) {
|
|
81
|
+
const tokens = pieces.map((p) => p.fixed + p.wide + Math.floor(p.narrow / CHARS_PER_TOKEN));
|
|
82
|
+
let left = total - tokens.reduce((sum, n) => sum + n, 0);
|
|
83
|
+
const order = pieces
|
|
84
|
+
.map((p, i) => ({ i, rest: p.narrow % CHARS_PER_TOKEN }))
|
|
85
|
+
.filter((x) => x.rest > 0)
|
|
86
|
+
.sort((a, b) => b.rest - a.rest || a.i - b.i);
|
|
87
|
+
for (const { i } of order) {
|
|
88
|
+
if (left <= 0)
|
|
89
|
+
break;
|
|
90
|
+
tokens[i] = (tokens[i] ?? 0) + 1;
|
|
91
|
+
left--;
|
|
92
|
+
}
|
|
93
|
+
// 理论上不会剩;保险起见补到最大的部分,保证合计与估算一致
|
|
94
|
+
if (left !== 0 && tokens.length > 0) {
|
|
95
|
+
let max = 0;
|
|
96
|
+
for (let i = 1; i < tokens.length; i++)
|
|
97
|
+
if ((tokens[i] ?? 0) > (tokens[max] ?? 0))
|
|
98
|
+
max = i;
|
|
99
|
+
tokens[max] = (tokens[max] ?? 0) + left;
|
|
100
|
+
}
|
|
101
|
+
return tokens;
|
|
102
|
+
}
|
|
103
|
+
/** 消息 → 分类明细(各类别之和 = `estimateMessagesTokens(messages)`)。 */
|
|
104
|
+
export function breakdownMessages(messages) {
|
|
105
|
+
const totals = new Map();
|
|
106
|
+
const parts = new Map();
|
|
107
|
+
for (const category of BREAKDOWN_CATEGORIES) {
|
|
108
|
+
totals.set(category, { tokens: 0, count: 0 });
|
|
109
|
+
parts.set(category, new Map());
|
|
110
|
+
}
|
|
111
|
+
const toolResults = [];
|
|
112
|
+
let total = 0;
|
|
113
|
+
let hasSystem = false;
|
|
114
|
+
for (const message of messages) {
|
|
115
|
+
const messageTokens = estimateMessageTokens(message);
|
|
116
|
+
total += messageTokens;
|
|
117
|
+
if (message.role === "system")
|
|
118
|
+
hasSystem = true;
|
|
119
|
+
if (message.role === "toolResult") {
|
|
120
|
+
toolResults.push({
|
|
121
|
+
ordinal: toolResults.length + 1,
|
|
122
|
+
toolName: message.toolName,
|
|
123
|
+
tokens: messageTokens,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const pieces = messagePieces(message);
|
|
127
|
+
const allocated = allocate(pieces, messageTokens);
|
|
128
|
+
pieces.forEach((piece, i) => {
|
|
129
|
+
const tokens = allocated[i] ?? 0;
|
|
130
|
+
const entry = totals.get(piece.category);
|
|
131
|
+
if (entry === undefined)
|
|
132
|
+
return;
|
|
133
|
+
entry.tokens += tokens;
|
|
134
|
+
if (piece.name !== undefined || piece.category !== "tools")
|
|
135
|
+
entry.count++;
|
|
136
|
+
if (piece.name !== undefined) {
|
|
137
|
+
const named = parts.get(piece.category);
|
|
138
|
+
named?.set(piece.name, (named.get(piece.name) ?? 0) + tokens);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const entries = BREAKDOWN_CATEGORIES.map((category) => {
|
|
143
|
+
const entry = totals.get(category) ?? { tokens: 0, count: 0 };
|
|
144
|
+
const named = [...(parts.get(category) ?? new Map())]
|
|
145
|
+
.map(([name, tokens]) => ({ name, tokens }))
|
|
146
|
+
.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
|
|
147
|
+
return { category, tokens: entry.tokens, count: entry.count, parts: named };
|
|
148
|
+
});
|
|
149
|
+
const prefixTokens = entries
|
|
150
|
+
.filter((e) => e.category === "system" || e.category === "tools")
|
|
151
|
+
.reduce((sum, e) => sum + e.tokens, 0);
|
|
152
|
+
return { entries, total, prefixTokens, hasSystem, toolResults };
|
|
153
|
+
}
|
|
154
|
+
/** 最大的 n 个工具结果(大小相同按出现顺序)。 */
|
|
155
|
+
export function largestToolResults(breakdown, n) {
|
|
156
|
+
return [...breakdown.toolResults]
|
|
157
|
+
.sort((a, b) => b.tokens - a.tokens || a.ordinal - b.ordinal)
|
|
158
|
+
.slice(0, Math.max(0, n));
|
|
159
|
+
}
|
|
@@ -15,6 +15,11 @@ import type { AgentMessage, SessionEntry } from "../session/types.js";
|
|
|
15
15
|
export declare const IMAGE_TOKENS = 1600;
|
|
16
16
|
/** 文本的 token 估算:CJK / 假名 / 谚文每字 1,其余字符 / 4(向上取整)。 */
|
|
17
17
|
export declare function estimateTextTokens(text: string): number;
|
|
18
|
+
interface CharCount {
|
|
19
|
+
wide: number;
|
|
20
|
+
narrow: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function countText(text: string, into: CharCount): CharCount;
|
|
18
23
|
export declare function calculateContextTokens(usage: Usage): number;
|
|
19
24
|
/** 内容的估算(文本按脚本,图片每张 1 600)。 */
|
|
20
25
|
export declare function estimateContentTokens(content: string | readonly ContentBlock[]): number;
|
|
@@ -36,3 +41,4 @@ export declare function estimateContextTokens(messages: readonly AgentMessage[])
|
|
|
36
41
|
* `items` 是 buildProjection(branch).items,`branch` 是同一活动分支。
|
|
37
42
|
*/
|
|
38
43
|
export declare function estimateProjectedTokens(items: readonly ContextItem[], branch: readonly SessionEntry[]): ContextEstimate;
|
|
44
|
+
export {};
|
|
@@ -29,7 +29,7 @@ function isWideCode(code) {
|
|
|
29
29
|
export function estimateTextTokens(text) {
|
|
30
30
|
return tokensOf(countText(text, { wide: 0, narrow: 0 }));
|
|
31
31
|
}
|
|
32
|
-
function countText(text, into) {
|
|
32
|
+
export function countText(text, into) {
|
|
33
33
|
let wide = 0;
|
|
34
34
|
let narrow = 0;
|
|
35
35
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动基线:系统消息落盘之前(第一次请求前)的上下文估算。
|
|
3
|
+
*
|
|
4
|
+
* 系统提示与工具表在第一次请求前才作为首条 `system` 消息写进分支,在此之前投影里没有它们,
|
|
5
|
+
* 状态栏会一直显示 0。这里按「将要发送的那一份」估算:命名节文本 + 工具声明 JSON,与
|
|
6
|
+
* `estimateMessageTokens` 对 `system` 消息的口径一致(CJK 每字 1,其余字符 / 4)。
|
|
7
|
+
*
|
|
8
|
+
* 只用于统计:不写分支、不改请求体,前缀缓存不受影响;压缩阈值也不看它(摘要压不掉这部分)。
|
|
9
|
+
*/
|
|
10
|
+
import type { ToolDecl } from "../ai/types.js";
|
|
11
|
+
import type { ContextEstimate } from "./estimate.js";
|
|
12
|
+
/** 系统提示(已装配的命名节)+ 工具声明的估算。 */
|
|
13
|
+
export declare function estimatePrefixTokens(sections: Readonly<Record<string, string>>, tools: readonly ToolDecl[]): number;
|
|
14
|
+
/** 上下文估算的来源(`SessionStats.context.source`)。 */
|
|
15
|
+
export type ContextSource = "usage" | "estimate" | "prefix";
|
|
16
|
+
export interface SourcedEstimate extends ContextEstimate {
|
|
17
|
+
source: ContextSource;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 给投影估算标来源;投影里还没有系统消息时把前缀基线加进去(`prefix` 懒算,只在需要时调用)。
|
|
21
|
+
* 有 usage 时照旧(usage 已经包含系统提示与工具表)。
|
|
22
|
+
*/
|
|
23
|
+
export declare function withPrefixBaseline(estimate: ContextEstimate, hasSystemMessage: boolean, prefix: () => number): SourcedEstimate;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动基线:系统消息落盘之前(第一次请求前)的上下文估算。
|
|
3
|
+
*
|
|
4
|
+
* 系统提示与工具表在第一次请求前才作为首条 `system` 消息写进分支,在此之前投影里没有它们,
|
|
5
|
+
* 状态栏会一直显示 0。这里按「将要发送的那一份」估算:命名节文本 + 工具声明 JSON,与
|
|
6
|
+
* `estimateMessageTokens` 对 `system` 消息的口径一致(CJK 每字 1,其余字符 / 4)。
|
|
7
|
+
*
|
|
8
|
+
* 只用于统计:不写分支、不改请求体,前缀缓存不受影响;压缩阈值也不看它(摘要压不掉这部分)。
|
|
9
|
+
*/
|
|
10
|
+
import { estimateMessageTokens } from "./estimate.js";
|
|
11
|
+
/** 系统提示(已装配的命名节)+ 工具声明的估算。 */
|
|
12
|
+
export function estimatePrefixTokens(sections, tools) {
|
|
13
|
+
const message = { role: "system", sections: { ...sections }, timestamp: 0 };
|
|
14
|
+
if (tools.length > 0)
|
|
15
|
+
message.toolsAdded = [...tools];
|
|
16
|
+
return estimateMessageTokens(message);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 给投影估算标来源;投影里还没有系统消息时把前缀基线加进去(`prefix` 懒算,只在需要时调用)。
|
|
20
|
+
* 有 usage 时照旧(usage 已经包含系统提示与工具表)。
|
|
21
|
+
*/
|
|
22
|
+
export function withPrefixBaseline(estimate, hasSystemMessage, prefix) {
|
|
23
|
+
if (estimate.usageTokens > 0)
|
|
24
|
+
return { ...estimate, source: "usage" };
|
|
25
|
+
if (hasSystemMessage)
|
|
26
|
+
return { ...estimate, source: "estimate" };
|
|
27
|
+
const base = prefix();
|
|
28
|
+
return {
|
|
29
|
+
...estimate,
|
|
30
|
+
tokens: estimate.tokens + base,
|
|
31
|
+
trailingTokens: estimate.trailingTokens + base,
|
|
32
|
+
source: "prefix",
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/config/auth-file.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - 本模块只搬运字符串,不解析 `!command` / `$ENV`(那是 ai/providers/auth.ts 的事),
|
|
7
7
|
* 也不把 key 写进任何日志或返回给展示层(`describeAuthFile` 只给来源与形态)。
|
|
8
8
|
*/
|
|
9
|
-
import { chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { retryTransientFs } from "./fs-retry.js";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import { loadConfigFile } from "./load.js";
|
|
12
13
|
import { AUTH_FILE } from "./paths.js";
|
|
@@ -54,7 +55,14 @@ export function writeAuthFile(path, file) {
|
|
|
54
55
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
55
56
|
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
56
57
|
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
-
|
|
58
|
+
try {
|
|
59
|
+
// Windows:别的 ama 进程正读着 auth.json 时覆盖它会暂时失败
|
|
60
|
+
retryTransientFs(() => renameSync(tmp, path));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
rmSync(tmp, { force: true });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
58
66
|
if (process.platform !== "win32")
|
|
59
67
|
chmodSync(path, 0o600);
|
|
60
68
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
|
|
3
|
+
*
|
|
4
|
+
* NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
|
|
5
|
+
* 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
|
|
6
|
+
* 其它平台上同样的错误码是真的权限问题,原样抛出。
|
|
7
|
+
*/
|
|
8
|
+
/** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
|
|
9
|
+
export declare function isTransientFsError(code: unknown, platform?: string): boolean;
|
|
10
|
+
export interface FsRetryOptions {
|
|
11
|
+
platform?: string;
|
|
12
|
+
attempts?: number;
|
|
13
|
+
/** 第 n 次重试前等 `delayMs * n`。 */
|
|
14
|
+
delayMs?: number;
|
|
15
|
+
sleep?: (ms: number) => void;
|
|
16
|
+
}
|
|
17
|
+
/** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
|
|
18
|
+
export declare function retryTransientFs<T>(fn: () => T, options?: FsRetryOptions): T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
|
|
3
|
+
*
|
|
4
|
+
* NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
|
|
5
|
+
* 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
|
|
6
|
+
* 其它平台上同样的错误码是真的权限问题,原样抛出。
|
|
7
|
+
*/
|
|
8
|
+
const TRANSIENT_WIN32 = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
9
|
+
/** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
|
|
10
|
+
export function isTransientFsError(code, platform = process.platform) {
|
|
11
|
+
return platform === "win32" && typeof code === "string" && TRANSIENT_WIN32.has(code);
|
|
12
|
+
}
|
|
13
|
+
/** 同步睡眠(只用在重试之间,最长几十毫秒)。 */
|
|
14
|
+
function sleepSync(ms) {
|
|
15
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
16
|
+
}
|
|
17
|
+
/** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
|
|
18
|
+
export function retryTransientFs(fn, options = {}) {
|
|
19
|
+
const attempts = options.attempts ?? 8;
|
|
20
|
+
const delayMs = options.delayMs ?? 20;
|
|
21
|
+
const sleep = options.sleep ?? sleepSync;
|
|
22
|
+
for (let n = 1;; n++) {
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const code = error.code;
|
|
28
|
+
if (n >= attempts || !isTransientFsError(code, options.platform))
|
|
29
|
+
throw error;
|
|
30
|
+
sleep(delayMs * n);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -281,6 +281,7 @@ function buildBaseSchema() {
|
|
|
281
281
|
animation: bool(),
|
|
282
282
|
restoreOnCancel: bool(),
|
|
283
283
|
enterWhileRunning: oneOf(["queue", "interrupt"]),
|
|
284
|
+
programStatus: oneOf(["auto", "on", "off"]),
|
|
284
285
|
statusLine: oneOf(STATUS_LINE_MODES),
|
|
285
286
|
language: oneOf(LANGUAGE_SETTINGS),
|
|
286
287
|
replyLanguage: str(),
|
package/dist/config/key-docs.js
CHANGED
package/dist/config/schema.js
CHANGED
|
@@ -261,6 +261,7 @@ export function validateConfig(value) {
|
|
|
261
261
|
"animation",
|
|
262
262
|
"restoreOnCancel",
|
|
263
263
|
"enterWhileRunning",
|
|
264
|
+
"programStatus",
|
|
264
265
|
...W5_UI_KEYS,
|
|
265
266
|
...W6_UI_KEYS,
|
|
266
267
|
], (s, p) => {
|
|
@@ -275,6 +276,7 @@ export function validateConfig(value) {
|
|
|
275
276
|
c.boolean(s, "animation", p);
|
|
276
277
|
c.boolean(s, "restoreOnCancel", p);
|
|
277
278
|
c.oneOf(s, "enterWhileRunning", p, ["queue", "interrupt"]);
|
|
279
|
+
c.oneOf(s, "programStatus", p, ["auto", "on", "off"]);
|
|
278
280
|
checkUiW5(c, s, p);
|
|
279
281
|
checkUiW6(c, s, p);
|
|
280
282
|
});
|
|
@@ -35,6 +35,7 @@ const ROWS = [
|
|
|
35
35
|
row("ui.statusLine", "ui", "now", "any"),
|
|
36
36
|
row("ui.restoreOnCancel", "ui", "nextSession", "any"),
|
|
37
37
|
row("ui.enterWhileRunning", "ui", "now", "any"),
|
|
38
|
+
row("ui.programStatus", "ui", "restart", "any"),
|
|
38
39
|
row("ui.quietStartup", "ui", "restart", "any"),
|
|
39
40
|
row("ui.ascii", "ui", "restart", "any", { envOverride: "AMA_ASCII" }),
|
|
40
41
|
row("ui.language", "ui", "restart", "any", { envOverride: "AMA_LANG" }),
|
package/dist/config/types.d.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface UiConfig extends UiConfigW6 {
|
|
|
153
153
|
restoreOnCancel?: boolean;
|
|
154
154
|
/** 运行中按 Enter:queue(缺省)排队插话,interrupt 打断并立即发送(与 `app.message.interrupt` 互换)。 */
|
|
155
155
|
enterWhileRunning?: "queue" | "interrupt";
|
|
156
|
+
/** 终端程序状态 OSC 7501:auto(缺省)检测通过才发,on 不检测直接发,off 不发。 */
|
|
157
|
+
programStatus?: "auto" | "on" | "off";
|
|
156
158
|
/** [W5-C0] 底部信息行(W5-A):缺省独立终端 full、嵌入宿主(有 profile)compact。 */
|
|
157
159
|
statusLine?: StatusLineMode;
|
|
158
160
|
}
|
|
@@ -83,6 +83,8 @@ class CodexSession {
|
|
|
83
83
|
tokensTotal;
|
|
84
84
|
tokensAtStart;
|
|
85
85
|
contextWindow;
|
|
86
|
+
/** 最近一次请求的总量(`tokenUsage.last.totalTokens`)= 线程当前的上下文占用。 */
|
|
87
|
+
contextTokens;
|
|
86
88
|
closed = false;
|
|
87
89
|
interruptRequested = false;
|
|
88
90
|
constructor(agentId, transport, deps) {
|
|
@@ -234,6 +236,7 @@ class CodexSession {
|
|
|
234
236
|
input: end.inputTokens - (start?.inputTokens ?? 0),
|
|
235
237
|
output: end.outputTokens - (start?.outputTokens ?? 0),
|
|
236
238
|
cacheRead: end.cachedInputTokens - (start?.cachedInputTokens ?? 0),
|
|
239
|
+
...(this.contextTokens !== undefined ? { contextTokens: this.contextTokens } : {}),
|
|
237
240
|
...(this.contextWindow !== undefined ? { contextWindow: this.contextWindow } : {}),
|
|
238
241
|
});
|
|
239
242
|
}
|
|
@@ -257,6 +260,9 @@ class CodexSession {
|
|
|
257
260
|
const total = readTokens(usage["total"]);
|
|
258
261
|
if (total !== undefined)
|
|
259
262
|
this.tokensTotal = total;
|
|
263
|
+
const last = readTokens(usage["last"]);
|
|
264
|
+
if (last !== undefined && last.totalTokens > 0)
|
|
265
|
+
this.contextTokens = last.totalTokens;
|
|
260
266
|
if (typeof usage["modelContextWindow"] === "number")
|
|
261
267
|
this.contextWindow = usage["modelContextWindow"];
|
|
262
268
|
return;
|
package/dist/drivers/runner.js
CHANGED
|
@@ -161,6 +161,8 @@ class ProcessHandle {
|
|
|
161
161
|
agent;
|
|
162
162
|
/** [W6-C0] 本回合工具的骨架(id → 种类 / 状态 / 起止),回合结束写 `turn_trace`。 */
|
|
163
163
|
turnTools = new Map();
|
|
164
|
+
/** 外部 Agent 最近报告的上下文占用与窗口(只记数字,跨回合保留)。 */
|
|
165
|
+
context = {};
|
|
164
166
|
constructor(runner, chosen, request, deps) {
|
|
165
167
|
this.runner = runner;
|
|
166
168
|
this.chosen = chosen;
|
|
@@ -299,6 +301,7 @@ class ProcessHandle {
|
|
|
299
301
|
this.emit({ type: "notice", level: event.level, text: event.text });
|
|
300
302
|
return;
|
|
301
303
|
case "usage":
|
|
304
|
+
this.noteContext(event.contextTokens, event.contextWindow);
|
|
302
305
|
if (event.costUsd !== undefined) {
|
|
303
306
|
turnCost.usd = event.costUsd;
|
|
304
307
|
const budget = this.request.budgetUsd;
|
|
@@ -319,6 +322,19 @@ class ProcessHandle {
|
|
|
319
322
|
return;
|
|
320
323
|
}
|
|
321
324
|
}
|
|
325
|
+
/** 上下文占用或窗口变了:记下并单独发一条只带它们的 usage(任务记录 / Agent 栏即时更新)。 */
|
|
326
|
+
noteContext(tokens, window) {
|
|
327
|
+
const next = { ...this.context };
|
|
328
|
+
if (tokens !== undefined)
|
|
329
|
+
next.contextTokens = tokens;
|
|
330
|
+
if (window !== undefined)
|
|
331
|
+
next.contextWindow = window;
|
|
332
|
+
if (next.contextTokens === this.context.contextTokens &&
|
|
333
|
+
next.contextWindow === this.context.contextWindow)
|
|
334
|
+
return;
|
|
335
|
+
this.context = next;
|
|
336
|
+
this.emit({ type: "usage", ...next });
|
|
337
|
+
}
|
|
322
338
|
now() {
|
|
323
339
|
return (this.deps.now ?? Date.now)();
|
|
324
340
|
}
|
|
@@ -412,6 +428,7 @@ class ProcessHandle {
|
|
|
412
428
|
unit,
|
|
413
429
|
amount,
|
|
414
430
|
...(usage.totalTokens > 0 ? { tokens: usage.totalTokens } : {}),
|
|
431
|
+
...this.context,
|
|
415
432
|
});
|
|
416
433
|
this.emit({ type: "usage", usage, unit, amount });
|
|
417
434
|
}
|
package/dist/drivers/store.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export interface AgentUsageRecord {
|
|
|
27
27
|
unit: ExternalUsageUnit;
|
|
28
28
|
amount: number;
|
|
29
29
|
tokens?: number;
|
|
30
|
+
/** 回合结束时该 Agent 报告的上下文占用与窗口(只记数字)。 */
|
|
31
|
+
contextTokens?: number;
|
|
32
|
+
contextWindow?: number;
|
|
30
33
|
}
|
|
31
34
|
/** 会话一侧的最小接口(`ToolContext.session` 或 SessionCore 都能提供)。 */
|
|
32
35
|
export interface AgentStoreBackend {
|
package/dist/drivers/store.js
CHANGED
|
@@ -75,6 +75,10 @@ export function aggregateExternal(entries) {
|
|
|
75
75
|
row.amount += u.amount;
|
|
76
76
|
if (typeof u.tokens === "number")
|
|
77
77
|
row.tokens = (row.tokens ?? 0) + u.tokens;
|
|
78
|
+
if (typeof u.contextTokens === "number")
|
|
79
|
+
row.contextTokens = u.contextTokens;
|
|
80
|
+
if (typeof u.contextWindow === "number")
|
|
81
|
+
row.contextWindow = u.contextWindow;
|
|
78
82
|
}
|
|
79
83
|
return { byAgent };
|
|
80
84
|
}
|
package/dist/i18n/catalog.d.ts
CHANGED
|
@@ -541,6 +541,8 @@ declare const en: {
|
|
|
541
541
|
cacheSilent: string;
|
|
542
542
|
sandbox: string;
|
|
543
543
|
cycleHint: string;
|
|
544
|
+
autoCompact: string;
|
|
545
|
+
noWindow: (model: string) => string;
|
|
544
546
|
quota: {
|
|
545
547
|
session: string;
|
|
546
548
|
reset: string;
|
|
@@ -998,6 +1000,7 @@ declare const en: {
|
|
|
998
1000
|
interrupted: string;
|
|
999
1001
|
};
|
|
1000
1002
|
turns: (n: number) => string;
|
|
1003
|
+
context: (percent: string) => string;
|
|
1001
1004
|
agentRunner: (agent: string, runner: string) => string;
|
|
1002
1005
|
background: string;
|
|
1003
1006
|
none: string;
|
|
@@ -1084,6 +1087,7 @@ declare const en: {
|
|
|
1084
1087
|
tools: string;
|
|
1085
1088
|
hooks: string;
|
|
1086
1089
|
session: string;
|
|
1090
|
+
context: string;
|
|
1087
1091
|
cache: string;
|
|
1088
1092
|
statusline: string;
|
|
1089
1093
|
planArgs: string;
|
|
@@ -1139,6 +1143,50 @@ declare const en: {
|
|
|
1139
1143
|
cwdAnswer: string;
|
|
1140
1144
|
notDirectory: (path: string) => string;
|
|
1141
1145
|
};
|
|
1146
|
+
contextReport: {
|
|
1147
|
+
title: string;
|
|
1148
|
+
keyUsed: string;
|
|
1149
|
+
used: (tokens: string, window: string, percent: string | undefined) => string;
|
|
1150
|
+
keyLeft: string;
|
|
1151
|
+
left: (tokens: string) => string;
|
|
1152
|
+
keySource: string;
|
|
1153
|
+
sourceUsage: (usage: string, estimate: string) => string;
|
|
1154
|
+
sourceEstimate: string;
|
|
1155
|
+
keyAutoCompact: string;
|
|
1156
|
+
autoCompact: (at: string, distance: string) => string;
|
|
1157
|
+
autoCompactReached: (at: string) => string;
|
|
1158
|
+
autoCompactOff: string;
|
|
1159
|
+
windowUnknown: string;
|
|
1160
|
+
keyPrune: string;
|
|
1161
|
+
prune: (at: string) => string;
|
|
1162
|
+
breakdown: (total: string) => string;
|
|
1163
|
+
prefixCounted: string;
|
|
1164
|
+
prefixPending: string;
|
|
1165
|
+
categories: {
|
|
1166
|
+
system: string;
|
|
1167
|
+
tools: string;
|
|
1168
|
+
user: string;
|
|
1169
|
+
assistant: string;
|
|
1170
|
+
reasoning: string;
|
|
1171
|
+
toolCalls: string;
|
|
1172
|
+
toolResults: string;
|
|
1173
|
+
images: string;
|
|
1174
|
+
summaries: string;
|
|
1175
|
+
custom: string;
|
|
1176
|
+
};
|
|
1177
|
+
partNames: {
|
|
1178
|
+
compaction: string;
|
|
1179
|
+
branch: string;
|
|
1180
|
+
user: string;
|
|
1181
|
+
toolResult: string;
|
|
1182
|
+
custom: string;
|
|
1183
|
+
};
|
|
1184
|
+
images: (count: number) => string;
|
|
1185
|
+
more: (count: number) => string;
|
|
1186
|
+
largest: (count: number) => string;
|
|
1187
|
+
noToolResults: string;
|
|
1188
|
+
empty: string;
|
|
1189
|
+
};
|
|
1142
1190
|
cache: {
|
|
1143
1191
|
unreported: string;
|
|
1144
1192
|
unknown: string;
|
|
@@ -1196,6 +1244,8 @@ declare const en: {
|
|
|
1196
1244
|
usage: (input: number, output: number, cacheRead: number, cacheWrite: number, cost: string) => string;
|
|
1197
1245
|
keyContext: string;
|
|
1198
1246
|
context: (tokens: string, window: string, percent: string) => string;
|
|
1247
|
+
toAutoCompact: (tokens: string) => string;
|
|
1248
|
+
autoCompactOff: string;
|
|
1199
1249
|
keyAgents: string;
|
|
1200
1250
|
externalTitle: string;
|
|
1201
1251
|
noFingerprint: string;
|
|
@@ -1333,6 +1383,7 @@ declare const en: {
|
|
|
1333
1383
|
"ui.animation": string;
|
|
1334
1384
|
"ui.restoreOnCancel": string;
|
|
1335
1385
|
"ui.enterWhileRunning": string;
|
|
1386
|
+
"ui.programStatus": string;
|
|
1336
1387
|
"ui.statusLine": string;
|
|
1337
1388
|
"ui.language": string;
|
|
1338
1389
|
"ui.replyLanguage": string;
|
|
@@ -1742,6 +1793,7 @@ declare const en: {
|
|
|
1742
1793
|
interrupted: string;
|
|
1743
1794
|
};
|
|
1744
1795
|
turns: (n: number) => string;
|
|
1796
|
+
context: (percent: string) => string;
|
|
1745
1797
|
bar: {
|
|
1746
1798
|
more: (n: number) => string;
|
|
1747
1799
|
keys: (up: string, down: string) => string;
|
|
@@ -2089,6 +2141,7 @@ declare const en: {
|
|
|
2089
2141
|
"ui.statusLine": string;
|
|
2090
2142
|
"ui.restoreOnCancel": string;
|
|
2091
2143
|
"ui.enterWhileRunning": string;
|
|
2144
|
+
"ui.programStatus": string;
|
|
2092
2145
|
"ui.quietStartup": string;
|
|
2093
2146
|
"ui.ascii": string;
|
|
2094
2147
|
"ui.language": string;
|