@armadra/agent 0.7.5 → 0.8.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/CHANGELOG.md +34 -0
- package/CHANGELOG.zh-CN.md +34 -0
- package/dist/agent/session-classifier.d.ts +2 -0
- package/dist/agent/session-classifier.js +24 -3
- package/dist/agent/session-extensions.d.ts +1 -0
- package/dist/agent/session-extensions.js +3 -0
- package/dist/agent/session-subagent.js +9 -2
- package/dist/agent/subagent-background.d.ts +5 -1
- package/dist/agent/subagent-background.js +13 -1
- package/dist/agent/subagent-fork.d.ts +10 -5
- package/dist/agent/subagent-fork.js +22 -6
- package/dist/agent/subagent-registry.d.ts +1 -1
- package/dist/agent/subagent-registry.js +1 -1
- package/dist/agents/catalog.d.ts +1 -0
- package/dist/ai/apis/chatgpt-backend.d.ts +3 -1
- package/dist/ai/apis/chatgpt-backend.js +10 -1
- package/dist/ai/apis/max-tokens.d.ts +4 -0
- package/dist/ai/apis/max-tokens.js +12 -1
- package/dist/ai/providers/enrich.d.ts +6 -2
- package/dist/ai/providers/enrich.js +13 -2
- package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
- package/dist/ai/providers/max-tokens-cache.js +95 -0
- package/dist/ai/providers/registry.js +3 -3
- package/dist/bundle/ama.cjs +1250 -375
- package/dist/checkpoints/shadow-git.d.ts +5 -1
- package/dist/checkpoints/shadow-git.js +11 -2
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-store.d.ts +6 -2
- package/dist/cli/compose-store.js +8 -4
- package/dist/cli/compose.js +2 -2
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema-w5.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +5 -0
- package/dist/drivers/acp/driver.d.ts +22 -1
- package/dist/drivers/acp/driver.js +59 -5
- package/dist/drivers/agents.js +3 -0
- package/dist/drivers/catalog.d.ts +2 -0
- package/dist/drivers/catalog.js +94 -4
- package/dist/drivers/native/claude-normalize.d.ts +8 -0
- package/dist/drivers/native/claude-normalize.js +27 -0
- package/dist/drivers/native/claude-stream.js +16 -1
- package/dist/drivers/native/codex-app-server.js +4 -2
- package/dist/drivers/native/oneshot.js +12 -2
- package/dist/drivers/native/pi-gate.d.ts +19 -0
- package/dist/drivers/native/pi-gate.js +39 -0
- package/dist/drivers/native/pi-rpc.d.ts +29 -0
- package/dist/drivers/native/pi-rpc.js +483 -0
- package/dist/drivers/runner.js +1 -0
- package/dist/drivers/types.d.ts +1 -1
- package/dist/i18n/catalog.d.ts +5 -0
- package/dist/i18n/messages/auth.d.ts +2 -0
- package/dist/i18n/messages/auth.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/drivers.d.ts +4 -0
- package/dist/i18n/messages/drivers.js +4 -0
- package/dist/i18n/messages/interactive.d.ts +2 -0
- package/dist/i18n/messages/interactive.js +2 -0
- package/dist/i18n/messages/session.js +2 -2
- package/dist/modes/interactive/agent-ui.js +7 -0
- package/dist/session/manager.d.ts +3 -1
- package/dist/session/manager.js +5 -0
- package/dist/session/offload.d.ts +10 -2
- package/dist/session/offload.js +27 -4
- package/dist/tools/background-jobs.d.ts +4 -4
- package/dist/tools/background-jobs.js +4 -4
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +4 -3
- package/dist/tools/task-ctl.js +2 -1
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +27 -1
- package/docs/agents.md +70 -14
- package/docs/en/permissions.md +1 -1
- package/docs/en/providers.md +11 -10
- package/docs/en/sessions.md +2 -1
- package/docs/permissions.md +1 -1
- package/docs/providers.md +16 -11
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +2 -1
- package/package.json +1 -1
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
export declare const SHADOW_DIR = "shadow";
|
|
17
17
|
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
18
18
|
export declare const SHADOW_MAX_FILES = 20000;
|
|
19
|
-
/**
|
|
19
|
+
/** 单次快照耗时上限(毫秒),连续 `SHADOW_SLOW_STREAK` 次超出本会话降级为 tools。 */
|
|
20
20
|
export declare const SHADOW_MAX_SNAPSHOT_MS = 3000;
|
|
21
|
+
/** 连续这么多次快照超时才降级(首次快照不计)。 */
|
|
22
|
+
export declare const SHADOW_SLOW_STREAK = 2;
|
|
21
23
|
export declare function shadowRoot(dataDir: string): string;
|
|
22
24
|
/** cwd 对应的影子仓库目录。 */
|
|
23
25
|
export declare function shadowRepoDir(dataDir: string, cwd: string): string;
|
|
@@ -73,6 +75,8 @@ export declare class ShadowRepo {
|
|
|
73
75
|
private readonly indexFile;
|
|
74
76
|
private initialized;
|
|
75
77
|
private counted;
|
|
78
|
+
/** 连续超时的快照数(首次快照不计),到 `SHADOW_SLOW_STREAK` 才降级。 */
|
|
79
|
+
private slowStreak;
|
|
76
80
|
constructor(dataDir: string, cwd: string, options?: ShadowGitOptions);
|
|
77
81
|
private now;
|
|
78
82
|
private env;
|
|
@@ -23,8 +23,10 @@ import { msg } from "../i18n/index.js";
|
|
|
23
23
|
export const SHADOW_DIR = "shadow";
|
|
24
24
|
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
25
25
|
export const SHADOW_MAX_FILES = 20_000;
|
|
26
|
-
/**
|
|
26
|
+
/** 单次快照耗时上限(毫秒),连续 `SHADOW_SLOW_STREAK` 次超出本会话降级为 tools。 */
|
|
27
27
|
export const SHADOW_MAX_SNAPSHOT_MS = 3_000;
|
|
28
|
+
/** 连续这么多次快照超时才降级(首次快照不计)。 */
|
|
29
|
+
export const SHADOW_SLOW_STREAK = 2;
|
|
28
30
|
/** 单个 git 进程的硬上限,防止卡死。 */
|
|
29
31
|
const GIT_TIMEOUT_MS = 60_000;
|
|
30
32
|
/** 本地配置的版本;改了配置项就加一,旧仓库会补写。 */
|
|
@@ -85,6 +87,8 @@ export class ShadowRepo {
|
|
|
85
87
|
indexFile;
|
|
86
88
|
initialized;
|
|
87
89
|
counted = false;
|
|
90
|
+
/** 连续超时的快照数(首次快照不计),到 `SHADOW_SLOW_STREAK` 才降级。 */
|
|
91
|
+
slowStreak = 0;
|
|
88
92
|
constructor(dataDir, cwd, options = {}) {
|
|
89
93
|
this.cwd = resolve(cwd);
|
|
90
94
|
this.dir = shadowRepoDir(dataDir, this.cwd);
|
|
@@ -207,6 +211,8 @@ export class ShadowRepo {
|
|
|
207
211
|
await this.init();
|
|
208
212
|
// 计时不含一次性的建仓与本地配置(十来个 git 子进程,Windows 上可达数秒),只量快照本身
|
|
209
213
|
const started = this.now();
|
|
214
|
+
// 首次快照要把整个工作区读进索引,天然偏慢,不计入超时
|
|
215
|
+
const first = !this.counted;
|
|
210
216
|
await this.writeExcludes();
|
|
211
217
|
if (!this.counted) {
|
|
212
218
|
const out = await this.run([
|
|
@@ -233,7 +239,10 @@ export class ShadowRepo {
|
|
|
233
239
|
args.push("-p", parent);
|
|
234
240
|
const commit = await this.text(args);
|
|
235
241
|
const elapsed = this.now() - started;
|
|
236
|
-
if (
|
|
242
|
+
if (first)
|
|
243
|
+
return { commit };
|
|
244
|
+
this.slowStreak = elapsed > this.maxSnapshotMs ? this.slowStreak + 1 : 0;
|
|
245
|
+
if (this.slowStreak >= SHADOW_SLOW_STREAK) {
|
|
237
246
|
return {
|
|
238
247
|
commit,
|
|
239
248
|
degrade: {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
|
|
15
15
|
* - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
|
|
16
16
|
* chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
|
|
17
|
+
* - max_tokens 上限缓存(`<dataDir>/models/max-tokens-caps.json`,30 天)载回进程内的表并订阅写回(#152)。
|
|
17
18
|
*/
|
|
18
19
|
import type { ApiRegistry } from "../ai/apis/api.js";
|
|
19
20
|
import { ProviderRegistry } from "../ai/providers/registry.js";
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
|
|
15
15
|
* - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
|
|
16
16
|
* chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
|
|
17
|
+
* - max_tokens 上限缓存(`<dataDir>/models/max-tokens-caps.json`,30 天)载回进程内的表并订阅写回(#152)。
|
|
17
18
|
*/
|
|
18
19
|
import { loadModelsDevIndex } from "../ai/providers/models-dev-cache.js";
|
|
19
20
|
import { ProviderRegistry, discoverLocalModels } from "../ai/providers/registry.js";
|
|
20
21
|
import { mergeDiscoveredModels } from "../ai/providers/discovered-cache.js";
|
|
22
|
+
import { attachMaxTokensCache } from "../ai/providers/max-tokens-cache.js";
|
|
21
23
|
import { lazyIndex } from "../ai/providers/enrich.js";
|
|
22
24
|
import { readOAuthEntry } from "../auth/oauth/token-store.js";
|
|
23
25
|
import { CHATGPT_BASE_URLS, CHATGPT_PROVIDER_ID } from "../auth/chatgpt/presets.js";
|
|
@@ -110,6 +112,7 @@ export async function buildProviderRegistry(input, options = {}) {
|
|
|
110
112
|
}
|
|
111
113
|
// 发现缓存(`ama auth login chatgpt` / `ama models discover chatgpt`)并入模型表为空的供应商
|
|
112
114
|
if (dataDir !== undefined) {
|
|
115
|
+
attachMaxTokensCache(dataDir);
|
|
113
116
|
const flavor = readOAuthEntry(input.authFile, CHATGPT_PROVIDER_ID)?.flavor;
|
|
114
117
|
mergeDiscoveredModels(registry, dataDir, lazyIndex(() => loadModelsDevIndex(dataDir)), (id) => (id === CHATGPT_PROVIDER_ID ? flavor : undefined));
|
|
115
118
|
}
|
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
* - `resume{id}` / `fork{id}`:先在本目录、再在全部子目录里按 id 或唯一前缀找文件;找不到 →
|
|
7
7
|
* `session_not_found`,前缀不唯一 → `invalid_arguments` 并列出候选;
|
|
8
8
|
* - `session-id{id}`:找到则打开,否则以该 id 新建;
|
|
9
|
-
* - `fork{id}
|
|
9
|
+
* - `fork{id}`:打开后从叶子复制出新文件,原会话立即关闭(释放锁);源会话不属于任何 AgentSession,
|
|
10
|
+
* 它的告警(图片读不回,#183)显式交给 `context.log`。
|
|
10
11
|
*/
|
|
11
12
|
import { SessionManager } from "../session/manager.js";
|
|
12
13
|
import type { SessionListItem } from "../session/types.js";
|
|
14
|
+
import type { LogFn } from "./compose-session.js";
|
|
13
15
|
import type { RuntimeDeps, SessionRequest } from "./deps.js";
|
|
14
16
|
/** 按 id / 唯一前缀找会话文件;本 cwd 目录优先。 */
|
|
15
17
|
export declare function findSessionFile(root: string, id: string, cwd?: string): string | undefined;
|
|
16
18
|
export declare function openSession(request: SessionRequest, context: {
|
|
17
19
|
sessionDir: string;
|
|
18
20
|
cwd: string;
|
|
21
|
+
log?: LogFn | undefined;
|
|
19
22
|
}): SessionManager;
|
|
20
23
|
/** 列会话(最新在前);子 Agent 会话缺省跳过,`includeSubagents` 时保留(带 `subagent` 标记)。 */
|
|
21
24
|
export declare function listSessions(context: {
|
|
@@ -23,4 +26,5 @@ export declare function listSessions(context: {
|
|
|
23
26
|
cwd?: string;
|
|
24
27
|
includeSubagents?: boolean;
|
|
25
28
|
}): SessionListItem[];
|
|
26
|
-
|
|
29
|
+
/** `log`:`--fork` 源会话的告警出口(通常是会话的 `options.log`)。 */
|
|
30
|
+
export declare function createSessionStore(log?: LogFn): RuntimeDeps["sessions"];
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - `resume{id}` / `fork{id}`:先在本目录、再在全部子目录里按 id 或唯一前缀找文件;找不到 →
|
|
7
7
|
* `session_not_found`,前缀不唯一 → `invalid_arguments` 并列出候选;
|
|
8
8
|
* - `session-id{id}`:找到则打开,否则以该 id 新建;
|
|
9
|
-
* - `fork{id}
|
|
9
|
+
* - `fork{id}`:打开后从叶子复制出新文件,原会话立即关闭(释放锁);源会话不属于任何 AgentSession,
|
|
10
|
+
* 它的告警(图片读不回,#183)显式交给 `context.log`。
|
|
10
11
|
*/
|
|
11
12
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
13
|
import { join } from "node:path";
|
|
@@ -81,7 +82,9 @@ export function openSession(request, context) {
|
|
|
81
82
|
: SessionManager.open(file);
|
|
82
83
|
}
|
|
83
84
|
case "fork": {
|
|
84
|
-
const source = SessionManager.open(requireFile(root, request.id, cwd)
|
|
85
|
+
const source = SessionManager.open(requireFile(root, request.id, cwd), {
|
|
86
|
+
warn: (message) => context.log?.("warn", message),
|
|
87
|
+
});
|
|
85
88
|
try {
|
|
86
89
|
const leaf = source.leafId();
|
|
87
90
|
if (leaf === null)
|
|
@@ -104,9 +107,10 @@ export function listSessions(context) {
|
|
|
104
107
|
.filter((item) => context.includeSubagents === true || item.subagent !== true)
|
|
105
108
|
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
/** `log`:`--fork` 源会话的告警出口(通常是会话的 `options.log`)。 */
|
|
111
|
+
export function createSessionStore(log) {
|
|
108
112
|
return {
|
|
109
|
-
open: (request, context) => openSession(request, context),
|
|
113
|
+
open: (request, context) => openSession(request, { ...context, log }),
|
|
110
114
|
list: async (context) => listSessions(context),
|
|
111
115
|
show: async (id, context) => {
|
|
112
116
|
const file = requireFile(context.sessionDir, id);
|
package/dist/cli/compose.js
CHANGED
|
@@ -40,7 +40,7 @@ import { builtinTools } from "../tools/registry.js";
|
|
|
40
40
|
import { MEMORY_TOOL, createMemoryTool } from "../memory/tool.js";
|
|
41
41
|
import { takeCodemodeNotice } from "./codemode-notice.js";
|
|
42
42
|
import { buildProviderRegistry } from "./compose-providers.js";
|
|
43
|
-
import { composeSession, emptyComposeState, } from "./compose-session.js";
|
|
43
|
+
import { composeSession, emptyComposeState, stderrLog, } from "./compose-session.js";
|
|
44
44
|
import { createSessionStore } from "./compose-store.js";
|
|
45
45
|
import { defaultSendUser } from "./startup-steps.js";
|
|
46
46
|
import { imageFitOptionsFor } from "../modes/image-input.js";
|
|
@@ -211,7 +211,7 @@ export function createRuntimeDeps(options = {}) {
|
|
|
211
211
|
return registry;
|
|
212
212
|
},
|
|
213
213
|
},
|
|
214
|
-
sessions: createSessionStore(),
|
|
214
|
+
sessions: createSessionStore(options.log ?? stderrLog()),
|
|
215
215
|
tools: { create: (input) => createTools(input, options, state) },
|
|
216
216
|
permissions: {
|
|
217
217
|
create: (input) => new PermissionPipeline({
|
|
@@ -161,6 +161,7 @@ function w5Sections() {
|
|
|
161
161
|
background: oneOf(SUBAGENT_BACKGROUND_MODES),
|
|
162
162
|
autoBackgroundAfterMs: num(0),
|
|
163
163
|
retainSessions: num(0),
|
|
164
|
+
forkMaxContextRatio: num(0.05, 0.95),
|
|
164
165
|
}),
|
|
165
166
|
models: object({ aliases: object({ fast: str(), strong: str() }), enabled: strings }),
|
|
166
167
|
fallbackModel: str(),
|
package/dist/config/key-docs.js
CHANGED
package/dist/config/schema-w5.js
CHANGED
|
@@ -90,6 +90,7 @@ export function validateConfigW5(c, config) {
|
|
|
90
90
|
"background",
|
|
91
91
|
"autoBackgroundAfterMs",
|
|
92
92
|
"retainSessions",
|
|
93
|
+
"forkMaxContextRatio",
|
|
93
94
|
], (s, p) => {
|
|
94
95
|
c.number(s, "maxConcurrent", p, 1, 64);
|
|
95
96
|
c.number(s, "maxPending", p, 0, 1024);
|
|
@@ -97,6 +98,7 @@ export function validateConfigW5(c, config) {
|
|
|
97
98
|
c.oneOf(s, "background", p, SUBAGENT_BACKGROUND_MODES);
|
|
98
99
|
c.number(s, "autoBackgroundAfterMs", p, 0);
|
|
99
100
|
c.number(s, "retainSessions", p, 0);
|
|
101
|
+
c.number(s, "forkMaxContextRatio", p, 0.05, 0.95);
|
|
100
102
|
});
|
|
101
103
|
checkSection(c, config, "models", ["aliases", "enabled"], (s, p) => {
|
|
102
104
|
c.stringArray(s, "enabled", p);
|
|
@@ -101,6 +101,7 @@ const ROWS = [
|
|
|
101
101
|
row("subagents.maxConcurrent", "agents", "nextSession", "deny"),
|
|
102
102
|
row("subagents.maxPending", "agents", "nextSession", "deny"),
|
|
103
103
|
row("subagents.retainSessions", "agents", "nextSession", "deny"),
|
|
104
|
+
row("subagents.forkMaxContextRatio", "agents", "nextSession", "deny"),
|
|
104
105
|
// [W7-B2] 改 task 工具描述(缺省后台 / 前台两种文案),故 prefix
|
|
105
106
|
row("subagents.background", "agents", "restart", "any", { prefix: true }),
|
|
106
107
|
row("subagents.autoBackgroundAfterMs", "agents", "restart", "any"),
|
|
@@ -79,6 +79,11 @@ export interface SubagentsConfig {
|
|
|
79
79
|
autoBackgroundAfterMs?: number;
|
|
80
80
|
/** [M-F] 已结束子会话保留在内存的句柄数(LRU),缺省 4;超出的续聊时从会话文件重开。只认用户级。 */
|
|
81
81
|
retainSessions?: number;
|
|
82
|
+
/**
|
|
83
|
+
* [#149] fork 回落比例:父上一次请求的输入 token 超过(窗口 − compaction.reserveTokens)× 本值时
|
|
84
|
+
* fork 回落为 fresh;0.05–0.95,缺省 0.5。只认用户级。
|
|
85
|
+
*/
|
|
86
|
+
forkMaxContextRatio?: number;
|
|
82
87
|
}
|
|
83
88
|
/** `models.enabled` 的一项:`provider/model[@channel]` 或 `provider/*`。 */
|
|
84
89
|
export declare const MODELS_ENABLED_REF: RegExp;
|
|
@@ -19,16 +19,36 @@ import { type DriverDeps } from "../base.js";
|
|
|
19
19
|
import type { CatalogCandidate } from "../catalog.js";
|
|
20
20
|
import { TurnCollector } from "../turn.js";
|
|
21
21
|
import type { AgentDriver, DriverOpenOptions, DriverProbe, DriverSession } from "../types.js";
|
|
22
|
-
import { type AcpAuthMethod, type AcpContentBlock, type AcpSessionConfigOption, type AcpSessionModeState, type AcpSessionUpdate } from "./types.js";
|
|
22
|
+
import { type AcpAuthMethod, type AcpContentBlock, type AcpPromptUsage, type AcpSessionConfigOption, type AcpSessionModeState, type AcpSessionUpdate } from "./types.js";
|
|
23
23
|
export declare function toAcpContent(blocks: readonly ContentBlock[], images: boolean): AcpContentBlock[];
|
|
24
24
|
/** session/update → DriverEvent(用户消息回放、命令表等忽略)。 */
|
|
25
25
|
export declare function pushAcpUpdate(update: AcpSessionUpdate, turn: TurnCollector): void;
|
|
26
|
+
type TurnUsage = {
|
|
27
|
+
input?: number;
|
|
28
|
+
output?: number;
|
|
29
|
+
cacheRead?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* `session/prompt` 答复的 usage → 本回合用量(ama 口径:input 不含缓存命中)。
|
|
33
|
+
* - `previous`:Agent 报的是会话累计(如 Copilot)时,上一回合的原始累计值,相减得本回合;
|
|
34
|
+
* - 有 `totalTokens` 且它等于 input + output 时,input 含缓存命中部分,减去 cachedRead。
|
|
35
|
+
*/
|
|
36
|
+
export declare function acpTurnUsage(usage: AcpPromptUsage, previous?: AcpPromptUsage): TurnUsage;
|
|
26
37
|
/** ama 模式 → 该 Agent 的模式 id(显式映射 > 同名)。 */
|
|
27
38
|
export declare function pickModeId(mode: PermissionMode, candidate: CatalogCandidate, state: AcpSessionModeState | null | undefined): string | undefined;
|
|
28
39
|
/** `configOptions` 里 category `mode` 的选择项(没有 `modes` 的 Agent 用它表达模式)。 */
|
|
29
40
|
export declare function modeConfigOption(options: readonly AcpSessionConfigOption[] | null | undefined): AcpSessionConfigOption | undefined;
|
|
30
41
|
/** 与 {@link pickModeId} 同一映射,在 mode 类配置项的可选值里找(分组的展开)。 */
|
|
31
42
|
export declare function pickConfigModeValue(mode: PermissionMode, candidate: CatalogCandidate, option: AcpSessionConfigOption | undefined): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* `agents.<id>.model` / task 的 `model` → configOptions 里 category `model` 的可选值:先精确匹配值,
|
|
45
|
+
* 再不分大小写匹配值或名称,最后匹配 `provider/model` 的模型部分。没有就返回 undefined(用 Agent 的缺省)。
|
|
46
|
+
*/
|
|
47
|
+
export declare function pickModelValue(model: string, options: readonly AcpSessionConfigOption[] | null | undefined): {
|
|
48
|
+
configId: string;
|
|
49
|
+
value: string;
|
|
50
|
+
current: boolean;
|
|
51
|
+
} | undefined;
|
|
32
52
|
/** 开会话回 -32000 → `agent_auth_required`,列出 `initialize` 给的认证方法。 */
|
|
33
53
|
export declare function authRequiredError(agentId: string, methods: readonly AcpAuthMethod[], command: readonly string[], cause: unknown): AmaError;
|
|
34
54
|
export declare class AcpDriver implements AgentDriver {
|
|
@@ -40,3 +60,4 @@ export declare class AcpDriver implements AgentDriver {
|
|
|
40
60
|
probe(): Promise<DriverProbe>;
|
|
41
61
|
open(options: DriverOpenOptions): Promise<DriverSession>;
|
|
42
62
|
}
|
|
63
|
+
export {};
|
|
@@ -90,6 +90,34 @@ export function pushAcpUpdate(update, turn) {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* `session/prompt` 答复的 usage → 本回合用量(ama 口径:input 不含缓存命中)。
|
|
95
|
+
* - `previous`:Agent 报的是会话累计(如 Copilot)时,上一回合的原始累计值,相减得本回合;
|
|
96
|
+
* - 有 `totalTokens` 且它等于 input + output 时,input 含缓存命中部分,减去 cachedRead。
|
|
97
|
+
*/
|
|
98
|
+
export function acpTurnUsage(usage, previous) {
|
|
99
|
+
const diff = (key) => {
|
|
100
|
+
const now = usage[key];
|
|
101
|
+
if (now === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
return previous === undefined ? now : Math.max(0, now - (previous[key] ?? 0));
|
|
104
|
+
};
|
|
105
|
+
const input = diff("inputTokens");
|
|
106
|
+
const output = diff("outputTokens");
|
|
107
|
+
const cacheRead = diff("cachedReadTokens");
|
|
108
|
+
const { totalTokens: total, inputTokens: rawIn = 0, outputTokens: rawOut = 0 } = usage;
|
|
109
|
+
const cached = usage.cachedReadTokens ?? 0;
|
|
110
|
+
const includesCache = total !== undefined &&
|
|
111
|
+
cached > 0 &&
|
|
112
|
+
Math.abs(total - (rawIn + rawOut)) < Math.abs(total - (rawIn + rawOut + cached));
|
|
113
|
+
return {
|
|
114
|
+
...(input !== undefined
|
|
115
|
+
? { input: includesCache ? Math.max(0, input - (cacheRead ?? 0)) : input }
|
|
116
|
+
: {}),
|
|
117
|
+
...(output !== undefined ? { output } : {}),
|
|
118
|
+
...(cacheRead !== undefined ? { cacheRead } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
93
121
|
/** ama 模式在 Agent 那边想要的模式 id,按优先级(显式映射 > 同名)。 */
|
|
94
122
|
function wantedModeIds(mode, candidate) {
|
|
95
123
|
const wanted = [candidate.modes?.[mode] ?? mode];
|
|
@@ -113,6 +141,23 @@ export function pickConfigModeValue(mode, candidate, option) {
|
|
|
113
141
|
const values = option.options.flatMap((o) => "options" in o ? o.options.map((x) => x.value) : [o.value]);
|
|
114
142
|
return wantedModeIds(mode, candidate).find((id) => values.includes(id));
|
|
115
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* `agents.<id>.model` / task 的 `model` → configOptions 里 category `model` 的可选值:先精确匹配值,
|
|
146
|
+
* 再不分大小写匹配值或名称,最后匹配 `provider/model` 的模型部分。没有就返回 undefined(用 Agent 的缺省)。
|
|
147
|
+
*/
|
|
148
|
+
export function pickModelValue(model, options) {
|
|
149
|
+
const option = options?.find((o) => o.category === "model" && o.type === "select");
|
|
150
|
+
if (option === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
const values = option.options.flatMap((o) => ("options" in o ? o.options : [o]));
|
|
153
|
+
const lower = model.toLowerCase();
|
|
154
|
+
const hit = values.find((v) => v.value === model) ??
|
|
155
|
+
values.find((v) => v.value.toLowerCase() === lower || v.name.toLowerCase() === lower) ??
|
|
156
|
+
values.find((v) => v.value.toLowerCase().split("/").at(-1) === lower);
|
|
157
|
+
return hit === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: { configId: option.id, value: hit.value, current: option.currentValue === hit.value };
|
|
160
|
+
}
|
|
116
161
|
/** 一条认证方法给人看的样子:terminal 型附上要在终端里跑的命令。 */
|
|
117
162
|
function authMethodText(method, command) {
|
|
118
163
|
if (method.type !== "terminal")
|
|
@@ -181,6 +226,9 @@ class AcpDriverSession {
|
|
|
181
226
|
notices = [];
|
|
182
227
|
/** 本回合已发过 cancel。 */
|
|
183
228
|
cancelling = false;
|
|
229
|
+
/** Agent 的 usage 是会话累计时,上一回合的原始值({@link acpTurnUsage})。 */
|
|
230
|
+
usageTotals;
|
|
231
|
+
cumulativeUsage = false;
|
|
184
232
|
constructor(agentId, transport, deps) {
|
|
185
233
|
this.agentId = agentId;
|
|
186
234
|
this.transport = transport;
|
|
@@ -200,6 +248,7 @@ class AcpDriverSession {
|
|
|
200
248
|
onProtocolError: (_line, reason) => this.deps.log?.("debug", `${this.agentId}: ${reason}`),
|
|
201
249
|
});
|
|
202
250
|
const signal = options.signal;
|
|
251
|
+
this.cumulativeUsage = candidate.acpUsage === "session";
|
|
203
252
|
await this.client.initialize(signal);
|
|
204
253
|
let opened;
|
|
205
254
|
try {
|
|
@@ -228,6 +277,13 @@ class AcpDriverSession {
|
|
|
228
277
|
else if (modes != null || modeOption !== undefined) {
|
|
229
278
|
this.notices.push(msg().drivers.agent.noMatchingMode(this.agentId, options.mode));
|
|
230
279
|
}
|
|
280
|
+
if (options.model !== undefined) {
|
|
281
|
+
const model = pickModelValue(options.model, opened.configOptions);
|
|
282
|
+
if (model === undefined)
|
|
283
|
+
this.notices.push(msg().drivers.agent.noMatchingModel(this.agentId, options.model));
|
|
284
|
+
else if (!model.current)
|
|
285
|
+
await this.client.setConfigOption(this.id, model.configId, model.value);
|
|
286
|
+
}
|
|
231
287
|
}
|
|
232
288
|
/** 续聊优先 resume(不回放),其次 load(回放丢弃),都不行就新开;设好 `this.id`。 */
|
|
233
289
|
async openSession(options) {
|
|
@@ -291,11 +347,9 @@ class AcpDriverSession {
|
|
|
291
347
|
});
|
|
292
348
|
const usage = result.usage;
|
|
293
349
|
if (usage != null) {
|
|
294
|
-
turn.mergeUsage({
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
...(usage.cachedReadTokens !== undefined ? { cacheRead: usage.cachedReadTokens } : {}),
|
|
298
|
-
});
|
|
350
|
+
turn.mergeUsage(acpTurnUsage(usage, this.cumulativeUsage ? (this.usageTotals ?? {}) : undefined));
|
|
351
|
+
if (this.cumulativeUsage)
|
|
352
|
+
this.usageTotals = usage;
|
|
299
353
|
}
|
|
300
354
|
return turn.result(result.stopReason);
|
|
301
355
|
}
|
package/dist/drivers/agents.js
CHANGED
|
@@ -17,6 +17,7 @@ import { HostRunnerRegistry } from "./host-runners.js";
|
|
|
17
17
|
import { ClaudeStreamDriver } from "./native/claude-stream.js";
|
|
18
18
|
import { CodexAppServerDriver } from "./native/codex-app-server.js";
|
|
19
19
|
import { OneshotDriver } from "./native/oneshot.js";
|
|
20
|
+
import { PiRpcDriver } from "./native/pi-rpc.js";
|
|
20
21
|
import { PidRegistry, pidsFile } from "./pids.js";
|
|
21
22
|
import { poolFromConfig } from "./pool.js";
|
|
22
23
|
import { ProgramProbe } from "./probe.js";
|
|
@@ -33,6 +34,8 @@ export function createDriver(agentId, candidate, deps) {
|
|
|
33
34
|
return new ClaudeStreamDriver(agentId, candidate, deps);
|
|
34
35
|
case "codex-app-server":
|
|
35
36
|
return new CodexAppServerDriver(agentId, candidate, deps);
|
|
37
|
+
case "pi-rpc":
|
|
38
|
+
return new PiRpcDriver(agentId, candidate, deps);
|
|
36
39
|
case "oneshot":
|
|
37
40
|
return new OneshotDriver(agentId, candidate, deps);
|
|
38
41
|
}
|
|
@@ -24,6 +24,8 @@ export interface CatalogCandidate {
|
|
|
24
24
|
modes?: Partial<Record<PermissionMode, string>>;
|
|
25
25
|
permissions?: DriverCapabilities["permissions"];
|
|
26
26
|
usage?: DriverCapabilities["usage"];
|
|
27
|
+
/** `session/prompt` 答复的 usage 是会话累计(缺省按本回合)。 */
|
|
28
|
+
acpUsage?: "session";
|
|
27
29
|
/** 一次性打印模式的方言。 */
|
|
28
30
|
oneshot?: "claude" | "codex" | "gemini";
|
|
29
31
|
}
|
package/dist/drivers/catalog.js
CHANGED
|
@@ -17,12 +17,55 @@ const ALL_MODES = [
|
|
|
17
17
|
"auto",
|
|
18
18
|
"full-auto",
|
|
19
19
|
];
|
|
20
|
+
/**
|
|
21
|
+
* 模式 id 与 ama 不同名的 ACP Agent 的映射(2026-10 实测)。没有映射时 ama 会留在 Agent 自己的当前模式,
|
|
22
|
+
* 而它可能比 ama 宽(如用户把 Claude 的缺省设成 bypassPermissions),所以已知的 Agent 一律写全。
|
|
23
|
+
* 从不映射到放开全部权限的模式(Claude `bypassPermissions`、Codex `agent-full-access`、Copilot autopilot)。
|
|
24
|
+
*/
|
|
25
|
+
const CLAUDE_ACP_MODES = {
|
|
26
|
+
plan: "plan",
|
|
27
|
+
default: "default",
|
|
28
|
+
"auto-edit": "acceptEdits",
|
|
29
|
+
auto: "auto",
|
|
30
|
+
"full-auto": "auto",
|
|
31
|
+
};
|
|
32
|
+
/** codex-acp:`read-only` 是只读沙箱、写操作要审批(等同 app-server 的 on-request / read-only)。 */
|
|
33
|
+
const CODEX_ACP_MODES = {
|
|
34
|
+
plan: "read-only",
|
|
35
|
+
default: "read-only",
|
|
36
|
+
"auto-edit": "workspace-write",
|
|
37
|
+
auto: "agent",
|
|
38
|
+
"full-auto": "agent",
|
|
39
|
+
};
|
|
40
|
+
const COPILOT_MODE = "https://agentclientprotocol.com/protocol/session-modes#";
|
|
41
|
+
const COPILOT_MODES = {
|
|
42
|
+
plan: `${COPILOT_MODE}plan`,
|
|
43
|
+
default: `${COPILOT_MODE}agent`,
|
|
44
|
+
"auto-edit": `${COPILOT_MODE}agent`,
|
|
45
|
+
auto: `${COPILOT_MODE}agent`,
|
|
46
|
+
"full-auto": `${COPILOT_MODE}agent`,
|
|
47
|
+
};
|
|
48
|
+
/** Cursor CLI(官方 ACP 文档):`plan` 只读,`agent` 是完整工具权限(需要授权的仍发 request_permission)。 */
|
|
49
|
+
const CURSOR_MODES = {
|
|
50
|
+
plan: "plan",
|
|
51
|
+
default: "agent",
|
|
52
|
+
"auto-edit": "agent",
|
|
53
|
+
auto: "agent",
|
|
54
|
+
"full-auto": "agent",
|
|
55
|
+
};
|
|
20
56
|
export const DRIVER_CATALOG = [
|
|
21
57
|
{
|
|
22
58
|
agentId: "claude",
|
|
23
59
|
label: "Claude Code",
|
|
24
60
|
candidates: [
|
|
25
|
-
{
|
|
61
|
+
{
|
|
62
|
+
kind: "acp-adapter",
|
|
63
|
+
program: "claude-agent-acp",
|
|
64
|
+
args: [],
|
|
65
|
+
verified: ">=0.89.0 <1.0.0",
|
|
66
|
+
usage: "usd",
|
|
67
|
+
modes: CLAUDE_ACP_MODES,
|
|
68
|
+
},
|
|
26
69
|
{
|
|
27
70
|
kind: "claude-stream",
|
|
28
71
|
program: "claude",
|
|
@@ -37,7 +80,14 @@ export const DRIVER_CATALOG = [
|
|
|
37
80
|
agentId: "codex",
|
|
38
81
|
label: "Codex",
|
|
39
82
|
candidates: [
|
|
40
|
-
{
|
|
83
|
+
{
|
|
84
|
+
kind: "acp-adapter",
|
|
85
|
+
program: "codex-acp",
|
|
86
|
+
args: [],
|
|
87
|
+
verified: ">=2.2.0 <3.0.0",
|
|
88
|
+
usage: "tokens",
|
|
89
|
+
modes: CODEX_ACP_MODES,
|
|
90
|
+
},
|
|
41
91
|
{
|
|
42
92
|
kind: "codex-app-server",
|
|
43
93
|
program: "codex",
|
|
@@ -70,15 +120,44 @@ export const DRIVER_CATALOG = [
|
|
|
70
120
|
{
|
|
71
121
|
agentId: "opencode",
|
|
72
122
|
label: "OpenCode",
|
|
73
|
-
candidates: [{ kind: "acp", program: "opencode", args: ["acp"] }],
|
|
123
|
+
candidates: [{ kind: "acp", program: "opencode", args: ["acp"], verified: ">=1.18.0 <2.0.0" }],
|
|
74
124
|
},
|
|
75
125
|
{
|
|
76
126
|
agentId: "copilot",
|
|
77
127
|
label: "GitHub Copilot CLI",
|
|
78
128
|
candidates: [
|
|
79
|
-
{
|
|
129
|
+
{
|
|
130
|
+
kind: "acp",
|
|
131
|
+
program: "copilot",
|
|
132
|
+
args: ["--acp", "--stdio"],
|
|
133
|
+
verified: ">=1.0.95 <2.0.0",
|
|
134
|
+
usage: "requests",
|
|
135
|
+
modes: COPILOT_MODES,
|
|
136
|
+
acpUsage: "session",
|
|
137
|
+
},
|
|
80
138
|
],
|
|
81
139
|
},
|
|
140
|
+
{
|
|
141
|
+
agentId: "pi",
|
|
142
|
+
label: "Pi",
|
|
143
|
+
// 社区 ACP 适配器 pi-acp 不发 request_permission(pi 没有审批通道),会让工具不经人直接执行,不收录
|
|
144
|
+
candidates: [
|
|
145
|
+
{
|
|
146
|
+
kind: "pi-rpc",
|
|
147
|
+
program: "pi",
|
|
148
|
+
args: ["--mode", "rpc"],
|
|
149
|
+
verified: ">=1.1.0 <2.0.0",
|
|
150
|
+
usage: "usd",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
agentId: "cursor",
|
|
156
|
+
label: "Cursor CLI",
|
|
157
|
+
// 未实测(本机未装):按官方文档 `cursor-agent acp`,模式 agent / plan / ask;`cursor/ask_question`
|
|
158
|
+
// 等阻塞式扩展方法 ama 不实现(回 method not found,不代答)
|
|
159
|
+
candidates: [{ kind: "acp", program: "cursor-agent", args: ["acp"], modes: CURSOR_MODES }],
|
|
160
|
+
},
|
|
82
161
|
{
|
|
83
162
|
agentId: "goose",
|
|
84
163
|
label: "Goose",
|
|
@@ -142,6 +221,17 @@ export function candidateCapabilities(c) {
|
|
|
142
221
|
usage: "tokens",
|
|
143
222
|
images: false,
|
|
144
223
|
};
|
|
224
|
+
case "pi-rpc":
|
|
225
|
+
// 审批经 ama 加载的审批闸扩展交给人(native/pi-gate.ts)
|
|
226
|
+
return {
|
|
227
|
+
resume: "resume",
|
|
228
|
+
list: false,
|
|
229
|
+
permissions: "interactive",
|
|
230
|
+
steer: true,
|
|
231
|
+
modes: ALL_MODES,
|
|
232
|
+
usage: "usd",
|
|
233
|
+
images: true,
|
|
234
|
+
};
|
|
145
235
|
case "oneshot":
|
|
146
236
|
return {
|
|
147
237
|
resume: c.oneshot === "gemini" ? "none" : "resume",
|
|
@@ -39,7 +39,15 @@ export interface ClaudeResult {
|
|
|
39
39
|
tool_name?: string;
|
|
40
40
|
}[];
|
|
41
41
|
errors?: string[];
|
|
42
|
+
/** 进程内累计,按模型分;`contextWindow` 是该模型的上下文窗口(2.1.x 起)。 */
|
|
43
|
+
modelUsage?: Record<string, {
|
|
44
|
+
contextWindow?: number;
|
|
45
|
+
}>;
|
|
42
46
|
}
|
|
47
|
+
/** 一条主线 assistant 消息的用量 = 这次请求后的上下文占用(输入 + 缓存读写 + 输出)。 */
|
|
48
|
+
export declare function claudeContextTokens(usage: unknown): number | undefined;
|
|
49
|
+
/** result.modelUsage 里某模型(缺省取最大)的上下文窗口。 */
|
|
50
|
+
export declare function claudeContextWindow(result: ClaudeResult, model: string | undefined): number | undefined;
|
|
43
51
|
/** result.subtype → 回合结束原因;`undefined` 表示执行出错(调用方按错误处理)。 */
|
|
44
52
|
export declare function claudeStopReason(result: ClaudeResult, interrupted: boolean): "end_turn" | "max_turn_requests" | "cancelled" | undefined;
|
|
45
53
|
/** 只保留会话范围的权限建议(`localSettings` 等会改用户 / 项目配置文件,不能替人写)。 */
|
|
@@ -81,6 +81,33 @@ export function claudePermissionMode(mode, manualName) {
|
|
|
81
81
|
}
|
|
82
82
|
/** 只能由人回答的提问类工具(ama 不代答)。 */
|
|
83
83
|
export const CLAUDE_QUESTION_TOOLS = new Set(["AskUserQuestion"]);
|
|
84
|
+
/** 一条主线 assistant 消息的用量 = 这次请求后的上下文占用(输入 + 缓存读写 + 输出)。 */
|
|
85
|
+
export function claudeContextTokens(usage) {
|
|
86
|
+
if (typeof usage !== "object" || usage === null)
|
|
87
|
+
return undefined;
|
|
88
|
+
const u = usage;
|
|
89
|
+
let total = 0;
|
|
90
|
+
for (const key of [
|
|
91
|
+
"input_tokens",
|
|
92
|
+
"cache_read_input_tokens",
|
|
93
|
+
"cache_creation_input_tokens",
|
|
94
|
+
"output_tokens",
|
|
95
|
+
])
|
|
96
|
+
if (typeof u[key] === "number")
|
|
97
|
+
total += u[key];
|
|
98
|
+
return total > 0 ? total : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** result.modelUsage 里某模型(缺省取最大)的上下文窗口。 */
|
|
101
|
+
export function claudeContextWindow(result, model) {
|
|
102
|
+
const entries = result.modelUsage ?? {};
|
|
103
|
+
const hit = model !== undefined ? entries[model]?.contextWindow : undefined;
|
|
104
|
+
if (typeof hit === "number" && hit > 0)
|
|
105
|
+
return hit;
|
|
106
|
+
const windows = Object.values(entries)
|
|
107
|
+
.map((e) => e.contextWindow)
|
|
108
|
+
.filter((w) => typeof w === "number" && w > 0);
|
|
109
|
+
return windows.length > 0 ? Math.max(...windows) : undefined;
|
|
110
|
+
}
|
|
84
111
|
/** result.subtype → 回合结束原因;`undefined` 表示执行出错(调用方按错误处理)。 */
|
|
85
112
|
export function claudeStopReason(result, interrupted) {
|
|
86
113
|
if (interrupted)
|
|
@@ -20,7 +20,7 @@ import { createLineReader } from "../../modes/rpc/jsonl.js";
|
|
|
20
20
|
import { armCancelWatchdog, probeCandidate, spawnOf } from "../base.js";
|
|
21
21
|
import { stderrHint } from "../process.js";
|
|
22
22
|
import { TurnCollector, oneLine } from "../turn.js";
|
|
23
|
-
import { CLAUDE_QUESTION_TOOLS, claudePermissionMode, claudeStopReason, claudeTodos, claudeToolKind, claudeToolLocations, claudeToolTitle, sessionScopedSuggestions, } from "./claude-normalize.js";
|
|
23
|
+
import { CLAUDE_QUESTION_TOOLS, claudeContextTokens, claudeContextWindow, claudePermissionMode, claudeStopReason, claudeTodos, claudeToolKind, claudeToolLocations, claudeToolTitle, sessionScopedSuggestions, } from "./claude-normalize.js";
|
|
24
24
|
import { msg as messages } from "../../i18n/index.js";
|
|
25
25
|
export function claudeArgs(options, sessionId, manualName = "manual") {
|
|
26
26
|
const args = [
|
|
@@ -116,6 +116,8 @@ class ClaudeStreamSession {
|
|
|
116
116
|
interrupted = false;
|
|
117
117
|
sawPartialText = false;
|
|
118
118
|
lastTotalCost = 0;
|
|
119
|
+
/** 最近一条主线 assistant 消息的上下文占用与模型(result 里查窗口用)。 */
|
|
120
|
+
context = {};
|
|
119
121
|
closed = false;
|
|
120
122
|
exited = false;
|
|
121
123
|
constructor(agentId, id, transport, deps) {
|
|
@@ -218,6 +220,13 @@ class ClaudeStreamSession {
|
|
|
218
220
|
...(u.cache_read_input_tokens !== undefined ? { cacheRead: u.cache_read_input_tokens } : {}),
|
|
219
221
|
...(cost !== undefined ? { costUsd: cost } : {}),
|
|
220
222
|
});
|
|
223
|
+
const window = claudeContextWindow(result, this.context.model);
|
|
224
|
+
if (this.context.tokens !== undefined || window !== undefined)
|
|
225
|
+
turn.push({
|
|
226
|
+
type: "usage",
|
|
227
|
+
...(this.context.tokens !== undefined ? { contextTokens: this.context.tokens } : {}),
|
|
228
|
+
...(window !== undefined ? { contextWindow: window } : {}),
|
|
229
|
+
});
|
|
221
230
|
if (typeof result.result === "string" && result.result !== "")
|
|
222
231
|
turn.setFinalText(result.result);
|
|
223
232
|
const denials = result.permission_denials?.length ?? 0;
|
|
@@ -337,6 +346,12 @@ class ClaudeStreamSession {
|
|
|
337
346
|
return;
|
|
338
347
|
// 子 Agent(Task)内部的消息不进最终文本
|
|
339
348
|
const nested = typeof parent === "string" && parent !== "";
|
|
349
|
+
const tokens = nested ? undefined : claudeContextTokens(message["usage"]);
|
|
350
|
+
if (tokens !== undefined)
|
|
351
|
+
this.context = {
|
|
352
|
+
tokens,
|
|
353
|
+
...(typeof message["model"] === "string" ? { model: message["model"] } : {}),
|
|
354
|
+
};
|
|
340
355
|
for (const block of message["content"]) {
|
|
341
356
|
if (block["type"] === "text" &&
|
|
342
357
|
!nested &&
|