Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,76 @@ describe("AcpRegistrySupport", () => {
});
});

it.effect("resolves an installed agent while another agent's download is still running", () => {
const slowArchiveUrl = "https://registry.test/slow-agent.bin";
const agent = makeAgent({
binary: { "linux-x86_64": { archive: archiveUrl, cmd: "./bin/example-agent" } },
});
const slowAgent = {
...agent,
distribution: {
binary: { "linux-x86_64": { archive: slowArchiveUrl, cmd: "./bin/slow-agent" } },
},
id: "slow-agent",
name: "Slow Agent",
};
const binaryBytes = new TextEncoder().encode("#!/bin/sh\necho example\n");
return Effect.gen(function* () {
const downloadStarted = yield* Deferred.make<void>();
const releaseDownload = yield* Deferred.make<void>();
return yield* Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const cacheDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-acp-registry-parallel-install-",
});
const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({
cacheDir,
toolsDir: `${cacheDir}/tools`,
registryUrl,
});

yield* resolver.resolve(settings(), "/workspace");

const prepareFiber = yield* resolver
.prepare({ agentId: slowAgent.id })
.pipe(Effect.forkChild({ startImmediately: true }));
yield* Deferred.await(downloadStarted);

const resolved = yield* resolver.resolve(settings(), "/workspace");
expect(resolved.spawn.command).toContain(
"/tools/example-agent/1.2.3/linux-x86_64/bin/example-agent",
);
expect(prepareFiber.pollUnsafe()).toBeUndefined();

yield* Deferred.succeed(releaseDownload, undefined);
expect(yield* Fiber.join(prepareFiber)).toMatchObject({ prepared: true });
}).pipe(
Effect.scoped,
Effect.provide(
layerResolver((request) => {
if (request.url === registryUrl) {
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(JSON.stringify({ version: "1.0.0", agents: [slowAgent, agent] })),
),
);
}
const response = HttpClientResponse.fromWeb(
request,
new Response(binaryBytes.buffer as ArrayBuffer),
);
if (request.url !== slowArchiveUrl) return Effect.succeed(response);
return Deferred.succeed(downloadStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseDownload)),
Effect.as(response),
);
}),
),
);
});
});

it.effect("reserves a prepared binary until a configured instance inspects it", () => {
const agent = makeAgent({
binary: {
Expand Down
20 changes: 13 additions & 7 deletions packages/provider-acp-registry/src/server/AcpRegistrySupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,13 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
const registryFetchedAtMs = yield* Ref.make<number | undefined>(undefined);
const registryRevision = yield* Ref.make(0);
const registrySemaphore = yield* Semaphore.make(1);
const installSemaphore = yield* Semaphore.make(1);
// Long installs must only block operations on the same agent.
const installPermits = new Map<string, Semaphore.Semaphore>();
const withInstallPermit = (agentId: string) => {
const permit = installPermits.get(agentId) ?? Semaphore.makeUnsafe(1);
installPermits.set(agentId, permit);
return permit.withPermits(1);
};
const preparedBinaryReservations = yield* Ref.make<ReadonlyMap<string, number>>(new Map());

const managerBinDirectory = Effect.fn("AcpRegistryCatalog.managerBinDirectory")(function* (
Expand Down Expand Up @@ -1663,13 +1669,13 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
const distribution = yield* compatibleDistribution(agent, "auto");
if (distribution.kind === "binary") {
// Only explicit command overrides may use PATH copies with different ACP capabilities.
yield* installSemaphore.withPermits(1)(
yield* withInstallPermit(agent.id)(
installBinary(agent, distribution.binaryTarget!).pipe(
Effect.tap(() => reservePreparedBinary(agent.id)),
),
);
} else {
yield* installSemaphore.withPermits(1)(
yield* withInstallPermit(agent.id)(
ensureGlobalPackage(agent, distribution.kind, distribution.packageName!, hostEnvironment),
);
}
Expand Down Expand Up @@ -1746,7 +1752,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
}

if (distribution.kind === "binary") {
return yield* installSemaphore.withPermits(1)(
return yield* withInstallPermit(agent.id)(
Effect.gen(function* () {
const paths = binaryPaths(agent, distribution.binaryTarget!);
if (paths === undefined) {
Expand Down Expand Up @@ -1858,7 +1864,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
command = resolvedOverride;
args = distribution.args;
} else if (distribution.kind === "npx" || distribution.kind === "uvx") {
const installed = yield* installSemaphore.withPermits(1)(
const installed = yield* withInstallPermit(agent.id)(
ensureGlobalPackage(
agent,
distribution.kind,
Expand All @@ -1871,7 +1877,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
commandBinDirectory = installed.binDirectory;
} else {
const target = distribution.binaryTarget!;
command = yield* installSemaphore.withPermits(1)(
command = yield* withInstallPermit(agent.id)(
installBinary(agent, target).pipe(
Effect.tap(() => consumePreparedBinaryReservation(agent.id)),
),
Expand Down Expand Up @@ -1904,7 +1910,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
const uninstallManagedBinary: AcpRegistryCatalog["Service"]["uninstallManagedBinary"] = (input) =>
hostSettings
.withSnapshot((settings) =>
installSemaphore.withPermits(1)(
withInstallPermit(input.agentId)(
Effect.gen(function* () {
const safeAgentId = yield* decodeBoundedAgentId(input.agentId).pipe(
Effect.mapError(
Expand Down
Loading