@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/openapi.yaml +6 -7
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
- package/src/__tests__/conversation-agent-loop.test.ts +48 -96
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +52 -49
- package/src/__tests__/memory-retrieval-hook.test.ts +68 -30
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
- package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
- package/src/acp/auto-install.test.ts +130 -46
- package/src/acp/auto-install.ts +86 -31
- package/src/acp/prepare-agent-env.ts +8 -9
- package/src/acp/resolve-agent.test.ts +26 -123
- package/src/acp/resolve-agent.ts +26 -85
- package/src/acp/resume-hint.ts +1 -3
- package/src/acp/session-manager.ts +32 -20
- package/src/acp/types.ts +0 -8
- package/src/agent/loop.ts +42 -26
- package/src/config/bundled-skills/acp/SKILL.md +14 -14
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/daemon/conversation-agent-loop.ts +15 -57
- package/src/daemon/conversation.ts +23 -3
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +0 -7
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +13 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +24 -25
- package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
- package/src/runtime/routes/plugins-routes.ts +10 -9
- package/src/tools/acp/list-agents.test.ts +2 -2
- package/src/tools/acp/spawn.test.ts +103 -225
- package/src/tools/acp/spawn.ts +10 -120
- package/src/tools/apps/executors.ts +153 -42
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
|
@@ -13,8 +13,7 @@ afterAll(() => {
|
|
|
13
13
|
setOverridesForTesting({});
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
const { resolveAcpAgent, listAcpAgents
|
|
17
|
-
await import("./resolve-agent.js");
|
|
16
|
+
const { resolveAcpAgent, listAcpAgents } = await import("./resolve-agent.js");
|
|
18
17
|
|
|
19
18
|
beforeEach(() => {
|
|
20
19
|
config.setConfig({});
|
|
@@ -217,7 +216,7 @@ describe("resolveAcpAgent", () => {
|
|
|
217
216
|
if (result.ok) return;
|
|
218
217
|
expect(result.reason).toBe("binary_not_found");
|
|
219
218
|
if (result.reason !== "binary_not_found") return;
|
|
220
|
-
expect(result.hint).toBe("
|
|
219
|
+
expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
|
|
221
220
|
expect(result.command).toBe("claude-agent-acp");
|
|
222
221
|
});
|
|
223
222
|
|
|
@@ -257,7 +256,7 @@ describe("resolveAcpAgent", () => {
|
|
|
257
256
|
if (result.ok) return;
|
|
258
257
|
expect(result.reason).toBe("binary_not_found");
|
|
259
258
|
if (result.reason !== "binary_not_found") return;
|
|
260
|
-
expect(result.hint).toBe("
|
|
259
|
+
expect(result.hint).toBe("bun add -g @zed-industries/codex-acp");
|
|
261
260
|
});
|
|
262
261
|
|
|
263
262
|
test("binary preflight honors agent.env.PATH override (matches spawn env)", () => {
|
|
@@ -300,7 +299,7 @@ describe("resolveAcpAgent", () => {
|
|
|
300
299
|
expect(result.agent.args).toEqual(["--verbose"]);
|
|
301
300
|
});
|
|
302
301
|
|
|
303
|
-
test("
|
|
302
|
+
test("resolves a full-path command directly without rewriting it", () => {
|
|
304
303
|
config.setConfig({
|
|
305
304
|
agents: {
|
|
306
305
|
custom: { command: "/opt/bin/claude-agent-acp", args: [] },
|
|
@@ -310,117 +309,38 @@ describe("resolveAcpAgent", () => {
|
|
|
310
309
|
const direct = resolveAcpAgent("claude");
|
|
311
310
|
expect(direct.ok).toBe(true);
|
|
312
311
|
if (!direct.ok) return;
|
|
313
|
-
expect(direct.agent.
|
|
314
|
-
expect(runsViaBunx(direct.agent)).toBe(false);
|
|
312
|
+
expect(direct.agent.command).toBe("claude-agent-acp");
|
|
315
313
|
|
|
316
314
|
const fullPath = resolveAcpAgent("custom");
|
|
317
315
|
expect(fullPath.ok).toBe(true);
|
|
318
316
|
if (!fullPath.ok) return;
|
|
319
|
-
expect(fullPath.agent.
|
|
317
|
+
expect(fullPath.agent.command).toBe("/opt/bin/claude-agent-acp");
|
|
320
318
|
});
|
|
321
319
|
});
|
|
322
320
|
|
|
323
321
|
// ---------------------------------------------------------------------------
|
|
324
|
-
// resolveAcpAgent -
|
|
322
|
+
// resolveAcpAgent - missing binaries are never run from the task cwd
|
|
325
323
|
// ---------------------------------------------------------------------------
|
|
326
324
|
|
|
327
|
-
describe("resolveAcpAgent -
|
|
328
|
-
test("binary missing + bun present:
|
|
325
|
+
describe("resolveAcpAgent - missing binary", () => {
|
|
326
|
+
test("binary missing + bun present: still binary_not_found (no bunx rewrite)", () => {
|
|
327
|
+
// bun on PATH no longer makes a missing adapter "runnable" at resolve
|
|
328
|
+
// time: the sandboxed install happens separately, never as a `bun x`
|
|
329
|
+
// rewrite in the untrusted cwd.
|
|
329
330
|
config.setConfig({ agents: {} });
|
|
330
331
|
which.setWhich({ bun: "/usr/local/bin/bun" });
|
|
331
332
|
|
|
332
333
|
const result = resolveAcpAgent("claude");
|
|
333
334
|
|
|
334
|
-
expect(result.ok).toBe(true);
|
|
335
|
-
if (!result.ok) return;
|
|
336
|
-
expect(result.agent.command).toBe("bun");
|
|
337
|
-
expect(result.agent.args).toEqual([
|
|
338
|
-
"x",
|
|
339
|
-
"--bun",
|
|
340
|
-
"@agentclientprotocol/claude-agent-acp",
|
|
341
|
-
]);
|
|
342
|
-
expect(result.agent.adapterCommand).toBe("claude-agent-acp");
|
|
343
|
-
expect(runsViaBunx(result.agent)).toBe(true);
|
|
344
|
-
expect(adapterCommandOf(result.agent)).toBe("claude-agent-acp");
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("bunx rewrite appends the original args after the package (gemini --acp)", () => {
|
|
348
|
-
config.setConfig({ agents: {} });
|
|
349
|
-
which.setWhich({ bun: "/usr/local/bin/bun" });
|
|
350
|
-
|
|
351
|
-
const result = resolveAcpAgent("gemini");
|
|
352
|
-
|
|
353
|
-
expect(result.ok).toBe(true);
|
|
354
|
-
if (!result.ok) return;
|
|
355
|
-
expect(result.agent.command).toBe("bun");
|
|
356
|
-
expect(result.agent.args).toEqual([
|
|
357
|
-
"x",
|
|
358
|
-
"--bun",
|
|
359
|
-
"@google/gemini-cli",
|
|
360
|
-
"--acp",
|
|
361
|
-
]);
|
|
362
|
-
expect(result.agent.adapterCommand).toBe("gemini");
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test("bunx rewrite leaves env unchanged", () => {
|
|
366
|
-
config.setConfig({
|
|
367
|
-
agents: {
|
|
368
|
-
claude: {
|
|
369
|
-
command: "claude-agent-acp",
|
|
370
|
-
args: [],
|
|
371
|
-
env: { KEEP_ME: "yes" },
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
which.setWhich({ bun: "/usr/local/bin/bun" });
|
|
376
|
-
|
|
377
|
-
const result = resolveAcpAgent("claude");
|
|
378
|
-
|
|
379
|
-
expect(result.ok).toBe(true);
|
|
380
|
-
if (!result.ok) return;
|
|
381
|
-
expect(result.agent.command).toBe("bun");
|
|
382
|
-
expect(result.agent.env).toEqual({ KEEP_ME: "yes" });
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
test("bun lookup honors agent.env.PATH override (matches spawn env)", () => {
|
|
386
|
-
config.setConfig({
|
|
387
|
-
agents: {
|
|
388
|
-
claude: {
|
|
389
|
-
command: "claude-agent-acp",
|
|
390
|
-
args: [],
|
|
391
|
-
env: { PATH: "/opt/custom/bin" },
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
which.setWhich((cmd, options) =>
|
|
396
|
-
cmd === "bun" && options?.PATH === "/opt/custom/bin"
|
|
397
|
-
? "/opt/custom/bin/bun"
|
|
398
|
-
: null,
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
const result = resolveAcpAgent("claude");
|
|
402
|
-
|
|
403
|
-
expect(result.ok).toBe(true);
|
|
404
|
-
if (!result.ok) return;
|
|
405
|
-
expect(result.agent.command).toBe("bun");
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test("user-config command without a package mapping is NOT rewritten even when bun is present", () => {
|
|
409
|
-
config.setConfig({
|
|
410
|
-
agents: {
|
|
411
|
-
custom: { command: "unknown-binary", args: [] },
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
which.setWhich({ bun: "/usr/local/bin/bun" });
|
|
415
|
-
|
|
416
|
-
const result = resolveAcpAgent("custom");
|
|
417
|
-
|
|
418
335
|
expect(result.ok).toBe(false);
|
|
419
336
|
if (result.ok) return;
|
|
420
337
|
expect(result.reason).toBe("binary_not_found");
|
|
338
|
+
if (result.reason !== "binary_not_found") return;
|
|
339
|
+
expect(result.command).toBe("claude-agent-acp");
|
|
340
|
+
expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
|
|
421
341
|
});
|
|
422
342
|
|
|
423
|
-
test("binary missing + bun missing: binary_not_found with the
|
|
343
|
+
test("binary missing + bun missing: binary_not_found with the bun hint", () => {
|
|
424
344
|
config.setConfig({ agents: {} });
|
|
425
345
|
which.setWhich({});
|
|
426
346
|
|
|
@@ -430,26 +350,7 @@ describe("resolveAcpAgent - bunx fallback", () => {
|
|
|
430
350
|
if (result.ok) return;
|
|
431
351
|
expect(result.reason).toBe("binary_not_found");
|
|
432
352
|
if (result.reason !== "binary_not_found") return;
|
|
433
|
-
expect(result.hint).toBe("
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// ---------------------------------------------------------------------------
|
|
438
|
-
// adapterCommandOf - fallback for plain configs
|
|
439
|
-
// ---------------------------------------------------------------------------
|
|
440
|
-
|
|
441
|
-
describe("adapterCommandOf", () => {
|
|
442
|
-
test("falls back to the command basename for configs without adapterCommand", () => {
|
|
443
|
-
expect(adapterCommandOf({ command: "/opt/bin/claude-agent-acp" })).toBe(
|
|
444
|
-
"claude-agent-acp",
|
|
445
|
-
);
|
|
446
|
-
expect(adapterCommandOf({ command: "codex-acp" })).toBe("codex-acp");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test("prefers an explicit adapterCommand over the command", () => {
|
|
450
|
-
expect(
|
|
451
|
-
adapterCommandOf({ command: "bun", adapterCommand: "claude-agent-acp" }),
|
|
452
|
-
).toBe("claude-agent-acp");
|
|
353
|
+
expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
|
|
453
354
|
});
|
|
454
355
|
});
|
|
455
356
|
|
|
@@ -523,18 +424,20 @@ describe("listAcpAgents", () => {
|
|
|
523
424
|
expect(codex?.source).toBe("default");
|
|
524
425
|
});
|
|
525
426
|
|
|
526
|
-
test("missing binaries
|
|
427
|
+
test("missing binaries are listed unavailable even when bun is present", () => {
|
|
428
|
+
// bun presence no longer makes a missing adapter available: it is
|
|
429
|
+
// installed on demand at spawn time, not run via `bun x` from the cwd.
|
|
527
430
|
config.setConfig({ agents: {} });
|
|
528
431
|
which.setWhich({ bun: "/usr/local/bin/bun" });
|
|
529
432
|
|
|
530
433
|
const result = listAcpAgents();
|
|
531
434
|
|
|
532
435
|
for (const entry of result.agents) {
|
|
533
|
-
expect(entry.available).toBe(
|
|
534
|
-
expect(entry.unavailableReason).
|
|
535
|
-
expect(entry.setupHint).
|
|
436
|
+
expect(entry.available).toBe(false);
|
|
437
|
+
expect(entry.unavailableReason).toBeDefined();
|
|
438
|
+
expect(entry.setupHint).toContain("bun add -g");
|
|
536
439
|
}
|
|
537
|
-
// The catalog keeps the canonical adapter commands
|
|
440
|
+
// The catalog keeps the canonical adapter commands.
|
|
538
441
|
expect(result.agents.map((a) => a.command)).toEqual([
|
|
539
442
|
"claude-agent-acp",
|
|
540
443
|
"codex-acp",
|
|
@@ -551,7 +454,7 @@ describe("listAcpAgents", () => {
|
|
|
551
454
|
const codex = result.agents.find((a) => a.id === "codex");
|
|
552
455
|
expect(codex?.available).toBe(false);
|
|
553
456
|
expect(codex?.unavailableReason).toBe("'codex-acp' is not on PATH");
|
|
554
|
-
expect(codex?.setupHint).toBe("
|
|
457
|
+
expect(codex?.setupHint).toBe("bun add -g @zed-industries/codex-acp");
|
|
555
458
|
});
|
|
556
459
|
|
|
557
460
|
test("unavailable gemini surfaces the @google/gemini-cli install hint", () => {
|
|
@@ -566,7 +469,7 @@ describe("listAcpAgents", () => {
|
|
|
566
469
|
const gemini = result.agents.find((a) => a.id === "gemini");
|
|
567
470
|
expect(gemini?.available).toBe(false);
|
|
568
471
|
expect(gemini?.unavailableReason).toBe("'gemini' is not on PATH");
|
|
569
|
-
expect(gemini?.setupHint).toBe("
|
|
472
|
+
expect(gemini?.setupHint).toBe("bun add -g @google/gemini-cli");
|
|
570
473
|
});
|
|
571
474
|
|
|
572
475
|
test("aliases are resolution sugar, not catalog entries", () => {
|
package/src/acp/resolve-agent.ts
CHANGED
|
@@ -13,22 +13,20 @@
|
|
|
13
13
|
* acp_list_agents, and the `/v1/acp/spawn` HTTP route) get a single source
|
|
14
14
|
* of truth and matching actionable hints.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* so downstream
|
|
23
|
-
*
|
|
16
|
+
* The resolver NEVER fetches or runs packages in the (untrusted) task cwd.
|
|
17
|
+
* When the adapter binary is missing, resolution simply fails with
|
|
18
|
+
* `binary_not_found`; the `resolveAgentWithAutoInstall` flow in
|
|
19
|
+
* `auto-install.ts` then performs a one-time sandboxed `bun` global install
|
|
20
|
+
* (clean temp cwd, secrets stripped from the installer env) and re-resolves.
|
|
21
|
+
* The resolved command is therefore ALWAYS the real adapter binary on PATH,
|
|
22
|
+
* so downstream gates (env injection, resume hints) key off the command
|
|
23
|
+
* basename directly.
|
|
24
24
|
*
|
|
25
25
|
* `listAcpAgents()` exposes the merged catalog with availability info for
|
|
26
26
|
* the `acp_list_agents` tool — same merge semantics, plus per-entry
|
|
27
|
-
* `available` / `setupHint` derived from the same binary
|
|
27
|
+
* `available` / `setupHint` derived from the same binary resolution.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { basename } from "node:path";
|
|
31
|
-
|
|
32
30
|
import {
|
|
33
31
|
DEFAULT_ACP_AGENT_PROFILES,
|
|
34
32
|
DEFAULT_AGENT_NPM_PACKAGES,
|
|
@@ -44,48 +42,8 @@ import { isAcpEnabled } from "./feature-gate.js";
|
|
|
44
42
|
*/
|
|
45
43
|
type AcpAgentSource = "config" | "default";
|
|
46
44
|
|
|
47
|
-
/**
|
|
48
|
-
* A resolver-produced agent config, ready to spawn. `adapterCommand` carries
|
|
49
|
-
* the canonical adapter identity (e.g. "claude-agent-acp") even when the
|
|
50
|
-
* spawn command was rewritten to run via `bun x`, so consumers that gate
|
|
51
|
-
* behavior on the adapter (env injection in `prepare-agent-env.ts`, resume
|
|
52
|
-
* hints, the version probe in `tools/acp/spawn.ts`) stay correct for
|
|
53
|
-
* bunx-resolved agents.
|
|
54
|
-
*/
|
|
55
|
-
export interface ResolvedAcpAgent extends AcpAgentConfig {
|
|
56
|
-
adapterCommand: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Canonical adapter identity for a (possibly rewritten) agent config.
|
|
61
|
-
* Resolver-produced configs carry `adapterCommand` explicitly; plain configs
|
|
62
|
-
* that never went through the resolver fall back to the command basename so
|
|
63
|
-
* user-supplied agent configs keep working.
|
|
64
|
-
*/
|
|
65
|
-
export function adapterCommandOf(config: {
|
|
66
|
-
command: string;
|
|
67
|
-
adapterCommand?: string;
|
|
68
|
-
}): string {
|
|
69
|
-
return config.adapterCommand ?? basename(config.command);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Whether the config was rewritten by the resolver to run through `bun x`
|
|
74
|
-
* (the only path where the canonical adapter identity diverges from the
|
|
75
|
-
* actual spawn command).
|
|
76
|
-
*/
|
|
77
|
-
export function runsViaBunx(config: {
|
|
78
|
-
command: string;
|
|
79
|
-
adapterCommand?: string;
|
|
80
|
-
}): boolean {
|
|
81
|
-
return (
|
|
82
|
-
config.adapterCommand !== undefined &&
|
|
83
|
-
config.adapterCommand !== basename(config.command)
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
45
|
export type ResolveAcpAgentResult =
|
|
88
|
-
| { ok: true; agent:
|
|
46
|
+
| { ok: true; agent: AcpAgentConfig }
|
|
89
47
|
| ResolveAcpAgentFailure;
|
|
90
48
|
|
|
91
49
|
export type ResolveAcpAgentFailure =
|
|
@@ -146,7 +104,7 @@ export const ACP_DISABLED_HINT =
|
|
|
146
104
|
function installHintFor(command: string): string {
|
|
147
105
|
const pkg = DEFAULT_AGENT_NPM_PACKAGES[command];
|
|
148
106
|
return pkg
|
|
149
|
-
? `
|
|
107
|
+
? `bun add -g ${pkg}`
|
|
150
108
|
: `Install '${command}' and ensure it is on PATH.`;
|
|
151
109
|
}
|
|
152
110
|
|
|
@@ -154,8 +112,7 @@ function installHintFor(command: string): string {
|
|
|
154
112
|
* Resolve a binary using the same PATH the spawn will see. `AcpAgentProcess`
|
|
155
113
|
* spawns with `{ ...process.env, ...config.env }`, so a per-agent `env.PATH`
|
|
156
114
|
* override wins over the assistant's PATH. Mirror that here so a config that
|
|
157
|
-
* relies on a custom PATH to locate the binary
|
|
158
|
-
* rewrite) doesn't fail preflight.
|
|
115
|
+
* relies on a custom PATH to locate the binary doesn't fail preflight.
|
|
159
116
|
*/
|
|
160
117
|
function whichOnAgentPath(
|
|
161
118
|
agent: AcpAgentConfig,
|
|
@@ -166,34 +123,17 @@ function whichOnAgentPath(
|
|
|
166
123
|
}
|
|
167
124
|
|
|
168
125
|
/**
|
|
169
|
-
* Resolve an agent config to its runnable form, or `null` when
|
|
170
|
-
*
|
|
126
|
+
* Resolve an agent config to its runnable form, or `null` when its `command`
|
|
127
|
+
* is not on PATH. Resolution is pure preflight: the binary is used directly
|
|
128
|
+
* when present, and otherwise the agent cannot spawn (callers fall back to
|
|
129
|
+
* the sandboxed `bun` global install in `auto-install.ts`, then re-resolve).
|
|
171
130
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* fetches the package on first use, preserving the
|
|
176
|
-
* install-latest-on-first-use design with no global install.
|
|
177
|
-
* 3. Otherwise → null (callers fall back to npm auto-install or surface the
|
|
178
|
-
* install hint).
|
|
179
|
-
*
|
|
180
|
-
* Security boundary (mirrors `auto-install.ts`): only commands present in
|
|
181
|
-
* `DEFAULT_AGENT_NPM_PACKAGES` are ever rewritten. The package names are
|
|
182
|
-
* vendored constants, NOT user input: an arbitrary command from user config
|
|
183
|
-
* must never be turned into a `bun x <attacker-controlled-name>` execution.
|
|
131
|
+
* Crucially, this never fetches or executes anything in the task cwd: a
|
|
132
|
+
* missing binary returns `null` rather than running a package manager from
|
|
133
|
+
* the (untrusted) project directory.
|
|
184
134
|
*/
|
|
185
|
-
function resolveRunnableAgent(agent: AcpAgentConfig):
|
|
186
|
-
|
|
187
|
-
return { ...agent, adapterCommand: basename(agent.command) };
|
|
188
|
-
}
|
|
189
|
-
const packageName = DEFAULT_AGENT_NPM_PACKAGES[agent.command];
|
|
190
|
-
if (!packageName || !whichOnAgentPath(agent, "bun")) return null;
|
|
191
|
-
return {
|
|
192
|
-
...agent,
|
|
193
|
-
command: "bun",
|
|
194
|
-
args: ["x", "--bun", packageName, ...agent.args],
|
|
195
|
-
adapterCommand: agent.command,
|
|
196
|
-
};
|
|
135
|
+
function resolveRunnableAgent(agent: AcpAgentConfig): AcpAgentConfig | null {
|
|
136
|
+
return whichOnAgentPath(agent, agent.command) ? agent : null;
|
|
197
137
|
}
|
|
198
138
|
|
|
199
139
|
/**
|
|
@@ -271,8 +211,8 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
|
|
|
271
211
|
* Order of checks:
|
|
272
212
|
* 1. ACP must be enabled (feature flag or config; see `isAcpEnabled`).
|
|
273
213
|
* 2. The id must resolve to an agent (user config wins; falls back to defaults).
|
|
274
|
-
* 3. The agent must be runnable: its `command` on PATH
|
|
275
|
-
*
|
|
214
|
+
* 3. The agent must be runnable: its `command` on PATH (see
|
|
215
|
+
* `resolveRunnableAgent`).
|
|
276
216
|
*
|
|
277
217
|
* Each failure mode carries an actionable hint so callers can surface a
|
|
278
218
|
* single user-facing message without re-deriving the remediation.
|
|
@@ -330,8 +270,9 @@ export function listAcpAgents(): {
|
|
|
330
270
|
const agents: AcpAgentEntry[] = mergedAgentIds(userAgents).map((id) => {
|
|
331
271
|
// Non-null: ids come from `mergedAgentIds` so the lookup always resolves.
|
|
332
272
|
const { agent, source } = lookupAgent(userAgents, id)!;
|
|
333
|
-
// Same binary
|
|
334
|
-
//
|
|
273
|
+
// Same binary preflight as `resolveAcpAgent`: available iff the command
|
|
274
|
+
// is on PATH. A missing binary is auto-installed at spawn time, but the
|
|
275
|
+
// catalog reflects what is runnable right now.
|
|
335
276
|
const available = resolveRunnableAgent(agent) !== null;
|
|
336
277
|
const entry: AcpAgentEntry = {
|
|
337
278
|
id,
|
package/src/acp/resume-hint.ts
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* adapter binary). Other adapters resume differently or not at all, so the
|
|
6
6
|
* hint is gated by the resolved adapter, not the agent id - this stays
|
|
7
7
|
* correct when a user aliases an id to a different binary. Callers pass the
|
|
8
|
-
*
|
|
9
|
-
* the raw spawn command, so the hint still fires when the claude adapter is
|
|
10
|
-
* run via `bun x` (spawn command "bun").
|
|
8
|
+
* resolved command basename, which is always the real adapter binary.
|
|
11
9
|
*
|
|
12
10
|
* Used by the `acp_spawn` tool's success payload and the session manager's
|
|
13
11
|
* completion notification so both surfaces render identical copy. Returns
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { basename } from "node:path";
|
|
7
8
|
|
|
8
9
|
import { eq, inArray } from "drizzle-orm";
|
|
9
10
|
|
|
@@ -15,13 +16,10 @@ import { acpSessionHistory } from "../memory/schema.js";
|
|
|
15
16
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
18
|
import { AcpAgentProcess } from "./agent-process.js";
|
|
19
|
+
import { resolveAgentWithAutoInstall } from "./auto-install.js";
|
|
18
20
|
import { VellumAcpClientHandler } from "./client-handler.js";
|
|
19
21
|
import { prepareAgentEnv } from "./prepare-agent-env.js";
|
|
20
|
-
import {
|
|
21
|
-
adapterCommandOf,
|
|
22
|
-
formatResolveFailure,
|
|
23
|
-
resolveAcpAgent,
|
|
24
|
-
} from "./resolve-agent.js";
|
|
22
|
+
import { formatResolveFailure } from "./resolve-agent.js";
|
|
25
23
|
import { claudeResumeHint } from "./resume-hint.js";
|
|
26
24
|
import type { AcpAgentConfig, AcpSessionState } from "./types.js";
|
|
27
25
|
|
|
@@ -74,8 +72,7 @@ interface SessionEntry {
|
|
|
74
72
|
currentPrompt: Promise<unknown> | null;
|
|
75
73
|
parentConversationId: string;
|
|
76
74
|
cwd: string;
|
|
77
|
-
/**
|
|
78
|
-
* "claude-agent-acp" even when the adapter runs via `bun x`). Used to
|
|
75
|
+
/** Resolved adapter command basename (e.g. "claude-agent-acp"). Used to
|
|
79
76
|
* gate resume hints to the only adapter (claude-agent-acp) whose CLI
|
|
80
77
|
* accepts `--resume`. */
|
|
81
78
|
command: string;
|
|
@@ -316,7 +313,7 @@ export class AcpSessionManager {
|
|
|
316
313
|
currentPrompt: null,
|
|
317
314
|
parentConversationId: opts.parentConversationId,
|
|
318
315
|
cwd: opts.cwd,
|
|
319
|
-
command:
|
|
316
|
+
command: basename(opts.agentConfig.command),
|
|
320
317
|
};
|
|
321
318
|
|
|
322
319
|
this.sessions.set(acpSessionId, entry);
|
|
@@ -351,8 +348,8 @@ export class AcpSessionManager {
|
|
|
351
348
|
* merges new events into the original row instead of losing them.
|
|
352
349
|
*
|
|
353
350
|
* Throws with an actionable message when the row is missing, was recorded
|
|
354
|
-
* before resume support (no cwd), the agent cannot be resolved
|
|
355
|
-
* agent advertises neither resume capability.
|
|
351
|
+
* before resume support (no cwd), the agent cannot be resolved or
|
|
352
|
+
* auto-installed, or the agent advertises neither resume capability.
|
|
356
353
|
*/
|
|
357
354
|
async resumeFromHistory(
|
|
358
355
|
acpSessionId: string,
|
|
@@ -388,11 +385,6 @@ export class AcpSessionManager {
|
|
|
388
385
|
);
|
|
389
386
|
}
|
|
390
387
|
|
|
391
|
-
const resolved = resolveAcpAgent(row.agentId);
|
|
392
|
-
if (!resolved.ok) {
|
|
393
|
-
throw new Error(formatResolveFailure(row.agentId, resolved));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
388
|
// Everything up to here is synchronous. Reserve the id + concurrency
|
|
397
389
|
// slot BEFORE the first await so a concurrent resume of the same id
|
|
398
390
|
// (or a spawn racing the cap) fails the guards above instead of
|
|
@@ -400,11 +392,12 @@ export class AcpSessionManager {
|
|
|
400
392
|
// reservation holds the resume's promise until it settles so
|
|
401
393
|
// steerOrResume can await a concurrent caller's in-flight resume, and
|
|
402
394
|
// so the delete guards see the id as live while the row's terminal
|
|
403
|
-
// status still reflects the previous run.
|
|
395
|
+
// status still reflects the previous run. Agent resolution (which may
|
|
396
|
+
// auto-install a missing adapter) is therefore deferred into
|
|
397
|
+
// performResume, after the slot is reserved.
|
|
404
398
|
const resumePromise = this.performResume(
|
|
405
399
|
acpSessionId,
|
|
406
400
|
row as ResumableHistoryRow,
|
|
407
|
-
resolved.agent,
|
|
408
401
|
sendToVellum,
|
|
409
402
|
);
|
|
410
403
|
this.pendingResumes.set(acpSessionId, resumePromise);
|
|
@@ -418,15 +411,34 @@ export class AcpSessionManager {
|
|
|
418
411
|
/**
|
|
419
412
|
* The async body of resumeFromHistory, split out so the caller can store
|
|
420
413
|
* its promise in `pendingResumes` synchronously before the first await.
|
|
421
|
-
* All guards and row validation have already passed
|
|
414
|
+
* All guards and row validation have already passed; this resolves the
|
|
415
|
+
* adapter (auto-installing a missing allowlisted binary), prepares its
|
|
416
|
+
* env, and reattaches.
|
|
422
417
|
*/
|
|
423
418
|
private async performResume(
|
|
424
419
|
acpSessionId: string,
|
|
425
420
|
row: ResumableHistoryRow,
|
|
426
|
-
agent: AcpAgentConfig,
|
|
427
421
|
sendToVellum: (msg: ServerMessage) => void,
|
|
428
422
|
): Promise<void> {
|
|
429
|
-
|
|
423
|
+
// Resolve the adapter, silently auto-installing a missing allowlisted
|
|
424
|
+
// binary via the same sandboxed `bun` path as spawn (see
|
|
425
|
+
// acp/auto-install.ts). A fresh container, or a session created under
|
|
426
|
+
// the old bunx fallback, may have no adapter on PATH; resume installs it
|
|
427
|
+
// transparently instead of failing with binary_not_found. The token is
|
|
428
|
+
// NEVER in scope here — resolveAgentWithAutoInstall strips it from the
|
|
429
|
+
// installer env; it is injected only by prepareAgentEnv below, at spawn
|
|
430
|
+
// time, on the real installed binary.
|
|
431
|
+
const { resolved, failureMessage } = await resolveAgentWithAutoInstall(
|
|
432
|
+
row.agentId,
|
|
433
|
+
);
|
|
434
|
+
if (failureMessage) {
|
|
435
|
+
throw new Error(failureMessage);
|
|
436
|
+
}
|
|
437
|
+
if (!resolved.ok) {
|
|
438
|
+
throw new Error(formatResolveFailure(row.agentId, resolved));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const agentConfig = await prepareAgentEnv(resolved.agent);
|
|
430
442
|
|
|
431
443
|
// The daemon may have shut down while prepareAgentEnv was pending.
|
|
432
444
|
// Registering now would spawn a child process on a disposed manager
|
package/src/acp/types.ts
CHANGED
|
@@ -12,14 +12,6 @@ export interface AcpAgentConfig {
|
|
|
12
12
|
args: string[];
|
|
13
13
|
description?: string;
|
|
14
14
|
env?: Record<string, string>;
|
|
15
|
-
/**
|
|
16
|
-
* Canonical adapter identity (e.g. "claude-agent-acp"), set by the
|
|
17
|
-
* resolver. It survives the bunx rewrite, where `command` becomes "bun"
|
|
18
|
-
* and the adapter package moves into `args`. Optional because plain user
|
|
19
|
-
* configs that bypass the resolver never set it; consumers fall back to
|
|
20
|
-
* the command basename via `adapterCommandOf` in `resolve-agent.ts`.
|
|
21
|
-
*/
|
|
22
|
-
adapterCommand?: string;
|
|
23
15
|
}
|
|
24
16
|
|
|
25
17
|
/**
|