@armadra/agent 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/CHANGELOG.zh-CN.md +32 -0
- package/dist/agent/session-classifier.d.ts +2 -0
- package/dist/agent/session-classifier.js +24 -3
- package/dist/agent/session-extensions.d.ts +1 -0
- package/dist/agent/session-extensions.js +3 -0
- package/dist/agent/session-subagent.js +9 -2
- package/dist/agent/subagent-background.d.ts +5 -1
- package/dist/agent/subagent-background.js +13 -1
- package/dist/agent/subagent-fork.d.ts +10 -5
- package/dist/agent/subagent-fork.js +22 -6
- package/dist/agent/subagent-registry.d.ts +1 -1
- package/dist/agent/subagent-registry.js +1 -1
- package/dist/agents/catalog.d.ts +1 -0
- package/dist/ai/apis/anthropic-request.js +6 -1
- package/dist/ai/apis/cache-params.js +2 -1
- package/dist/ai/apis/google-request.js +2 -1
- package/dist/ai/apis/max-tokens.d.ts +4 -0
- package/dist/ai/apis/max-tokens.js +12 -1
- package/dist/ai/apis/openai-request.js +3 -1
- package/dist/ai/apis/openai-responses-request.js +2 -1
- package/dist/ai/json-body.d.ts +33 -1
- package/dist/ai/json-body.js +96 -18
- package/dist/ai/providers/enrich.d.ts +6 -2
- package/dist/ai/providers/enrich.js +13 -2
- package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
- package/dist/ai/providers/max-tokens-cache.js +95 -0
- package/dist/ai/providers/registry.js +3 -3
- package/dist/bundle/ama.cjs +982 -442
- package/dist/checkpoints/shadow-git.d.ts +5 -1
- package/dist/checkpoints/shadow-git.js +11 -2
- package/dist/cli/bootstrap.js +13 -5
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-store.d.ts +6 -2
- package/dist/cli/compose-store.js +8 -4
- package/dist/cli/compose.js +2 -2
- package/dist/cli/deps.d.ts +1 -1
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema-w5.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +5 -0
- package/dist/i18n/catalog.d.ts +2 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/interactive.d.ts +2 -0
- package/dist/i18n/messages/interactive.js +2 -0
- package/dist/i18n/messages/session.js +2 -2
- package/dist/modes/acp/acp-server.js +17 -15
- package/dist/modes/acp/acp-sessions.d.ts +14 -1
- package/dist/modes/acp/acp-sessions.js +44 -1
- package/dist/modes/interactive/agent-ui.js +7 -0
- package/dist/session/manager.d.ts +18 -0
- package/dist/session/manager.js +75 -14
- package/dist/session/offload.d.ts +59 -0
- package/dist/session/offload.js +218 -0
- package/dist/session/store.d.ts +12 -4
- package/dist/session/store.js +18 -8
- package/dist/tools/background-jobs.d.ts +4 -4
- package/dist/tools/background-jobs.js +4 -4
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +4 -3
- package/dist/tools/read-lines.d.ts +39 -0
- package/dist/tools/read-lines.js +163 -86
- package/dist/tools/read.js +3 -3
- package/dist/tools/task-ctl.js +2 -1
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +27 -1
- package/docs/acp.md +6 -3
- package/docs/agents.md +14 -3
- package/docs/en/acp.md +2 -2
- package/docs/en/permissions.md +1 -1
- package/docs/en/providers.md +4 -4
- package/docs/en/sessions.md +2 -1
- package/docs/permissions.md +1 -1
- package/docs/providers.md +9 -5
- package/docs/session-format.md +11 -0
- package/docs/sessions.md +2 -1
- package/package.json +1 -1
|
@@ -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: {
|
package/dist/cli/bootstrap.js
CHANGED
|
@@ -76,7 +76,8 @@ export async function bootstrap(argsIn, deps, io) {
|
|
|
76
76
|
request = { kind: "resume", id };
|
|
77
77
|
}
|
|
78
78
|
const sessionRequest = request;
|
|
79
|
-
|
|
79
|
+
// [#165] 会话替换后跟随当前会话(闭包只经它取,启动会话的 manager 不被常驻)
|
|
80
|
+
let sessionManager = await step(ExitCode.Session, m().steps.session, () => deps.sessions.open(sessionRequest, { sessionDir: paths.sessionDir, cwd: paths.cwd }));
|
|
80
81
|
let sessionCwd = sessionManager.cwd;
|
|
81
82
|
if (!existsSync(sessionCwd)) {
|
|
82
83
|
const replacement = interactive ? await deps.ui?.askCwd?.(sessionCwd) : undefined;
|
|
@@ -281,6 +282,8 @@ export async function bootstrap(argsIn, deps, io) {
|
|
|
281
282
|
uiBroker: () => uiBroker,
|
|
282
283
|
onSessionReplaced: (next) => {
|
|
283
284
|
session = next;
|
|
285
|
+
sessionManager = next.manager ?? sessionManager;
|
|
286
|
+
assembly.sessionManager = sessionManager;
|
|
284
287
|
},
|
|
285
288
|
sessionStartContext: () => sessionStartContext,
|
|
286
289
|
unattended,
|
|
@@ -302,7 +305,7 @@ export async function bootstrap(argsIn, deps, io) {
|
|
|
302
305
|
if (Object.keys(overrides).length > 0)
|
|
303
306
|
assembly.overrides = overrides;
|
|
304
307
|
session = await step(ExitCode.RuntimeError, m().steps.assembly, () => deps.session.create(assembly));
|
|
305
|
-
const active = session;
|
|
308
|
+
const active = session; // 只同步使用:闭包捕获它会让启动会话常驻(#165)
|
|
306
309
|
let disposed;
|
|
307
310
|
shutdown = (reason) => {
|
|
308
311
|
disposed ??= (async () => {
|
|
@@ -310,7 +313,7 @@ export async function bootstrap(argsIn, deps, io) {
|
|
|
310
313
|
await hooks.run("SessionEnd", { reason }).catch(() => undefined);
|
|
311
314
|
await disposeHost(host, (e) => warn(m().hostDisposeFailed(String(e))));
|
|
312
315
|
// 会话被替换过时 dispose 当前那个(旧会话由替换方负责)
|
|
313
|
-
await
|
|
316
|
+
await session?.dispose();
|
|
314
317
|
})();
|
|
315
318
|
return disposed;
|
|
316
319
|
};
|
|
@@ -339,8 +342,13 @@ export async function bootstrap(argsIn, deps, io) {
|
|
|
339
342
|
providers,
|
|
340
343
|
model,
|
|
341
344
|
thinkingLevel,
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
// [#165] 跟随当前会话(ACP 关掉启动会话后它可被回收)
|
|
346
|
+
get sessionManager() {
|
|
347
|
+
return sessionManager;
|
|
348
|
+
},
|
|
349
|
+
get session() {
|
|
350
|
+
return session;
|
|
351
|
+
},
|
|
344
352
|
hooks,
|
|
345
353
|
host,
|
|
346
354
|
permission,
|
|
@@ -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({
|
package/dist/cli/deps.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ export interface SessionAssembly {
|
|
|
107
107
|
/**
|
|
108
108
|
* 模式层把当前会话换成另一个(`/new`、`/resume`、`/fork`、RPC `switch_session`)后调用:
|
|
109
109
|
* bootstrap 让 HostApi.session.*、宿主 sendUser、Hook 公共字段与退出时的 dispose 跟随 `next`。
|
|
110
|
-
* 旧会话由调用方自行 dispose;`Runtime.session`
|
|
110
|
+
* 旧会话由调用方自行 dispose;`Runtime.session` / `sessionManager` 也随之指向 `next`(#165)。
|
|
111
111
|
*/
|
|
112
112
|
onSessionReplaced(next: AgentSession): void;
|
|
113
113
|
/** SessionStart Hook 的 additionalContext(系统提示 hooks 节);首次装配系统提示时读取。 */
|
|
@@ -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;
|
package/dist/i18n/catalog.d.ts
CHANGED
|
@@ -534,6 +534,7 @@ declare const en: {
|
|
|
534
534
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
535
535
|
compactionFailed: (error: string) => string;
|
|
536
536
|
compactionCancelled: string;
|
|
537
|
+
imageReadBackFailed: (id: string) => string;
|
|
537
538
|
};
|
|
538
539
|
statusLine: {
|
|
539
540
|
usage: string;
|
|
@@ -1446,6 +1447,7 @@ declare const en: {
|
|
|
1446
1447
|
"subagents.maxPending": string;
|
|
1447
1448
|
"subagents.retainSessions": string;
|
|
1448
1449
|
"subagents.defaultModel": string;
|
|
1450
|
+
"subagents.forkMaxContextRatio": string;
|
|
1449
1451
|
"subagents.background": string;
|
|
1450
1452
|
"subagents.autoBackgroundAfterMs": string;
|
|
1451
1453
|
models: string;
|
|
@@ -115,6 +115,7 @@ export declare const en: {
|
|
|
115
115
|
"subagents.maxPending": string;
|
|
116
116
|
"subagents.retainSessions": string;
|
|
117
117
|
"subagents.defaultModel": string;
|
|
118
|
+
"subagents.forkMaxContextRatio": string;
|
|
118
119
|
"subagents.background": string;
|
|
119
120
|
"subagents.autoBackgroundAfterMs": string;
|
|
120
121
|
models: string;
|
|
@@ -269,6 +270,7 @@ export declare const zh: {
|
|
|
269
270
|
"subagents.maxPending": string;
|
|
270
271
|
"subagents.retainSessions": string;
|
|
271
272
|
"subagents.defaultModel": string;
|
|
273
|
+
"subagents.forkMaxContextRatio": string;
|
|
272
274
|
"subagents.background": string;
|
|
273
275
|
"subagents.autoBackgroundAfterMs": string;
|
|
274
276
|
models: string;
|
|
@@ -164,6 +164,7 @@ const keysEn = {
|
|
|
164
164
|
"subagents.maxPending": "Queue limit; task fails beyond it",
|
|
165
165
|
"subagents.retainSessions": "Finished sub-sessions kept in memory (least recently used first out); others reopen from their session file when continued",
|
|
166
166
|
"subagents.defaultModel": "Default sub-session model provider/model[@channel]",
|
|
167
|
+
"subagents.forkMaxContextRatio": "Fork sub-agents fall back to fresh when the parent's last request used more than this share of (window − compaction.reserveTokens); 0.05–0.95, default 0.5",
|
|
167
168
|
"subagents.background": "Whether task runs in the background by default: auto = background in the TUI / RPC / ACP and foreground with -p; always / never are fixed. The call argument and the agent type's background: override it; user, project and host level",
|
|
168
169
|
"subagents.autoBackgroundAfterMs": "Move a foreground task to the background after it has run this many milliseconds; 0 disables; user and project level",
|
|
169
170
|
models: "Model aliases and the model picker list; user level only (effective from wave 5 W5-G)",
|
|
@@ -292,6 +293,7 @@ const keysZh = {
|
|
|
292
293
|
"subagents.maxPending": "排队上限,超出时 task 报错",
|
|
293
294
|
"subagents.retainSessions": "内存中保留的已结束子会话数(最久未用的先释放),其余续聊时从会话文件重开",
|
|
294
295
|
"subagents.defaultModel": "子会话缺省模型 provider/model[@channel]",
|
|
296
|
+
"subagents.forkMaxContextRatio": "fork 子 Agent 的回落比例:父上一次请求超过(窗口 − compaction.reserveTokens)的这一比例时改为 fresh;0.05–0.95,缺省 0.5",
|
|
295
297
|
"subagents.background": "task 缺省是否后台:auto = TUI / RPC / ACP 下后台、-p 下前台;always / never 固定。调用参数与类型定义的 background: 优先;用户、项目、宿主级都认",
|
|
296
298
|
"subagents.autoBackgroundAfterMs": "前台任务运行超过该毫秒数自动转后台;0 关闭;用户、项目级都认",
|
|
297
299
|
models: "模型别名与模型选择器清单;只认用户级(第五波 W5-G 起生效)",
|
|
@@ -115,6 +115,7 @@ export declare const en: {
|
|
|
115
115
|
"subagents.maxPending": string;
|
|
116
116
|
"subagents.retainSessions": string;
|
|
117
117
|
"subagents.defaultModel": string;
|
|
118
|
+
"subagents.forkMaxContextRatio": string;
|
|
118
119
|
"subagents.background": string;
|
|
119
120
|
"subagents.autoBackgroundAfterMs": string;
|
|
120
121
|
models: string;
|
|
@@ -383,6 +384,7 @@ export declare const zh: {
|
|
|
383
384
|
"subagents.maxPending": string;
|
|
384
385
|
"subagents.retainSessions": string;
|
|
385
386
|
"subagents.defaultModel": string;
|
|
387
|
+
"subagents.forkMaxContextRatio": string;
|
|
386
388
|
"subagents.background": string;
|
|
387
389
|
"subagents.autoBackgroundAfterMs": string;
|
|
388
390
|
models: string;
|
|
@@ -85,6 +85,7 @@ export declare const en: {
|
|
|
85
85
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
86
86
|
compactionFailed: (error: string) => string;
|
|
87
87
|
compactionCancelled: string;
|
|
88
|
+
imageReadBackFailed: (id: string) => string;
|
|
88
89
|
};
|
|
89
90
|
statusLine: {
|
|
90
91
|
usage: string;
|
|
@@ -335,6 +336,7 @@ export declare const zh: {
|
|
|
335
336
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
336
337
|
compactionFailed: (error: string) => string;
|
|
337
338
|
compactionCancelled: string;
|
|
339
|
+
imageReadBackFailed: (id: string) => string;
|
|
338
340
|
};
|
|
339
341
|
statusLine: {
|
|
340
342
|
usage: string;
|
|
@@ -99,6 +99,7 @@ export const en = {
|
|
|
99
99
|
backgroundStopped: (jobId, command) => `Background command ${jobId} stopped: ${command}`,
|
|
100
100
|
compactionFailed: (error) => `Compaction failed: ${error}`,
|
|
101
101
|
compactionCancelled: "Compaction cancelled",
|
|
102
|
+
imageReadBackFailed: (id) => `Session image for entry ${id} could not be read back (file changed); it stays empty`,
|
|
102
103
|
},
|
|
103
104
|
statusLine: {
|
|
104
105
|
usage: "Usage: /statusline [full|compact]",
|
|
@@ -234,6 +235,7 @@ export const zh = {
|
|
|
234
235
|
backgroundStopped: (jobId, command) => `后台命令 ${jobId} 已停止:${command}`,
|
|
235
236
|
compactionFailed: (error) => `压缩失败:${error}`,
|
|
236
237
|
compactionCancelled: "压缩已取消",
|
|
238
|
+
imageReadBackFailed: (id) => `会话文件已改变,条目 ${id} 的图片读不回,保留为空`,
|
|
237
239
|
},
|
|
238
240
|
statusLine: {
|
|
239
241
|
usage: "用法:/statusline [full|compact]",
|
|
@@ -88,7 +88,7 @@ export const en = {
|
|
|
88
88
|
cwdIsHome: "the working directory is the home directory",
|
|
89
89
|
gitNotFound: "git not found",
|
|
90
90
|
tooManyFiles: (files, max) => `the working directory has ${plural(files, "file")} (limit ${max})`,
|
|
91
|
-
tooSlow: (seconds, max) => `
|
|
91
|
+
tooSlow: (seconds, max) => `snapshots took longer than ${max} s twice in a row (latest ${seconds} s)`,
|
|
92
92
|
gitFailed: (command, code, detail) => `git ${command} failed (${code})${detail === "" ? "" : `: ${detail}`}`,
|
|
93
93
|
noShadowCommit: "the checkpoint has no shadow commit",
|
|
94
94
|
shadowCommitMissing: (commit) => `shadow commit ${commit} does not exist`,
|
|
@@ -211,7 +211,7 @@ export const zh = {
|
|
|
211
211
|
cwdIsHome: "工作目录是家目录",
|
|
212
212
|
gitNotFound: "找不到 git",
|
|
213
213
|
tooManyFiles: (files, max) => `工作目录有 ${files} 个文件(上限 ${max})`,
|
|
214
|
-
tooSlow: (seconds, max) =>
|
|
214
|
+
tooSlow: (seconds, max) => `连续两次快照超过 ${max} 秒(最近一次 ${seconds} 秒)`,
|
|
215
215
|
gitFailed: (command, code, detail) => `git ${command} 失败(${code})${detail === "" ? "" : `:${detail}`}`,
|
|
216
216
|
noShadowCommit: "检查点没有影子提交",
|
|
217
217
|
shadowCommitMissing: (commit) => `影子提交 ${commit} 不存在`,
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
import { realpathSync } from "node:fs";
|
|
20
20
|
import { resolve } from "node:path";
|
|
21
21
|
import { stopReasonOf } from "../../ai/apis/shared.js";
|
|
22
|
-
import { createSessionAlongside, currentSession,
|
|
22
|
+
import { createSessionAlongside, currentSession, setForegroundSession, } from "../../cli/compose-session.js";
|
|
23
23
|
import { listSessions } from "../../cli/compose-store.js";
|
|
24
24
|
import { AgentSessionImpl } from "../../agent/session.js";
|
|
25
|
+
import { taskControl } from "../../agents/task-control.js";
|
|
25
26
|
import { RpcError } from "../../drivers/jsonrpc.js";
|
|
26
27
|
import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "../../drivers/acp/types.js";
|
|
27
28
|
import { isPermissionMode } from "../../permissions/modes.js";
|
|
@@ -31,7 +32,7 @@ import { AcpEventMapper, permissionModes, toolKind, toolLocations, toolTitle, }
|
|
|
31
32
|
import { msg } from "../../i18n/index.js";
|
|
32
33
|
import { CONFIG_IDS, applyConfigOption, availableCommands, buildConfigOptions, prepareConfigOptions, } from "./acp-config.js";
|
|
33
34
|
import { clientCapabilitiesOf } from "./acp-connection.js";
|
|
34
|
-
import { PromptQueue, cancelledResult, pageSessions, promptWhenIdle, sessionTitle, } from "./acp-sessions.js";
|
|
35
|
+
import { PromptQueue, cancelledResult, disposeQuiet, pageSessions, promptWhenIdle, quiesce, sessionTitle, } from "./acp-sessions.js";
|
|
35
36
|
export const ACP_AGENT_CAPABILITIES = {
|
|
36
37
|
loadSession: true,
|
|
37
38
|
promptCapabilities: { image: true, audio: false, embeddedContext: true },
|
|
@@ -157,8 +158,9 @@ export class AcpServer {
|
|
|
157
158
|
.abort()
|
|
158
159
|
.catch(() => undefined);
|
|
159
160
|
await this.queue.settled();
|
|
160
|
-
for (const
|
|
161
|
-
|
|
161
|
+
for (const s of this.sessions())
|
|
162
|
+
if (s instanceof AgentSessionImpl)
|
|
163
|
+
await quiesce(s, taskControl(s.state.sessionId));
|
|
162
164
|
const foreground = this.session();
|
|
163
165
|
for (const entry of this.pool.values())
|
|
164
166
|
entry.unsubscribe();
|
|
@@ -167,7 +169,7 @@ export class AcpServer {
|
|
|
167
169
|
continue;
|
|
168
170
|
if (!(foreground instanceof AgentSessionImpl))
|
|
169
171
|
continue;
|
|
170
|
-
await
|
|
172
|
+
await disposeQuiet(this.runtime, session, foreground).catch(() => undefined);
|
|
171
173
|
}
|
|
172
174
|
this.pool.clear();
|
|
173
175
|
this.standby = undefined;
|
|
@@ -311,12 +313,11 @@ export class AcpServer {
|
|
|
311
313
|
return {};
|
|
312
314
|
this.pool.delete(id);
|
|
313
315
|
this.queue.cancelQueued(id);
|
|
314
|
-
|
|
315
|
-
if (running?.sessionId === id)
|
|
316
|
-
running.cancelRequested = true;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
316
|
+
// [#167] 通知回合不经队列:不论谁在跑都先安静下来(停后台任务、abort 到空闲)
|
|
317
|
+
if (this.queue.running?.sessionId === id)
|
|
318
|
+
this.queue.running.cancelRequested = true;
|
|
319
|
+
await quiesce(entry.session, taskControl(id));
|
|
320
|
+
await this.queue.settled(id);
|
|
320
321
|
entry.unsubscribe();
|
|
321
322
|
let fallback = this.session();
|
|
322
323
|
if (fallback === entry.session) {
|
|
@@ -330,7 +331,7 @@ export class AcpServer {
|
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
333
|
if (fallback instanceof AgentSessionImpl)
|
|
333
|
-
await
|
|
334
|
+
await disposeQuiet(this.runtime, entry.session, fallback, taskControl(id));
|
|
334
335
|
return {};
|
|
335
336
|
}
|
|
336
337
|
async prompt(params, signal) {
|
|
@@ -377,16 +378,17 @@ export class AcpServer {
|
|
|
377
378
|
setForegroundSession(this.runtime, session);
|
|
378
379
|
if (this.runtime.permission.mode !== entry.mode)
|
|
379
380
|
session.setPermissionMode(entry.mode);
|
|
380
|
-
|
|
381
|
+
let before = session.getStats().tokens;
|
|
381
382
|
let failure;
|
|
382
383
|
try {
|
|
383
|
-
|
|
384
|
+
// [#166] 起点取在真正发起时:等待期间结束的通知回合不计入本次 usage
|
|
385
|
+
await promptWhenIdle(session, job, () => void (before = session.getStats().tokens));
|
|
384
386
|
}
|
|
385
387
|
catch (error) {
|
|
386
388
|
failure = error;
|
|
387
389
|
}
|
|
388
|
-
await this.peer.flush();
|
|
389
390
|
const after = session.getStats().tokens;
|
|
391
|
+
await this.peer.flush();
|
|
390
392
|
const usage = {
|
|
391
393
|
inputTokens: after.input - before.input,
|
|
392
394
|
outputTokens: after.output - before.output,
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ImageBlock } from "../../ai/types.js";
|
|
10
10
|
import type { AgentSessionImpl } from "../../agent/session.js";
|
|
11
|
+
import type { TaskControl } from "../../agents/task-control.js";
|
|
12
|
+
import type { Runtime } from "../../cli/runtime.js";
|
|
11
13
|
import type { AgentMessage, SessionListItem } from "../../session/types.js";
|
|
12
14
|
import { type AcpPromptResult, type AcpListSessionsResult } from "../../drivers/acp/types.js";
|
|
13
15
|
import type { PermissionMode } from "../../permissions/types.js";
|
|
@@ -86,4 +88,15 @@ export declare function pageSessions(items: readonly SessionListItem[], cursor:
|
|
|
86
88
|
* 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
|
|
87
89
|
* 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
|
|
88
90
|
*/
|
|
89
|
-
export declare function promptWhenIdle(session: AgentSessionImpl, job: PromptJob): Promise<void>;
|
|
91
|
+
export declare function promptWhenIdle(session: AgentSessionImpl, job: PromptJob, onStart?: () => void): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* 关会话前把它安静下来(Issue #167):先停在跑的后台任务(被停止的任务不发完成通知),再反复 abort,
|
|
94
|
+
* 直到隔一个宏任务仍然空闲——已完成任务排在投递链上的通知会在上一周期结算后的微任务里开新周期,
|
|
95
|
+
* 一次 abort 不够。超过 `rounds` 轮仍不空闲返回 false。被打断的通知回合与用户 cancel 同样留下中断标记。
|
|
96
|
+
*/
|
|
97
|
+
export declare function quiesce(session: AgentSessionImpl, control: TaskControl | undefined, rounds?: number): Promise<boolean>;
|
|
98
|
+
/**
|
|
99
|
+
* `disposeSessionAlongside` 的安静版:撞上 busy(SessionEnd Hook 期间又开了通知回合)→ 再 quiesce 重试;
|
|
100
|
+
* dispose 之后若还有周期(与 dispose 同一时刻开出来的)再 abort 一次。
|
|
101
|
+
*/
|
|
102
|
+
export declare function disposeQuiet(runtime: Runtime, session: AgentSessionImpl, fallback: AgentSessionImpl, control?: TaskControl, retries?: number): Promise<void>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - 同一时刻只跑一个回合,其它会话的 `session/prompt` 进 {@link PromptQueue}(FIFO),不再回 busy;
|
|
7
7
|
* - `session/list`:按 `updatedAt` 降序、`sessionId` 次序分页,cursor 是 `updatedAt|sessionId` 的 base64。
|
|
8
8
|
*/
|
|
9
|
+
import { disposeSessionAlongside } from "../../cli/compose-session.js";
|
|
9
10
|
import { RPC_ERRORS, } from "../../drivers/acp/types.js";
|
|
10
11
|
import { RpcError } from "../../drivers/jsonrpc.js";
|
|
11
12
|
import { AmaError } from "../../errors.js";
|
|
@@ -164,12 +165,14 @@ export function pageSessions(items, cursor, pageSize = LIST_PAGE_SIZE) {
|
|
|
164
165
|
* 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
|
|
165
166
|
* 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
|
|
166
167
|
*/
|
|
167
|
-
export async function promptWhenIdle(session, job) {
|
|
168
|
+
export async function promptWhenIdle(session, job, onStart) {
|
|
168
169
|
for (;;) {
|
|
169
170
|
// 等周期而不是看 `state.isStreaming`:busy 以周期为准,周期的收尾阶段 isStreaming 已是 false
|
|
170
171
|
await session.waitForIdle();
|
|
171
172
|
if (job.cancelRequested)
|
|
172
173
|
return;
|
|
174
|
+
// [#166] 每次真正发起前回调(busy 重试时再调,最后一次为准):usage 的起点不含之前的通知回合
|
|
175
|
+
onStart?.();
|
|
173
176
|
try {
|
|
174
177
|
await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
|
|
175
178
|
return;
|
|
@@ -180,3 +183,43 @@ export async function promptWhenIdle(session, job) {
|
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
}
|
|
186
|
+
const BUSY = Symbol("busy");
|
|
187
|
+
/** 当前没有周期(含收尾阶段):`waitForIdle()` 已结算即空闲。 */
|
|
188
|
+
async function idleNow(session) {
|
|
189
|
+
return (await Promise.race([session.waitForIdle(), Promise.resolve(BUSY)])) !== BUSY;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 关会话前把它安静下来(Issue #167):先停在跑的后台任务(被停止的任务不发完成通知),再反复 abort,
|
|
193
|
+
* 直到隔一个宏任务仍然空闲——已完成任务排在投递链上的通知会在上一周期结算后的微任务里开新周期,
|
|
194
|
+
* 一次 abort 不够。超过 `rounds` 轮仍不空闲返回 false。被打断的通知回合与用户 cancel 同样留下中断标记。
|
|
195
|
+
*/
|
|
196
|
+
export async function quiesce(session, control, rounds = 50) {
|
|
197
|
+
for (const task of control?.list() ?? [])
|
|
198
|
+
if (task.status === "running")
|
|
199
|
+
await control?.stop(task.taskId).catch(() => undefined);
|
|
200
|
+
for (let i = 0; i < rounds; i++) {
|
|
201
|
+
await session.abort();
|
|
202
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
203
|
+
if (await idleNow(session))
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* `disposeSessionAlongside` 的安静版:撞上 busy(SessionEnd Hook 期间又开了通知回合)→ 再 quiesce 重试;
|
|
210
|
+
* dispose 之后若还有周期(与 dispose 同一时刻开出来的)再 abort 一次。
|
|
211
|
+
*/
|
|
212
|
+
export async function disposeQuiet(runtime, session, fallback, control, retries = 3) {
|
|
213
|
+
for (let i = 0;; i++) {
|
|
214
|
+
try {
|
|
215
|
+
await disposeSessionAlongside(runtime, session, fallback);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (i >= retries || !(error instanceof AmaError && error.code === "busy"))
|
|
220
|
+
throw error;
|
|
221
|
+
await quiesce(session, control);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
await session.abort();
|
|
225
|
+
}
|
|
@@ -45,6 +45,8 @@ export function agentCommandHooks(get) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
const TASK_NOTICE = /^\[task (t\d+)\] ([\s\S]*)$/;
|
|
48
|
+
/** #183:会话图片读不回(`ImageOffload` 的告警)转通知区。 */
|
|
49
|
+
const SESSION_NOTICE = /^cannot read session entry (\S+) back from /;
|
|
48
50
|
export class AgentUi {
|
|
49
51
|
deps;
|
|
50
52
|
plan;
|
|
@@ -164,6 +166,11 @@ export class AgentUi {
|
|
|
164
166
|
this.deps.notice(level === "warn" ? "warn" : "info", `[${agent} · ${taskId}] ${match[2]}`);
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
169
|
+
const image = level === "warn" ? SESSION_NOTICE.exec(message) : null;
|
|
170
|
+
if (image !== null) {
|
|
171
|
+
this.deps.notice("warn", msg().interactive.events.imageReadBackFailed(image[1]));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
167
174
|
original?.call(holder, level, message);
|
|
168
175
|
};
|
|
169
176
|
holder.log = routed;
|