howtofixcli 0.7.24 → 0.7.26
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/dist/account-credential.js +136 -0
- package/dist/acp.js +234 -0
- package/dist/activity.js +124 -0
- package/dist/agent-defs.js +237 -0
- package/dist/agent-limiter.js +68 -0
- package/dist/agents.js +554 -0
- package/dist/anim.js +361 -0
- package/dist/args.js +250 -0
- package/dist/at-file.js +28 -0
- package/dist/brand-extension.js +87 -0
- package/dist/brand.js +76 -0
- package/dist/browser.js +58 -0
- package/dist/cache-report.js +139 -0
- package/dist/checkpoint.js +437 -0
- package/dist/claims.js +167 -0
- package/dist/cli-auth.js +142 -0
- package/dist/cli-checkin.js +121 -0
- package/dist/cli-credentials.js +254 -0
- package/dist/clipboard.js +149 -0
- package/dist/collapse.js +129 -0
- package/dist/commands.js +381 -0
- package/dist/compaction-guard.js +81 -0
- package/dist/connect-prompt.js +22 -0
- package/dist/context-extension.js +17 -0
- package/dist/dcp/commands.js +172 -0
- package/dist/dcp/compress-state.js +139 -0
- package/dist/dcp/compress.js +329 -0
- package/dist/dcp/dcp-extension.js +295 -0
- package/dist/dcp/dedup.js +93 -0
- package/dist/dcp/ids.js +189 -0
- package/dist/dcp/index.js +25 -0
- package/dist/dcp/middle-prune.js +76 -0
- package/dist/dcp/prompts.js +68 -0
- package/dist/dcp/protected-content.js +221 -0
- package/dist/dcp/protected.js +161 -0
- package/dist/dcp/prune.js +418 -0
- package/dist/dcp/state.js +176 -0
- package/dist/errors.js +63 -0
- package/dist/extensions.js +48 -0
- package/dist/fatal.js +35 -0
- package/dist/feed.js +498 -0
- package/dist/fs-atomic.js +54 -0
- package/dist/gateway-errors.js +107 -0
- package/dist/git.js +230 -0
- package/dist/goal-extension.js +88 -0
- package/dist/goal.js +222 -0
- package/dist/hooks-extension.js +176 -0
- package/dist/hooks.js +443 -0
- package/dist/howtofix-account.js +834 -0
- package/dist/hud-line.js +158 -0
- package/dist/hwid.js +130 -0
- package/dist/interrupt.js +60 -0
- package/dist/json-events.js +50 -0
- package/dist/level-style.js +112 -0
- package/dist/loop-guard.js +95 -0
- package/dist/lsp-tools.js +32 -0
- package/dist/lsp.js +429 -0
- package/dist/main.js +573 -98
- package/dist/mcp-config.js +85 -0
- package/dist/mcp-lifecycle.js +147 -0
- package/dist/model-catalog.js +217 -0
- package/dist/model-facts.js +35 -0
- package/dist/model-resolve.js +65 -0
- package/dist/notes.js +78 -0
- package/dist/oauth-login.js +568 -0
- package/dist/paths.js +61 -0
- package/dist/permissions.js +298 -0
- package/dist/plan-mode.js +363 -0
- package/dist/preset-command.js +227 -0
- package/dist/preset-extension.js +35 -0
- package/dist/preset-mcp.js +292 -0
- package/dist/presets/mcp/rea/preset.json +28 -0
- package/dist/presets/mcp/rea/prompt.md +5 -0
- package/dist/presets/mcp/rea/skills/mcp-rea/SKILL.md +6 -0
- package/dist/presets/mcp/remotion/preset.json +27 -0
- package/dist/presets/mcp/remotion/prompt.md +7 -0
- package/dist/presets/mcp/remotion/skills/mcp-remotion/SKILL.md +6 -0
- package/dist/presets.js +727 -0
- package/dist/prompt-files.js +103 -0
- package/dist/prompt-text.js +41 -0
- package/dist/proxy.js +137 -0
- package/dist/quality/ast.js +196 -0
- package/dist/quality/cognitive.js +83 -0
- package/dist/quality/cyclomatic.js +30 -0
- package/dist/quality/halstead.js +83 -0
- package/dist/quality/index.js +18 -0
- package/dist/quality/length.js +3 -0
- package/dist/quality/maintainability.js +15 -0
- package/dist/quality/nesting.js +57 -0
- package/dist/quality/report.js +803 -0
- package/dist/quality/smells.js +67 -0
- package/dist/quality-tool.js +63 -0
- package/dist/read-only-commands.js +597 -0
- package/dist/repo-map.js +292 -0
- package/dist/roblox-connect.js +125 -0
- package/dist/rpc.js +91 -0
- package/dist/rtk.js +193 -0
- package/dist/sandbox.js +105 -0
- package/dist/schema.js +214 -0
- package/dist/serve.js +50 -0
- package/dist/session-lifecycle.js +19 -0
- package/dist/session-retention.js +111 -0
- package/dist/session.js +460 -0
- package/dist/sessions.js +100 -0
- package/dist/settings.js +148 -0
- package/dist/skills.js +35 -0
- package/dist/slop-tool.js +96 -0
- package/dist/spend-cap.js +43 -0
- package/dist/subagent.js +171 -0
- package/dist/tar.js +146 -0
- package/dist/theme.js +79 -0
- package/dist/thinking.js +45 -0
- package/dist/todo.js +114 -0
- package/dist/tool-loadout.js +102 -0
- package/dist/tool-render.js +769 -0
- package/dist/tools.js +176 -0
- package/dist/ui.js +4931 -0
- package/dist/update.js +551 -0
- package/dist/usage-segment.js +74 -0
- package/dist/web-tools.js +46 -0
- package/dist/web.js +265 -0
- package/dist/welcome.js +179 -0
- package/dist/workflow-engine.js +246 -0
- package/dist/workflow-progress.js +446 -0
- package/dist/workflow-view.js +195 -0
- package/dist/workflow.js +349 -0
- package/package.json +62 -15
- package/dist/chunk-27TRKD65.js +0 -2
- package/dist/chunk-3FYCFLNK.js +0 -1
- package/dist/chunk-3LZR7OEU.js +0 -1
- package/dist/chunk-56SEAGAQ.js +0 -1
- package/dist/chunk-5AWTL34S.js +0 -1
- package/dist/chunk-75P2SCOT.js +0 -1
- package/dist/chunk-7CHDNTH6.js +0 -1
- package/dist/chunk-7PHRNXLN.js +0 -34
- package/dist/chunk-AGGIYXN7.js +0 -2
- package/dist/chunk-AUDMRGWF.js +0 -189
- package/dist/chunk-C5E3SFOQ.js +0 -2
- package/dist/chunk-CMTKCUL5.js +0 -1
- package/dist/chunk-DAPXGSGD.js +0 -12
- package/dist/chunk-DOD7MUGX.js +0 -1
- package/dist/chunk-FPXVDP32.js +0 -8
- package/dist/chunk-GXB7RZHH.js +0 -1
- package/dist/chunk-I7KVLBMF.js +0 -2
- package/dist/chunk-ILDDDLRC.js +0 -1
- package/dist/chunk-JFYVRWJI.js +0 -2
- package/dist/chunk-LAHVHRCS.js +0 -2
- package/dist/chunk-LSJC73DG.js +0 -1
- package/dist/chunk-MNDKS6UH.js +0 -6
- package/dist/chunk-N345KWWK.js +0 -1
- package/dist/chunk-N3J2J7EQ.js +0 -5
- package/dist/chunk-ONZLNFQ2.js +0 -3
- package/dist/chunk-ORJ2QZBG.js +0 -3
- package/dist/chunk-PCALBZOO.js +0 -2
- package/dist/chunk-QPHSHL7H.js +0 -2
- package/dist/chunk-TKROS3BE.js +0 -5
- package/dist/chunk-TSEKPGK4.js +0 -1
- package/dist/chunk-UP7ENLEL.js +0 -117
- package/dist/chunk-UTYFLLSM.js +0 -1
- package/dist/chunk-V3R4FYTJ.js +0 -1
- package/dist/chunk-V656XVXV.js +0 -8
- package/dist/chunk-WKUJ7CFO.js +0 -1
- package/dist/chunk-WOT6VMZA.js +0 -1
- package/dist/chunk-WUPU6LTD.js +0 -4
- package/dist/chunk-WX3V5P7U.js +0 -5
- package/dist/chunk-XW6LMQGX.js +0 -1
- package/dist/chunk-YW7NORH3.js +0 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getAgentDir, parseFrontmatter } from "@netrodev/hxf-coding-agent";
|
|
4
|
+
import { BUILTIN_AGENT_NAMES } from "./agents.js";
|
|
5
|
+
/**
|
|
6
|
+
* Subagent definitions written by the user, as markdown.
|
|
7
|
+
*
|
|
8
|
+
* One file is one agent. The frontmatter carries what the model needs to choose
|
|
9
|
+
* it and what the run needs to be set up, and the body becomes the child's
|
|
10
|
+
* standing system prompt:
|
|
11
|
+
*
|
|
12
|
+
* ---
|
|
13
|
+
* name: migration
|
|
14
|
+
* description: moves one module at a time and tests after each
|
|
15
|
+
* tools: read, bash, edit, write
|
|
16
|
+
* model: gemini-3-flash
|
|
17
|
+
* effort: medium
|
|
18
|
+
* ---
|
|
19
|
+
* Migrate exactly one module per turn. Run its tests before moving on, and
|
|
20
|
+
* report the file you changed and the test result.
|
|
21
|
+
*
|
|
22
|
+
* The parser is the harness's own, so a file means the same thing here and in
|
|
23
|
+
* the harness. The locations follow this CLI's rule: everything a user writes
|
|
24
|
+
* for a project lives under one directory, `.howtofix`, the same one that holds
|
|
25
|
+
* project skills and presets.
|
|
26
|
+
*
|
|
27
|
+
* <project>/.howtofix/agents/*.md nearest ancestor, found by walking up
|
|
28
|
+
* ~/.howtofix/agent/agents/*.md the CLI's own user scope
|
|
29
|
+
*
|
|
30
|
+
* `parseFrontmatter` is the SDK's YAML parser, which means quoted values,
|
|
31
|
+
* arrays, comments and anything else YAML allows are read the same way the
|
|
32
|
+
* harness reads them. The user-scope directory is the one the run store also
|
|
33
|
+
* uses (`AGENTS_STORE_DIR`); the two do not collide because the store reads only
|
|
34
|
+
* `*.json` and this reads only `*.md`.
|
|
35
|
+
*
|
|
36
|
+
* A definition replaces a built-in of the same name. That is the point of having
|
|
37
|
+
* them: `scout`, `architect`, `task` and `reviewer` are a starting set, not a
|
|
38
|
+
* fixed menu, and an agent whose prompt is wrong for a project should be fixable
|
|
39
|
+
* without patching this package.
|
|
40
|
+
*/
|
|
41
|
+
/** The effort levels a definition may declare. Ours, not the harness's. */
|
|
42
|
+
const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
43
|
+
/** Frontmatter keys this loader reads; anything else is reported and ignored. */
|
|
44
|
+
const KEYS = ["name", "description", "tools", "model", "effort"];
|
|
45
|
+
/**
|
|
46
|
+
* Tools a definition may name.
|
|
47
|
+
*
|
|
48
|
+
* Not a permission list — the permission extension still decides what a child
|
|
49
|
+
* may actually do — but a spelling list, so a typo is reported at load instead of
|
|
50
|
+
* silently resolving to nothing at call time. `mcp__<server>__<tool>` names are
|
|
51
|
+
* accepted as written because they cannot be known in advance.
|
|
52
|
+
*/
|
|
53
|
+
export const KNOWN_AGENT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "bash_output", "kill_shell", "todo", "context", "goal", "task", "workflow", "web_search", "web_fetch", "lsp", "quality", "slop", "claim", "repo_map"];
|
|
54
|
+
/**
|
|
55
|
+
* What a definition gets when it names no tools: the same set the built-in `task`
|
|
56
|
+
* agent has.
|
|
57
|
+
*
|
|
58
|
+
* The harness leaves an absent `tools` meaning "no restriction". Here the field
|
|
59
|
+
* is the child's allowlist, so absent has to mean something concrete, and a
|
|
60
|
+
* working set is the useful reading: the permission extension already refuses
|
|
61
|
+
* anything above the parent's approval mode inside a child, and a spawned agent
|
|
62
|
+
* has no interactive UI, so the gate fails closed. Narrowing is what `tools` is
|
|
63
|
+
* for.
|
|
64
|
+
*/
|
|
65
|
+
export const DEFAULT_AGENT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "context", "todo", "web_search", "web_fetch", "lsp", "claim"];
|
|
66
|
+
/** `a, b` and `[a, b]` both mean two tools; anything else means none declared. */
|
|
67
|
+
function asToolList(value) {
|
|
68
|
+
const raw = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
|
|
69
|
+
const tools = raw
|
|
70
|
+
.map((item) => String(item)
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/^["']|["']$/g, ""))
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
return tools.length > 0 ? tools : undefined;
|
|
75
|
+
}
|
|
76
|
+
function isDirectory(path) {
|
|
77
|
+
try {
|
|
78
|
+
return statSync(path).isDirectory();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The nearest project agent directory, walking up from `cwd`.
|
|
86
|
+
*
|
|
87
|
+
* Walking up is the harness's rule and the useful one: a definition written for a
|
|
88
|
+
* monorepo root applies to every package under it.
|
|
89
|
+
*/
|
|
90
|
+
export function findProjectAgentDir(cwd) {
|
|
91
|
+
let current = cwd;
|
|
92
|
+
for (;;) {
|
|
93
|
+
// Beside project skills and presets: one project directory, .howtofix, so a
|
|
94
|
+
// repository does not end up with two dot-directories holding the same kind
|
|
95
|
+
// of file.
|
|
96
|
+
const candidate = join(current, ".howtofix", "agents");
|
|
97
|
+
if (isDirectory(candidate))
|
|
98
|
+
return candidate;
|
|
99
|
+
const parent = dirname(current);
|
|
100
|
+
if (parent === current)
|
|
101
|
+
return undefined;
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The directories definitions are read from, in load order.
|
|
107
|
+
*
|
|
108
|
+
* User first, project last, and a later definition replaces an earlier one — so a
|
|
109
|
+
* project agent wins a name collision, the precedence the harness's own subagent
|
|
110
|
+
* discovery uses.
|
|
111
|
+
*/
|
|
112
|
+
export function subagentDirs(cwd) {
|
|
113
|
+
// One user location, the one this CLI owns. The harness default, ~/.hxf/agent,
|
|
114
|
+
// is not read: two folders holding the same kind of file is how a definition
|
|
115
|
+
// ends up in the place nothing looks at.
|
|
116
|
+
const dirs = [{ scope: "user", dir: join(getAgentDir(), "agents") }];
|
|
117
|
+
const project = findProjectAgentDir(cwd);
|
|
118
|
+
if (project)
|
|
119
|
+
dirs.push({ scope: "project", dir: project });
|
|
120
|
+
return dirs;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Parse one definition file. Pure apart from the SDK's YAML parser, so the rules
|
|
124
|
+
* can be tested without touching a filesystem.
|
|
125
|
+
*/
|
|
126
|
+
export function parseAgentDef(text, file) {
|
|
127
|
+
const notes = [];
|
|
128
|
+
const problems = [];
|
|
129
|
+
const source = text.replace(/^\uFEFF/, "");
|
|
130
|
+
if (!/^---\r?\n/.test(source)) {
|
|
131
|
+
return { notes, problems: [`${file}: no frontmatter — the file must start with --- and end the block with ---`] };
|
|
132
|
+
}
|
|
133
|
+
let frontmatter = {};
|
|
134
|
+
let body = "";
|
|
135
|
+
try {
|
|
136
|
+
const parsed = parseFrontmatter(source);
|
|
137
|
+
frontmatter = parsed.frontmatter ?? {};
|
|
138
|
+
body = (parsed.body ?? "").trim();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return { notes, problems: [`${file}: the frontmatter is not readable YAML — ${error instanceof Error ? error.message : String(error)}`] };
|
|
142
|
+
}
|
|
143
|
+
for (const key of Object.keys(frontmatter)) {
|
|
144
|
+
if (!KEYS.includes(key))
|
|
145
|
+
notes.push(`${file}: ignoring the unknown frontmatter key '${key}'`);
|
|
146
|
+
}
|
|
147
|
+
// A file's own name is the natural default, so `reviewer.md` needs no `name:`.
|
|
148
|
+
const stem = (file.split(/[\\/]/).pop() ?? file).replace(/\.md$/i, "");
|
|
149
|
+
const rawName = frontmatter.name;
|
|
150
|
+
const name = (typeof rawName === "string" && rawName.trim() ? rawName : stem).trim();
|
|
151
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
152
|
+
problems.push(`${file}: '${name}' is not a usable agent name — letters, digits, dot, dash and underscore only`);
|
|
153
|
+
}
|
|
154
|
+
const rawDescription = frontmatter.description;
|
|
155
|
+
const description = typeof rawDescription === "string" ? rawDescription.trim() : "";
|
|
156
|
+
if (!description)
|
|
157
|
+
problems.push(`${file}: a description is required — it is how the model decides to use this agent`);
|
|
158
|
+
if (!body)
|
|
159
|
+
problems.push(`${file}: no prompt body — the text below the frontmatter is the agent's standing instruction`);
|
|
160
|
+
const rawEffort = frontmatter.effort;
|
|
161
|
+
const effort = typeof rawEffort === "string" ? rawEffort.trim().toLowerCase() : "";
|
|
162
|
+
if (effort && !EFFORTS.includes(effort)) {
|
|
163
|
+
problems.push(`${file}: effort '${effort}' is not one of ${EFFORTS.join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
const declared = asToolList(frontmatter.tools);
|
|
166
|
+
const tools = declared ?? [...DEFAULT_AGENT_TOOLS];
|
|
167
|
+
if (declared) {
|
|
168
|
+
const unknown = declared.filter((tool) => !KNOWN_AGENT_TOOLS.includes(tool) && !tool.startsWith("mcp__"));
|
|
169
|
+
if (unknown.length > 0) {
|
|
170
|
+
notes.push(`${file}: unknown tool${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")} — kept, but nothing registers them yet`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const rawModel = frontmatter.model;
|
|
174
|
+
const model = typeof rawModel === "string" && rawModel.trim() ? rawModel.trim() : undefined;
|
|
175
|
+
if (problems.length > 0)
|
|
176
|
+
return { notes, problems };
|
|
177
|
+
return {
|
|
178
|
+
def: { name, description, tools, prompt: body, effort: (effort || "medium"), model, source: file },
|
|
179
|
+
notes,
|
|
180
|
+
problems,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Load every definition into `into`.
|
|
185
|
+
*
|
|
186
|
+
* Re-running cannot shuffle which definition a session runs with: the same files
|
|
187
|
+
* produce the same map, and a definition only ever replaces one that loaded
|
|
188
|
+
* before it.
|
|
189
|
+
*/
|
|
190
|
+
export function loadAgentDefs(cwd, into) {
|
|
191
|
+
const loaded = [];
|
|
192
|
+
const notes = [];
|
|
193
|
+
const problems = [];
|
|
194
|
+
const scopes = [];
|
|
195
|
+
for (const { scope, dir } of subagentDirs(cwd)) {
|
|
196
|
+
let entries;
|
|
197
|
+
try {
|
|
198
|
+
entries = readdirSync(dir);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// No directory is the normal case, not a problem to report.
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const entry of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
|
|
205
|
+
const file = join(dir, entry);
|
|
206
|
+
let text;
|
|
207
|
+
try {
|
|
208
|
+
text = readFileSync(file, "utf8");
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const parsed = parseAgentDef(text, file);
|
|
215
|
+
notes.push(...parsed.notes);
|
|
216
|
+
problems.push(...parsed.problems);
|
|
217
|
+
const def = parsed.def;
|
|
218
|
+
if (!def)
|
|
219
|
+
continue;
|
|
220
|
+
if (into.has(def.name)) {
|
|
221
|
+
notes.push(`${file}: replaces ${BUILTIN_AGENT_NAMES.has(def.name) ? "the built-in" : "an earlier"} '${def.name}'`);
|
|
222
|
+
}
|
|
223
|
+
into.set(def.name, def);
|
|
224
|
+
loaded.push(def);
|
|
225
|
+
if (!scopes.includes(scope))
|
|
226
|
+
scopes.push(scope);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (loaded.length > 0) {
|
|
230
|
+
// The effective set, not every file read: a definition that a later one
|
|
231
|
+
// replaced is not what a session will run with, and naming it twice would
|
|
232
|
+
// read as two agents.
|
|
233
|
+
const effective = loaded.filter((def) => into.get(def.name) === def);
|
|
234
|
+
notes.unshift(`loaded ${effective.length} subagent definition${effective.length === 1 ? "" : "s"} (${scopes.join(" and ")}): ${effective.map((def) => def.name).join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
return { loaded, notes, problems };
|
|
237
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One budget for agent sessions, shared by every path that opens one.
|
|
3
|
+
*
|
|
4
|
+
* Two paths spawn children: the `task` tool (`drain`, up to `MAX_PARALLEL` at a
|
|
5
|
+
* time) and the `workflow` tool (its own semaphore, `defaultConcurrency()`). Each
|
|
6
|
+
* had its own limiter and neither knew about the other, so on a six-core box a
|
|
7
|
+
* workflow could run four agents while a task call ran three — seven sessions,
|
|
8
|
+
* seven provider streams, on a machine sized for far fewer. Nothing failed; the
|
|
9
|
+
* box simply went quiet, which is the worst way for a limit to be missing.
|
|
10
|
+
*
|
|
11
|
+
* The width is the machine's, not a constant: `os.availableParallelism()`
|
|
12
|
+
* respects the cgroup, so a container sized to two cores does not fan out as if
|
|
13
|
+
* it owned the host. The `agents` setting overrides it for a user who knows their
|
|
14
|
+
* box, and `agents=1` serialises every child — the escape hatch for a host that
|
|
15
|
+
* cannot take any concurrency at all.
|
|
16
|
+
*/
|
|
17
|
+
import { availableParallelism } from "node:os";
|
|
18
|
+
import { AGENT_DIR } from "./paths.js";
|
|
19
|
+
import { readWorkbenchSettings } from "./settings.js";
|
|
20
|
+
/** The ceiling, whatever the machine reports. */
|
|
21
|
+
export const MAX_AGENT_CONCURRENCY = 4;
|
|
22
|
+
/**
|
|
23
|
+
* How many agent sessions may be in flight at once, across `task` and `workflow`.
|
|
24
|
+
*
|
|
25
|
+
* The default leaves the host usable: two cores free for the CLI, the model's
|
|
26
|
+
* own request handling, and whatever else the user is running.
|
|
27
|
+
*/
|
|
28
|
+
export function agentConcurrency() {
|
|
29
|
+
const configured = readWorkbenchSettings(AGENT_DIR).agents;
|
|
30
|
+
if (configured !== undefined)
|
|
31
|
+
return configured;
|
|
32
|
+
return Math.max(1, Math.min(MAX_AGENT_CONCURRENCY, availableParallelism() - 2));
|
|
33
|
+
}
|
|
34
|
+
let active = 0;
|
|
35
|
+
const waiting = [];
|
|
36
|
+
/**
|
|
37
|
+
* Take one slot, returning the function that releases it.
|
|
38
|
+
*
|
|
39
|
+
* FIFO, so a `workflow` that asks for four and a `task` that asks for three are
|
|
40
|
+
* served in the order they arrived rather than by whichever retries first — an
|
|
41
|
+
* unfair queue starves one path under load, which reads as a hang in that path.
|
|
42
|
+
*
|
|
43
|
+
* The width is read when the slot is taken, not when the module loads, so
|
|
44
|
+
* changing the setting affects the next child rather than the next process.
|
|
45
|
+
*/
|
|
46
|
+
export async function acquireAgentSlot() {
|
|
47
|
+
while (active >= agentConcurrency()) {
|
|
48
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
49
|
+
}
|
|
50
|
+
active += 1;
|
|
51
|
+
let released = false;
|
|
52
|
+
return () => {
|
|
53
|
+
if (released)
|
|
54
|
+
return;
|
|
55
|
+
released = true;
|
|
56
|
+
active -= 1;
|
|
57
|
+
waiting.shift()?.();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Sessions in flight right now, for a check and for `/agents`. */
|
|
61
|
+
export function activeAgentCount() {
|
|
62
|
+
return active;
|
|
63
|
+
}
|
|
64
|
+
/** Test seam: drop the in-flight count and release everyone waiting. */
|
|
65
|
+
export function resetAgentLimiterForTest() {
|
|
66
|
+
active = 0;
|
|
67
|
+
waiting.splice(0).forEach((resolve) => resolve());
|
|
68
|
+
}
|