agent-from-zero 0.34.2 → 0.35.0

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/README.md CHANGED
@@ -43,6 +43,153 @@ MINI_AGENT_MODEL=gpt-4.1-mini
43
43
 
44
44
  各家 base URL 见 [.env.example](.env.example)。两个注意:模型必须支持 function calling;窗口比 DeepSeek 小的话设 `MINI_AGENT_CONTEXT_WINDOW`(单位 token)。换完跑一遍 `npm run eval`——同样 10 道题,正好当模型横评。
45
45
 
46
+ ## 非交互执行(print / exec)
47
+
48
+ 像 `claude -p` 一样执行一次任务并退出,也可以使用 `exec` 入口:
49
+
50
+ ```bash
51
+ mini-agent -p "解释这个项目" --model deepseek-flash --effort high
52
+ mini-agent exec --model deepseek-flash --effort max --prompt "检查当前改动"
53
+ cat error.log | mini-agent -p "分析这份日志"
54
+ mini-agent -p "总结当前改动" --output-format json > result.json
55
+
56
+ # 从源码运行同样的参数
57
+ npm start -- -p "解释这个项目" --model deepseek-flash --effort high
58
+ ```
59
+
60
+ `-p` / `--print` 是非交互开关,参数可以放在 prompt 前后;prompt 可以是位置参数或
61
+ `--prompt`,二者不能同时使用。以 `-` 开头的任务文字放在 `--` 后,例如
62
+ `mini-agent -p -- "--help 参数是什么意思"`。没有位置 prompt 时也可以只从 stdin 读取任务;
63
+ 有 prompt 时,管道内容追加在其后。管道会读到 EOF,脚本调用时应关闭 stdin,或用 `< /dev/null`。
64
+
65
+ `--model` 和 `--effort` 只覆盖本次进程,不修改保存的配置;交互启动也接受这两个选项。
66
+ effort 使用 `/effort` 的同一套能力判断,不支持的值会列出可选项并以退出码 2 退出。
67
+ 未知模型或代理需要在 settings 中配置 `effortProfiles`;`--effort default` 使用供应商默认值。
68
+ `--model` 使用当前 endpoint 的模型 ID,不自动切换 vendor、endpoint 或 API key。
69
+
70
+ 默认 `text` 输出只包含最终回答,保留原始 Markdown;进度与诊断走 stderr,不输出动画或输入框。
71
+ `--output-format json` 输出单个结果对象,含 `result`、`is_error`、`reason`、`session_id`、
72
+ `model`、`effort`、`usage` 和 `estimated_cost_usd`。usage 与费用是本地会话计量,
73
+ 不含 Jev 等辅助审核调用。当前支持 text / json,不兼容 Claude 的 stream-json SDK 协议。
74
+
75
+ 退出码:`0` 正常完成,`1` 执行失败,`2` 参数错误,`130` 被中断。
76
+ `-r` / `--resume` 仍可续接最近会话;`--auto` 仍经过原有审核,无人确认的操作直接拒绝。
77
+ 单独使用 `-p` 不会放宽权限;它不能弹出交互审批,未获批准的操作会被拒绝。
78
+
79
+ 需要显式跳过审批时,使用与 Claude Code 相同的参数:
80
+
81
+ ```bash
82
+ mini-agent -p "执行这项任务" --permission-mode bypassPermissions
83
+ mini-agent exec "执行这项任务" --dangerously-skip-permissions
84
+ npm start -- -p "执行这项任务" --model deepseek-flash --effort high --permission-mode bypassPermissions
85
+ ```
86
+
87
+ `bypassPermissions` 跳过普通人工确认和 Jev/模型审核,也适用于子 agent、后台 shell 和 MCP 调用。
88
+ 它只对本次进程生效,不写入 settings;显式 bypass 优先于 `--auto` 和配置里的 auto mode。
89
+ `--permission-mode auto` 启用自动审核,`--permission-mode default` 关闭 auto 和 bypass。
90
+ `--dangerously-skip-permissions` 等价于 bypass 模式,并优先于同一命令中的 mode 参数。
91
+ 交互界面会在底部显示 `bypass permissions on`;`/auto` 不会改变这个启动选项,需重新启动才能取消。
92
+
93
+ 显式 deny、受保护路径、plan 模式限制以及 PreToolUse hook 拦截仍然有效;
94
+ 业务澄清问题不会自动代答。源码对照为 `utils/permissions/permissionSetup.ts` 的
95
+ `initialPermissionModeFromCLI` 与 `utils/permissions/permissions.ts` 的 bypass 分支:
96
+ 先检查规则,再放行普通审批。mini-agent 保留自己的硬性规则,不宣称完全复制 Claude 的全部例外。
97
+
98
+ 交互参考 [Claude Code CLI](https://code.claude.com/docs/en/cli-reference) 和本地
99
+ `claude-code-sourcemap/restored-src/src/main.tsx` 的参数及 stdin 处理;不是其全部参数的兼容实现。
100
+
101
+ ## Usage and account status
102
+
103
+ 输入 `/status` 查看当前模型、本会话 token/缓存命中/估算费用,以及 vendor 支持的账户信息。
104
+ DeepSeek 可直接查余额;OpenAI、Anthropic 的组织 usage/cost 需要另外配置
105
+ `OPENAI_ADMIN_KEY` / `ANTHROPIC_ADMIN_KEY`,不支持查询的余额会明确显示不可用。
106
+
107
+ `/status local` 只看本地统计,不联网。使用代理时可显式输入 `/status deepseek`、
108
+ `/status openai`、`/status anthropic` 查询对应的官方账户;不会把代理 key 当成官方账户 key。
109
+ 组织报表按 UTC 本月至今统计,涵盖管理凭证所属组织,区别于当前会话。
110
+ 接口、凭证和统计范围见 [Status 说明](docs/status.md)。
111
+
112
+ ## Auto mode
113
+
114
+ ### Rule-based policy preview
115
+
116
+ 新规则策略已可选启用,审核结果分为 `allow / deny / ask`,不再用授权和风险两个分数作为放行门槛:
117
+
118
+ ```sh
119
+ MINI_AGENT_AUTO_POLICY=rules npm start -- --auto
120
+ ```
121
+
122
+ 也可在 settings 中设置 `autoMode.policy: "rules"`。配置 `JEV_API_KEY` 或
123
+ `TYPESAFE_API_KEY` 后先用 Jev 初筛;有疑点再用同一 vendor 的 `judge.model` 复核,
124
+ 未指定则使用当前模型。没有 key 或 Jev 故障时直接走模型。普通任务内检查、测试、修改及明确要求的常规
125
+ commit/push 按实际效果审核;删除、丢弃工作、改写共享历史、基础设施写入和权限修改
126
+ 仍要求人工确认。违反明确限制的操作直接拒绝,让 agent 改用合规方案;连续 3 次或
127
+ 累计 20 次自动拒绝会停止,等待用户给出方向。子 agent 和中断恢复共用预算。
128
+
129
+ Jev 初筛要求每条阻断规则和不确定性概率均 `≤ 0.05`,否则交给模型复核一次,
130
+ 不会直接因初筛分数弹人工审批。历史动作有缺失时也转模型。这个预览阈值尚未经过
131
+ 真实数据校准,不代表整体误放率;新策略仍需显式选择,默认保留旧 `scores` 策略。
132
+ Jev 额度耗尽或出错会提示并在本会话回退到模型。使用 `/auto debug on` 可记录两阶段
133
+ 实际上下文、分流原因与最终决定,详见 [策略设计及评测](docs/auto-mode-redesign.md)。
134
+
135
+ ### Existing score policy (`scores`, default)
136
+
137
+ 启动时加 `--auto`,或在会话中输入 `/auto` 开关自动审核。默认关闭;开启后,审核通过的工具操作自动执行,有风险、判断不确定或审核失败时仍会询问。Ink、readline 和 `-p` 模式共用同一条权限路径,后台 shell 和子 agent 的操作也会经过检查。
138
+
139
+ 审核后端自动选择:
140
+
141
+ - 配置了 `JEV_API_KEY` 或 `TYPESAFE_API_KEY`:优先使用 Jev。
142
+ - 没有 Jev key:复用主 agent 的 API endpoint 和 key,默认使用当前模型;启动时显示一条 Jev 配置提示,不影响使用。
143
+ - 在 `.mini-agent/settings.json` 或全局 settings 中设置 `judge.model`,可以选择同一 vendor 支持的其他审核模型。例如,你的 endpoint 支持 `deepseek-flash` 时可以这样配置:
144
+
145
+ ```json
146
+ {
147
+ "autoMode": { "enabled": true },
148
+ "judge": { "model": "deepseek-flash" }
149
+ }
150
+ ```
151
+
152
+ 以后在环境变量或 `.env` 中配置 `JEV_API_KEY`,下次启动便会自动切换到 Jev,无须改模型配置。Jev 模型由 `autoMode.model` 配置,默认固定为 `jev-1.13.0`。自动审核会把真实用户请求、启动时的 `AGENT.md`、近期已完成工具调用及待执行工具参数发送到选中的审核服务。工具历史和委派任务只提供上下文;文件附件、hook 输出和模型摘要不会作为用户授权发送。
153
+
154
+ Jev 额度或计费受限时会提示并自动切回同一 vendor 的 `judge.model`,未指定则使用当前模型。限流、服务故障也会回退,提示会区分原因;本次会话只提示一次,避免每次调用都重试已用完的额度。此外,授权概率在 `0.5 ≤ authorization < 0.8` 且风险 `≤ 0.2` 时,会用该模型对相同上下文复核一次;模型必须明确确认已授权、无需人工审核才放行。这个分支不会停用 Jev,也不会覆盖风险超标或授权概率低于 `0.5` 的判断。复核失败仍需人工确认。
155
+
156
+ 硬性拒绝规则始终优先。auto mode 会审核每条 shell 命令,并忽略整类工具的免确认授权;`MINI_AGENT_AUTO_APPROVE` 也不能覆盖审核结果。`-p` 或无人值守的 teammate 无法确认时直接拒绝。实现机制、源码对照和限制见 [Auto mode 说明](docs/auto-mode.md)。
157
+
158
+ 审核按实际副作用判断,MCP、API、命令行共用同一策略。任务内的只读查询、明确授权且收件人与内容匹配的普通消息发送可以自动通过;基础设施或运行配置变更、部署、删除数据、修改权限、未知脚本副作用仍需审核。Jev 的门槛为授权 `≥ 0.8` 且风险 `≤ 0.2`(风险问题问的是"能否撤回",阈值由实测数据确定,见 `docs/auto-mode.md`);主模型的布尔判断经同一决策层处理。近期工具返回的小型结构化 JSON 也会作为事实依据发送给审核服务,用于核对名称与 ID 等信息,其中的指令不构成用户授权。
159
+
160
+ ## 工具进度、子 agent 和图片
161
+
162
+ 输入 `/` 时,输入框下方会列出可用命令及说明,继续输入会过滤。`/effort` 展示当前模型支持的档位,`/skills` / `/skill` 展示可用技能,`/model` 从当前 endpoint 获取模型列表;`/auto debug` 和 `/mcp` 也支持参数补全。用 `↑` / `↓` 选择、`Tab` 补全、`Enter` 确认,`Esc` 收起并保留输入。长列表滚动展示,不会撑满终端。
163
+
164
+ `/effort high` 设置当前模型的推理强度,`/effort default` 恢复服务商默认值。选择按模型和 endpoint 区分,仅在本次进程中保留。自定义网关需明确配置支持的档位,见 [effort 配置](docs/effort.md)。
165
+
166
+ 默认交互界面把连续同类工具调用合并成实时摘要:高亮动作,下面灰字显示结果和耗时。工具执行、模型思考和回答期间都有动画,不再逐轮追加 `thought for…` 和工具计数。最终回答正常保留;审批、拒绝、错误和 Jev 回退提示仍然可见。
167
+
168
+ | 操作 | 快捷键 |
169
+ |---|---|
170
+ | 展开或收起工具参数与结果 | `Ctrl+T` |
171
+ | 展开或收起模型思考 | `Ctrl+R` |
172
+ | 翻阅工具或思考详情 | `↑` / `↓` 或 `PgUp` / `PgDn` |
173
+ | 进入 agent 列表 | `Tab`,或输入框开头的 `←` |
174
+ | 选择 agent,查看实时过程 | `↑` / `↓`,`Enter` |
175
+ | 翻阅 agent 过程 | `PgUp` / `PgDn` |
176
+ | 返回主界面;无面板时中断任务 | `Esc` |
177
+ | 粘贴剪贴板图片或文字 | `Ctrl+V` |
178
+ | 工作中插入补充消息 | 输入后按 `Enter`,在下一个工具边界送入 |
179
+ | 中断当前步骤并优先处理补充消息 | `Ctrl+Enter`;已有排队消息时也可按 `Esc` |
180
+
181
+ 工作中的补充消息先以灰字显示在待处理区,送入模型时转为正常用户消息。支持多条消息和图片,保持发送顺序;当前已执行的操作不会因此撤销,尚未启动的旧工具调用会跳过并按新消息重新规划。`Esc` 优先关闭详情面板。部分终端不能区分 `Ctrl+Enter` 和 `Enter`,此时可先按 `Enter` 排队,再按 `Esc`。
182
+
183
+ 审批或问题选项出现时,输入框仍可输入文字;有文字时按 `Enter` 会取消当前选择并发送 follow-up,空输入时按 `Enter` 才确认选项。只有一个问题时,选中答案后按一次 `Enter` 即提交,不再显示额外的 Submit answers;多个问题仍统一提交。
184
+
185
+ 粘贴至少 4 行或 1000 字符的文本时,会折成 `[Pasted text #1 +13 lines]` 之类的占位符;长单行文本显示字符数。左右方向键跨过整个占位符,Backspace 整块删除。可以混合输入文字、多个文本块和图片。发送及权限审核收到完整原文,输入历史仍可恢复文本块;补充消息和已发送消息保留简洁展示。
186
+
187
+ agent 列表竖排显示 `main`、subagent 和 teammate,右侧显示当前活动、`idle` / `done` / `failed`、耗时及输出 token。带 `~` 的 token 是流式估算;API 返回用量后使用实报数据。切换视图不会停止其他 agent,也不会把它们的详细过程塞进主对话。
188
+
189
+ 粘贴图片后,输入框显示 `[Image #1]`;可以继续输入说明或粘贴多张图片。光标紧跟占位符时按 Backspace 可以删除附件。每条消息最多 4 张、每张最多 5 MB。发送时使用真实图像数据,当前模型须同时支持图片和工具调用;模型拒绝图片时会提示检查限制或用 `/model` 切换。图片随会话保存,恢复会话和后续追问可以继续引用。
190
+
191
+ 剪贴板通过 `ClipboardSource` 接口接入:macOS 使用 AppKit,Windows 使用 PowerShell/Windows Forms,Linux Wayland 使用 `wl-paste`,X11 使用 `xclip`(Linux 需安装相应工具)。界面和图像消息构造不依赖操作系统。以上交互适用于默认 Ink 界面;`MINI_AGENT_NO_INK=1` 保留文字版 readline 界面。实现对照和验证范围见 [交互界面说明](docs/interactive-ui.md)。
192
+
46
193
  ## 进度
47
194
 
48
195
  | Tag | What it adds |
@@ -0,0 +1,172 @@
1
+ const unavailable = (reason) => ({ state: "unavailable", reason });
2
+ const origins = {
3
+ deepseek: "https://api.deepseek.com", openai: "https://api.openai.com", anthropic: "https://api.anthropic.com",
4
+ };
5
+ export function accountProvider(baseURL) {
6
+ try {
7
+ const url = new URL(baseURL);
8
+ if (url.username || url.password)
9
+ return undefined;
10
+ return Object.keys(origins).find((id) => origins[id] === url.origin);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ class AccountError extends Error {
17
+ }
18
+ function object(value) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ throw new AccountError("Invalid account API response");
21
+ return value;
22
+ }
23
+ function array(value) {
24
+ if (!Array.isArray(value))
25
+ throw new AccountError("Invalid account API response");
26
+ return value;
27
+ }
28
+ function number(value) {
29
+ if (!(typeof value === "number" || (typeof value === "string" && /^-?\d+(?:\.\d+)?$/.test(value))))
30
+ throw new AccountError("Invalid numeric account data");
31
+ const result = Number(value);
32
+ if (!Number.isFinite(result))
33
+ throw new AccountError("Invalid numeric account data");
34
+ return result;
35
+ }
36
+ function tokens(value) {
37
+ const result = number(value);
38
+ if (!Number.isSafeInteger(result) || result < 0)
39
+ throw new AccountError("Invalid token count");
40
+ return result;
41
+ }
42
+ function currency(value) {
43
+ if (typeof value !== "string" || !/^[a-z]{3}$/i.test(value))
44
+ throw new AccountError("Invalid account currency");
45
+ return value.toUpperCase();
46
+ }
47
+ async function metric(run) {
48
+ try {
49
+ return { state: "ok", value: await run() };
50
+ }
51
+ catch (error) {
52
+ // Never print provider bodies or transport errors: either can contain keys.
53
+ return unavailable(error instanceof AccountError ? error.message : "Request failed, timed out or was cancelled; try /status again");
54
+ }
55
+ }
56
+ async function json(adapter, path, query = {}) {
57
+ const url = new URL(path, adapter.origin);
58
+ url.search = new URLSearchParams(query).toString();
59
+ adapter.signal.throwIfAborted();
60
+ const response = await adapter.request(url.toString(), { method: "GET", headers: adapter.headers, signal: adapter.signal, redirect: "error" });
61
+ if (!response.ok)
62
+ throw new AccountError(response.status === 401 || response.status === 403
63
+ ? `Authentication/permission denied (HTTP ${response.status}); check the account credential`
64
+ : response.status === 429 ? "Account API rate-limited; try later" : `Account API unavailable (HTTP ${response.status})`);
65
+ return object(await response.json());
66
+ }
67
+ async function report(adapter, path, query) {
68
+ const results = [];
69
+ const seen = new Set();
70
+ // Fetch all pages or fail this metric. Never label a partial sum as a total.
71
+ for (let page = 0; page < 50; page++) {
72
+ const body = await json(adapter, path, query);
73
+ for (const bucket of array(body.data))
74
+ for (const result of array(object(bucket).results))
75
+ results.push(object(result));
76
+ if (body.has_more === false)
77
+ return results;
78
+ if (body.has_more !== true || typeof body.next_page !== "string" || !body.next_page || seen.has(body.next_page))
79
+ throw new AccountError("Invalid account report pagination");
80
+ seen.add(body.next_page);
81
+ query = { ...query, page: body.next_page };
82
+ }
83
+ throw new AccountError("Account report exceeded the page limit; total unavailable");
84
+ }
85
+ function sumUsage(rows, provider) {
86
+ return rows.reduce((sum, row) => {
87
+ let input;
88
+ let cached;
89
+ if (provider === "openai") {
90
+ input = tokens(row.input_tokens);
91
+ cached = tokens(row.input_cached_tokens ?? 0); // included in input_tokens
92
+ if (cached > input)
93
+ throw new AccountError("Invalid cached token count");
94
+ }
95
+ else {
96
+ const creation = object(row.cache_creation);
97
+ cached = tokens(row.cache_read_input_tokens);
98
+ input = tokens(row.uncached_input_tokens) + cached + tokens(creation.ephemeral_1h_input_tokens) + tokens(creation.ephemeral_5m_input_tokens);
99
+ }
100
+ return { input: sum.input + input, cached: sum.cached + cached, output: sum.output + tokens(row.output_tokens) };
101
+ }, { input: 0, cached: 0, output: 0 });
102
+ }
103
+ function sumCost(rows, provider) {
104
+ const totals = new Map();
105
+ for (const row of rows) {
106
+ const money = provider === "openai" ? object(row.amount) : row;
107
+ const unit = currency(money.currency);
108
+ // Anthropic reports decimal cents; OpenAI reports currency units.
109
+ const amount = provider === "openai" ? number(money.value) : number(money.amount) / 100;
110
+ totals.set(unit, (totals.get(unit) ?? 0) + amount);
111
+ }
112
+ return totals.size ? [...totals].map(([currency, amount]) => ({ currency, amount })) : [{ currency: "USD", amount: 0 }];
113
+ }
114
+ export async function getAccountStatus(options) {
115
+ const detected = accountProvider(options.baseURL);
116
+ const provider = options.provider ?? detected;
117
+ const now = options.now ?? new Date();
118
+ const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
119
+ const result = {
120
+ provider: provider ?? "custom", checkedAt: now.toISOString(), period: `${start.toISOString()} → ${now.toISOString()}`,
121
+ balance: unavailable("No documented public balance endpoint integrated; check the vendor billing console"),
122
+ usage: unavailable("Not provided by this account API; see Current session"),
123
+ cost: unavailable("Not provided by this account API; see Current session estimate"), note: "",
124
+ };
125
+ if (!provider) {
126
+ result.note = "Custom/proxy endpoint: billing is vendor-specific. Use /status <provider> only to query a separate official account with its dedicated credential.";
127
+ return result;
128
+ }
129
+ const adminEnv = provider === "openai" ? "OPENAI_ADMIN_KEY" : "ANTHROPIC_ADMIN_KEY";
130
+ const key = provider === "deepseek" ? (detected === "deepseek" ? options.apiKey : options.env.DEEPSEEK_API_KEY)
131
+ : options.env[adminEnv] || options.env[adminEnv.replace("_KEY", "_API_KEY")];
132
+ if (!key?.trim()) {
133
+ const reason = provider === "deepseek" ? "No DeepSeek API key configured" : `Set ${adminEnv} (admin credential); a normal inference key is not used for organization reports`;
134
+ if (provider === "deepseek")
135
+ result.balance = unavailable(reason);
136
+ else {
137
+ result.usage = unavailable(reason);
138
+ result.cost = unavailable(reason);
139
+ }
140
+ return result;
141
+ }
142
+ // Fixed official origins, no redirects and no model-name inference: never
143
+ // send an admin key to an OpenAI-compatible proxy or a lookalike host.
144
+ const adapter = {
145
+ origin: origins[provider], request: options.request ?? fetch,
146
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
147
+ headers: provider === "anthropic" ? { "x-api-key": key.trim(), "anthropic-version": "2023-06-01", "User-Agent": "mini-agent" } : { Authorization: `Bearer ${key.trim()}` },
148
+ };
149
+ if (provider === "deepseek") {
150
+ result.balance = await metric(async () => {
151
+ const body = await json(adapter, "/user/balance");
152
+ if (typeof body.is_available !== "boolean")
153
+ throw new AccountError("Invalid balance availability");
154
+ const balances = array(body.balance_infos).map((entry) => { const v = object(entry); return { currency: currency(v.currency), amount: number(v.total_balance) }; });
155
+ if (!balances.length)
156
+ throw new AccountError("Balance API returned no currencies");
157
+ result.note = `Balance sufficient for API calls: ${body.is_available ? "yes" : "no"}`;
158
+ return balances;
159
+ });
160
+ return result;
161
+ }
162
+ result.note = `Organization-wide reports for the admin credential, across all keys/projects; may be delayed. Usage covers ${provider === "openai" ? "completions" : "Messages"} only.${provider === "anthropic" ? " Cost excludes Priority Tier." : ""}`;
163
+ const query = provider === "openai"
164
+ ? { start_time: String(Math.floor(start.getTime() / 1000)), end_time: String(Math.floor(now.getTime() / 1000)), bucket_width: "1d", limit: "31" }
165
+ : { starting_at: start.toISOString(), ending_at: now.toISOString(), bucket_width: "1d", limit: "31" };
166
+ const paths = provider === "openai" ? ["/v1/organization/usage/completions", "/v1/organization/costs"] : ["/v1/organizations/usage_report/messages", "/v1/organizations/cost_report"];
167
+ [result.usage, result.cost] = await Promise.all([
168
+ metric(async () => sumUsage(await report(adapter, paths[0], query), provider)),
169
+ metric(async () => sumCost(await report(adapter, paths[1], query), provider)),
170
+ ]);
171
+ return result;
172
+ }
@@ -0,0 +1,39 @@
1
+ export class AgentProgress {
2
+ model;
3
+ startedAt = Date.now();
4
+ endedAt;
5
+ tokens = 0;
6
+ estimated = false;
7
+ liveTokens = 0;
8
+ toolCalls = 0;
9
+ activity = "Starting";
10
+ transcript = "";
11
+ truncated = false;
12
+ constructor(model) {
13
+ this.model = model;
14
+ }
15
+ append(text) {
16
+ this.transcript += text;
17
+ if (this.transcript.length > 32_000) {
18
+ this.transcript = this.transcript.slice(-32_000);
19
+ this.truncated = true;
20
+ }
21
+ }
22
+ finishModelCall(reportedTokens) {
23
+ this.tokens += reportedTokens ?? this.liveTokens;
24
+ if (reportedTokens === undefined && this.liveTokens > 0)
25
+ this.estimated = true;
26
+ this.liveTokens = 0;
27
+ }
28
+ finish() { this.endedAt ??= Date.now(); }
29
+ view(id, name, description, status) {
30
+ return {
31
+ id, name, description, status, model: this.model,
32
+ elapsedMs: (this.endedAt ?? Date.now()) - this.startedAt,
33
+ tokens: this.tokens + this.liveTokens,
34
+ estimated: this.estimated || this.liveTokens > 0,
35
+ toolCalls: this.toolCalls, activity: this.activity,
36
+ transcript: (this.truncated ? "… (earlier activity omitted)\n" : "") + this.transcript,
37
+ };
38
+ }
39
+ }