authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-09 09:56:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 09:56:39-07:00
logf853d514cab475e73468f7cb7bab1aae48dbe549
tree36873d827cc8e363ee25749de8a22c7e2b759167
parent31b04e2ee929660d80d5931ed53fd17bf1d33ce6
signature Signed by PGP key B5690EEEBB952194

refactor(provider-acp): ACP, ACP Registry and Grok follow the Effect service conventions (#17544)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

28 files changed, 436 insertions(+), 452 deletions(-)

apps/server/scripts/record-grok-acp-replay-fixture.ts-3
......@@ -21,7 +21,6 @@ import * as FileSystem from "effect/FileSystem";
2121import * as Layer from "effect/Layer";
2222import * as Path from "effect/Path";
2323import * as Schema from "effect/Schema";
24import { ChildProcessSpawner } from "effect/process";
2524import type * as EffectAcpProtocol from "effect-acp/protocol";
2625
2726import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
......@@ -443,7 +442,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
443442 const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" };
444443 const layerRegistry = ProviderAdapterRegistry.layerFromAdaptersEffect(
445444 Effect.gen(function* () {
446 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
447445 const environment = yield* HostProcessEnvironment;
448446 const adapter = yield* makeGrokAdapterV2({
449447 instanceId: GROK_DEFAULT_INSTANCE_ID,
......@@ -460,7 +458,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
460458 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
461459 grokSettings: settings,
462460 environment,
463 childProcessSpawner,
464461 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
465462 }),
466463 });
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+23-15
......@@ -48,7 +48,7 @@ import * as Sink from "effect/Sink";
4848import * as Stream from "effect/Stream";
4949import * as Scope from "effect/Scope";
5050import * as TestClock from "effect/testing/TestClock";
51import { ChildProcessSpawner } from "effect/process";
51import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
5252import * as EffectAcpErrors from "effect-acp/errors";
5353import type * as EffectAcpProtocol from "effect-acp/protocol";
5454import type * as EffectAcpSchema from "effect-acp/compat";
......@@ -1150,7 +1150,7 @@ describe("AcpAdapterV2", () => {
11501150 const adapter = yield* makeAcpAdapterV2({
11511151 instanceId,
11521152 // Production Devin runs commands through client terminals.
1153 clientTerminals: { childProcessSpawner, shellCommands: true },
1153 clientTerminals: { shellCommands: true },
11541154 selfInvocation: yield* resolveSelfInvocation(),
11551155 flavor: {
11561156 driver: ACP_TEST_DRIVER,
......@@ -2190,7 +2190,7 @@ describe("AcpAdapterV2", () => {
21902190 makeRuntime,
21912191 },
21922192 selfInvocation,
2193 clientTerminals: { childProcessSpawner },
2193 clientTerminals: {},
21942194 });
21952195 const sourceThreadId = ThreadId.make("thread-acp-native-fork-source");
21962196 const targetThreadId = ThreadId.make("thread-acp-native-fork-target");
......@@ -2476,7 +2476,7 @@ describe("AcpAdapterV2", () => {
24762476 }).pipe(Effect.andThen(makeRuntime(input))),
24772477 },
24782478 selfInvocation,
2479 clientTerminals: { childProcessSpawner },
2479 clientTerminals: {},
24802480 });
24812481 const threadId = ThreadId.make("thread-acp-unknown-permission-grant");
24822482 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
......@@ -12045,9 +12045,11 @@ describe("AcpAdapterV2", () => {
1204512045 Effect.gen(function* () {
1204612046 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
1204712047 [];
12048 yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(
12049 makeTaskkillSpawner({ exitCode: 0, commands }),
12050 4321,
12048 yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12049 Effect.provideService(
12050 ChildProcessSpawner.ChildProcessSpawner,
12051 makeTaskkillSpawner({ exitCode: 0, commands }),
12052 ),
1205112053 );
1205212054 assert.deepEqual(commands, [{ command: "taskkill", args: ["/PID", "4321", "/T", "/F"] }]);
1205312055
......@@ -12058,10 +12060,13 @@ describe("AcpAdapterV2", () => {
1205812060 { exitCode: 128, output: "ERROR: Access is denied." },
1205912061 { exitCode: 1, output: "generic failure" },
1206012062 ]) {
12061 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(
12062 makeTaskkillSpawner(fixture),
12063 4321,
12064 ).pipe(Effect.exit);
12063 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12064 Effect.provideService(
12065 ChildProcessSpawner.ChildProcessSpawner,
12066 makeTaskkillSpawner(fixture),
12067 ),
12068 Effect.exit,
12069 );
1206512070 if (Exit.isSuccess(failed)) assert.fail(`taskkill ${fixture.exitCode} must fail`);
1206612071 const error = Cause.squash(failed.cause);
1206712072 assert.instanceOf(error, AcpSessionRuntime.AcpProcessGroupTerminationError);
......@@ -12082,10 +12087,13 @@ describe("AcpAdapterV2", () => {
1208212087 { outputFailure: true },
1208312088 { exitFailure: true },
1208412089 ]) {
12085 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(
12086 makeTaskkillSpawner(fixture),
12087 4321,
12088 ).pipe(Effect.exit);
12090 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12091 Effect.provideService(
12092 ChildProcessSpawner.ChildProcessSpawner,
12093 makeTaskkillSpawner(fixture),
12094 ),
12095 Effect.exit,
12096 );
1208912097 if (Exit.isSuccess(failed)) assert.fail("taskkill infrastructure failure must fail");
1209012098 const error = Cause.squash(failed.cause);
1209112099 assert.instanceOf(error, AcpSessionRuntime.AcpProcessGroupTerminationError);
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.testkit.ts+1-1
......@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";
1010import * as Schema from "effect/Schema";
1111import type * as Scope from "effect/Scope";
1212import * as Stream from "effect/Stream";
13import { ChildProcessSpawner } from "effect/process";
13import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1414import * as EffectAcpErrors from "effect-acp/errors";
1515
1616import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts+64-58
......@@ -9,8 +9,9 @@ import * as Layer from "effect/Layer";
99import * as Option from "effect/Option";
1010import * as Path from "effect/Path";
1111import * as Schema from "effect/Schema";
12import { HttpClient, HttpClientResponse } from "effect/http";
13import { ChildProcessSpawner } from "effect/process";
12import * as HttpClient from "effect/http/HttpClient";
13import * as HttpClientResponse from "effect/http/HttpClientResponse";
14import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1415import * as EffectAcpErrors from "effect-acp/errors";
1516
1617import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
......@@ -22,6 +23,7 @@ import type {
2223 AcpRegistryLiveConfiguration,
2324} from "@t3tools/provider-acp-registry/testing";
2425import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
26import * as AcpRegistryRuntimeCoordinator from "@t3tools/provider-acp-registry/server/AcpRegistryRuntimeCoordinator";
2527import { ACP_SESSION_MODE_OPTION_ID } from "@t3tools/provider-acp/server/sessionConfig";
2628import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
2729import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
......@@ -82,6 +84,11 @@ const layerRegistry = Layer.succeed(
8284 ),
8385);
8486
87// Adapters given an explicit runtime never resolve through the catalog.
88const layerInjectedRuntimeCatalog = Layer.mock(AcpRegistrySupport.AcpRegistryCatalog)({
89 resolve: () => Effect.die("the runtime is injected"),
90});
91
8592const layerTest = Layer.mergeAll(
8693 NodeServices.layer,
8794 IdAllocator.layer,
......@@ -197,8 +204,6 @@ describe("AcpRegistryAdapterV2", () => {
197204 authMethodId: "test",
198205 }),
199206 environment: {},
200 childProcessSpawner,
201 resolver: { resolve: () => Effect.die("the runtime is injected") },
202207 makeRuntime: makeAcpReplayRuntime({
203208 transcript,
204209 statusPath,
......@@ -208,7 +213,7 @@ describe("AcpRegistryAdapterV2", () => {
208213 childProcessSpawner,
209214 fileSystem,
210215 }),
211 });
216 }).pipe(Effect.provide(layerInjectedRuntimeCatalog));
212217 yield* adapter
213218 .openSession({
214219 threadId: ThreadId.make("thread-acp-registry-mode-pick"),
......@@ -279,8 +284,6 @@ describe("AcpRegistryAdapterV2", () => {
279284 instanceId,
280285 settings: yield* decodeAcpRegistryAdapterSettings({ agentId, authMethodId: "test" }),
281286 environment: {},
282 childProcessSpawner,
283 resolver: { resolve: () => Effect.die("the runtime is injected") },
284287 makeRuntime: (input) =>
285288 Effect.gen(function* () {
286289 clientCapabilities = input.clientCapabilities;
......@@ -305,7 +308,7 @@ describe("AcpRegistryAdapterV2", () => {
305308 Effect.provide(context),
306309 );
307310 }),
308 });
311 }).pipe(Effect.provide(layerInjectedRuntimeCatalog));
309312 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
310313 runtimeMode: "full-access",
311314 interactionMode: "default",
......@@ -333,7 +336,6 @@ describe("AcpRegistryAdapterV2", () => {
333336
334337 it.effect("opens a real ACP child process resolved from registry configuration", () =>
335338 Effect.gen(function* () {
336 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
337339 const path = yield* Path.Path;
338340 const host = yield* ProviderHost.ProviderHost;
339341 const mockAgentPath = yield* path.fromFileUrl(
......@@ -365,55 +367,59 @@ describe("AcpRegistryAdapterV2", () => {
365367 T3_ACP_SESSION_LIFECYCLE: "1",
366368 T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS: "750",
367369 },
368 childProcessSpawner,
369 runtimeCoordinator: {
370 withForegroundStartup: (agentId, effect) =>
371 Effect.acquireUseRelease(
372 Effect.sync(() => {
373 assert.equal(agentId, "fixture-agent");
374 startupActive = true;
375 startupCount += 1;
376 }),
377 () => effect,
378 () =>
379 Effect.sync(() => {
380 startupActive = false;
381 }),
382 ),
383 runBackgroundProbe: (_agentId, effect) => effect.pipe(Effect.map(Option.some)),
384 withSessionMutation: (effect) => effect,
385 clearAvailableCommands: () => Effect.void,
386 publishAvailableCommands: (publishedInstanceId, commands) =>
387 Deferred.succeed(commandsPublished, {
388 instanceId: publishedInstanceId,
389 commands,
390 }).pipe(Effect.asVoid),
391 getAvailableCommands: () => Effect.succeed(Option.none()),
392 watchAvailableCommands: () => Effect.never,
393 clearLiveConfiguration: () => Effect.void,
394 publishLiveConfiguration: (_publishedInstanceId, configuration) =>
395 Deferred.succeed(configurationPublished, configuration).pipe(Effect.asVoid),
396 getLiveConfiguration: () => Effect.succeed(Option.none()),
397 watchLiveConfiguration: () => Effect.never,
398 requestUrlAuthentication: () => Effect.succeed(false),
399 acceptUrlAuthentication: () => Effect.succeed(false),
400 getUrlAuthAction: () => Effect.succeed(Option.none()),
401 watchUrlAuthAction: () => Effect.never,
402 },
403 resolver: {
404 resolve: (configuredSettings, cwd, environment) =>
405 Effect.sync(() => assert.isTrue(startupActive)).pipe(
406 Effect.andThen(resolver.resolve(configuredSettings, cwd, environment)),
407 Effect.map((resolved) => ({
408 ...resolved,
409 spawn: {
410 ...resolved.spawn,
411 args: [mockAgentPath],
412 },
413 })),
414 ),
415 },
416 });
370 }).pipe(
371 Effect.provide(
372 Layer.mergeAll(
373 Layer.mock(AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator)({
374 withForegroundStartup: (agentId, effect) =>
375 Effect.acquireUseRelease(
376 Effect.sync(() => {
377 assert.equal(agentId, "fixture-agent");
378 startupActive = true;
379 startupCount += 1;
380 }),
381 () => effect,
382 () =>
383 Effect.sync(() => {
384 startupActive = false;
385 }),
386 ),
387 runBackgroundProbe: (_agentId, effect) => effect.pipe(Effect.map(Option.some)),
388 withSessionMutation: (effect) => effect,
389 clearAvailableCommands: () => Effect.void,
390 publishAvailableCommands: (publishedInstanceId, commands) =>
391 Deferred.succeed(commandsPublished, {
392 instanceId: publishedInstanceId,
393 commands,
394 }).pipe(Effect.asVoid),
395 getAvailableCommands: () => Effect.succeed(Option.none()),
396 watchAvailableCommands: () => Effect.never,
397 clearLiveConfiguration: () => Effect.void,
398 publishLiveConfiguration: (_publishedInstanceId, configuration) =>
399 Deferred.succeed(configurationPublished, configuration).pipe(Effect.asVoid),
400 getLiveConfiguration: () => Effect.succeed(Option.none()),
401 watchLiveConfiguration: () => Effect.never,
402 requestUrlAuthentication: () => Effect.succeed(false),
403 acceptUrlAuthentication: () => Effect.succeed(false),
404 getUrlAuthAction: () => Effect.succeed(Option.none()),
405 watchUrlAuthAction: () => Effect.never,
406 }),
407 Layer.mock(AcpRegistrySupport.AcpRegistryCatalog)({
408 resolve: (configuredSettings, cwd, environment) =>
409 Effect.sync(() => assert.isTrue(startupActive)).pipe(
410 Effect.andThen(resolver.resolve(configuredSettings, cwd, environment)),
411 Effect.map((resolved) => ({
412 ...resolved,
413 spawn: {
414 ...resolved.spawn,
415 args: [mockAgentPath],
416 },
417 })),
418 ),
419 }),
420 ),
421 ),
422 );
417423 const threadId = ThreadId.make("thread-acp-registry-fixture");
418424 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
419425 runtimeMode: "full-access",
apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts+12-6
......@@ -6,7 +6,7 @@ import * as FileSystem from "effect/FileSystem";
66import * as Layer from "effect/Layer";
77import * as Path from "effect/Path";
88import * as Schema from "effect/Schema";
9import { ChildProcessSpawner } from "effect/process";
9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1010
1111import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
1212import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
......@@ -25,6 +25,7 @@ import {
2525 ACP_REGISTRY_PROVIDER,
2626 makeAcpRegistryAdapterV2,
2727} from "@t3tools/provider-acp-registry/testing";
28import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
2829
2930const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({
3031 agentId: "replay-agent",
......@@ -56,10 +57,6 @@ function layerAcpRegistryProviderAdapterRegistryReplay(
5657 instanceId: ACP_REGISTRY_DEFAULT_INSTANCE_ID,
5758 settings: REPLAY_SETTINGS,
5859 environment: {},
59 childProcessSpawner,
60 resolver: {
61 resolve: () => Effect.die("ACP registry resolver must not run during replay"),
62 },
6360 selfInvocation: yield* resolveSelfInvocation(),
6461 makeRuntime: makeAcpReplayRuntime({
6562 transcript,
......@@ -74,7 +71,16 @@ function layerAcpRegistryProviderAdapterRegistryReplay(
7471 return [adapter];
7572 }),
7673 ).pipe(
77 Layer.provide(Layer.mergeAll(layerHost, NodeServices.layer, IdAllocator.layer)),
74 Layer.provide(
75 Layer.mergeAll(
76 layerHost,
77 NodeServices.layer,
78 IdAllocator.layer,
79 Layer.mock(AcpRegistrySupport.AcpRegistryCatalog)({
80 resolve: () => Effect.die("ACP registry resolver must not run during replay"),
81 }),
82 ),
83 ),
7884 // Held inbound lines must not outlive the scenario and wedge teardown.
7985 Layer.merge(
8086 Layer.effectDiscard(
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts+1-1
......@@ -17,7 +17,7 @@ import * as Effect from "effect/Effect";
1717import * as Layer from "effect/Layer";
1818import * as PlatformError from "effect/PlatformError";
1919import * as Schema from "effect/Schema";
20import { ChildProcessSpawner } from "effect/process";
20import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2121import type * as EffectAcpSchema from "effect-acp/compat";
2222
2323import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts+1-1
......@@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem";
55import * as Layer from "effect/Layer";
66import * as Path from "effect/Path";
77import * as Schema from "effect/Schema";
8import { ChildProcessSpawner } from "effect/process";
8import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
99import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1010import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
1111
apps/server/src/terminal/Manager.ts+4-3
......@@ -2253,13 +2253,14 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
22532253 options.managedBinaryToolsDir !== undefined
22542254 ) {
22552255 const managedDirectories = yield* acpRegistryManagedBinaryDirectories({
2256 fileSystem,
2257 path,
22582256 cacheDir: options.managedBinaryCacheDir,
22592257 toolsDir: options.managedBinaryToolsDir,
22602258 platform,
22612259 architecture,
2262 });
2260 }).pipe(
2261 Effect.provideService(FileSystem.FileSystem, fileSystem),
2262 Effect.provideService(Path.Path, path),
2263 );
22632264 if (managedDirectories.length > 0) {
22642265 const delimiter = platform === "win32" ? ";" : ":";
22652266 const pathKey =
packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts+4-6
......@@ -19,9 +19,11 @@ import * as Path from "effect/Path";
1919import * as Fiber from "effect/Fiber";
2020import * as Layer from "effect/Layer";
2121import * as Schema from "effect/Schema";
22import { HttpClient, HttpClientResponse } from "effect/http";
22import * as HttpClient from "effect/http/HttpClient";
23import * as HttpClientResponse from "effect/http/HttpClientResponse";
2324import * as TestClock from "effect/testing/TestClock";
24import { ChildProcess, ChildProcessSpawner } from "effect/process";
25import * as ChildProcess from "effect/process/ChildProcess";
26import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2527
2628import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
2729import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
......@@ -1713,8 +1715,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {
17131715 );
17141716
17151717 const directories = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({
1716 fileSystem,
1717 path,
17181718 cacheDir,
17191719 toolsDir: path.join(cacheDir, "tools"),
17201720 platform: "linux",
......@@ -1727,8 +1727,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {
17271727 ]);
17281728
17291729 const missing = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({
1730 fileSystem,
1731 path,
17321730 cacheDir: path.join(cacheDir, "does-not-exist"),
17331731 toolsDir: path.join(cacheDir, "does-not-exist", "tools"),
17341732 platform: "linux",
packages/provider-acp-registry/src/server/AcpRegistrySupport.ts+31-27
......@@ -39,8 +39,11 @@ import * as Schema from "effect/Schema";
3939import * as Semaphore from "effect/Semaphore";
4040import * as Stream from "effect/Stream";
4141import { sha256 } from "@noble/hashes/sha2";
42import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
43import { ChildProcess, ChildProcessSpawner } from "effect/process";
42import * as HttpClient from "effect/http/HttpClient";
43import * as HttpClientRequest from "effect/http/HttpClientRequest";
44import * as HttpClientResponse from "effect/http/HttpClientResponse";
45import * as ChildProcess from "effect/process/ChildProcess";
46import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
4447
4548import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";
4649import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
......@@ -308,26 +311,26 @@ function withPreferredPath(
308311 * managed binary agents are both available by name. Best effort: unreadable
309312 * directories resolve to no entries.
310313 */
311export const acpRegistryManagedBinaryDirectories = (input: {
312 readonly fileSystem: FileSystem.FileSystem;
313 readonly path: Path.Path;
314 readonly cacheDir: string;
315 readonly toolsDir: string;
316 readonly platform: NodeJS.Platform;
317 readonly architecture: NodeJS.Architecture;
318}): Effect.Effect<ReadonlyArray<string>> =>
319 Effect.gen(function* () {
314export const acpRegistryManagedBinaryDirectories = Effect.fn("acpRegistryManagedBinaryDirectories")(
315 function* (input: {
316 readonly cacheDir: string;
317 readonly toolsDir: string;
318 readonly platform: NodeJS.Platform;
319 readonly architecture: NodeJS.Architecture;
320 }): Effect.fn.Return<ReadonlyArray<string>, never, FileSystem.FileSystem | Path.Path> {
321 const fileSystem = yield* FileSystem.FileSystem;
322 const path = yield* Path.Path;
320323 const target = resolveAcpRegistryPlatformTarget(input.platform, input.architecture);
321 const registryDirectory = input.path.join(input.cacheDir, "acp-registry");
324 const registryDirectory = path.join(input.cacheDir, "acp-registry");
322325 const installsDirectory = input.toolsDir;
323 const packageReceiptsDirectory = input.path.join(registryDirectory, "package-installs");
326 const packageReceiptsDirectory = path.join(registryDirectory, "package-installs");
324327 const listDirectories = (directory: string) =>
325 input.fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): Array<string> => []));
328 fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): Array<string> => []));
326329 const directories: Array<string> = [];
327330
328331 for (const receiptFile of (yield* listDirectories(packageReceiptsDirectory)).toSorted()) {
329 const receipt = yield* input.fileSystem
330 .readFileString(input.path.join(packageReceiptsDirectory, receiptFile))
332 const receipt = yield* fileSystem
333 .readFileString(path.join(packageReceiptsDirectory, receiptFile))
331334 .pipe(
332335 Effect.map(decodePackageInstallReceipt),
333336 Effect.orElseSucceed(() => Option.none()),
......@@ -335,15 +338,15 @@ export const acpRegistryManagedBinaryDirectories = (input: {
335338 if (
336339 Option.isSome(receipt) &&
337340 receipt.value.binDirectory ===
338 input.path.join(
341 path.join(
339342 installsDirectory,
340343 receipt.value.agentId,
341344 encodeURIComponent(receipt.value.agentVersion),
342345 receipt.value.distribution === "npx" ? "npm" : "python",
343346 ...(receipt.value.distribution === "npx" && input.platform === "win32" ? [] : ["bin"]),
344347 ) &&
345 input.path.dirname(receipt.value.executablePath) === receipt.value.binDirectory &&
346 (yield* input.fileSystem
348 path.dirname(receipt.value.executablePath) === receipt.value.binDirectory &&
349 (yield* fileSystem
347350 .exists(receipt.value.executablePath)
348351 .pipe(Effect.orElseSucceed(() => false)))
349352 ) {
......@@ -352,8 +355,8 @@ export const acpRegistryManagedBinaryDirectories = (input: {
352355 }
353356
354357 if (target === undefined) return Array.from(new Set(directories));
355 const cachedAgents = yield* input.fileSystem
356 .readFileString(input.path.join(registryDirectory, "registry.json"))
358 const cachedAgents = yield* fileSystem
359 .readFileString(path.join(registryDirectory, "registry.json"))
357360 .pipe(
358361 Effect.flatMap(decodeJson),
359362 Effect.flatMap(decodeRegistryIndexEnvelope),
......@@ -372,12 +375,12 @@ export const acpRegistryManagedBinaryDirectories = (input: {
372375 ),
373376 );
374377 for (const agent of (yield* listDirectories(installsDirectory)).toSorted()) {
375 const versions = (yield* listDirectories(input.path.join(installsDirectory, agent))).toSorted(
378 const versions = (yield* listDirectories(path.join(installsDirectory, agent))).toSorted(
376379 (left, right) => right.localeCompare(left, undefined, { numeric: true }),
377380 );
378381 for (const version of versions) {
379 const installRoot = input.path.join(installsDirectory, agent, version, target);
380 if (yield* input.fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) {
382 const installRoot = path.join(installsDirectory, agent, version, target);
383 if (yield* fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) {
381384 const binaryTarget = cachedAgentByInstall.get(`${agent}\0${version}`)?.distribution
382385 .binary?.[target];
383386 const commandSegments =
......@@ -385,15 +388,16 @@ export const acpRegistryManagedBinaryDirectories = (input: {
385388 const directory =
386389 commandSegments === undefined
387390 ? installRoot
388 : input.path.join(installRoot, ...commandSegments.slice(0, -1));
389 if (yield* input.fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) {
391 : path.join(installRoot, ...commandSegments.slice(0, -1));
392 if (yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) {
390393 directories.push(directory);
391394 }
392395 }
393396 }
394397 }
395398 return Array.from(new Set(directories));
396 });
399 },
400);
397401
398402export interface ResolvedAcpRegistryDistribution {
399403 readonly kind: AcpRegistryDistributionKind;
packages/provider-acp-registry/src/server/adapter.ts+14-26
......@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";
1515import * as Option from "effect/Option";
1616import * as Schema from "effect/Schema";
1717import type * as Scope from "effect/Scope";
18import { ChildProcessSpawner } from "effect/process";
18import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1919import * as EffectAcpErrors from "effect-acp/errors";
2020
2121import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
......@@ -56,17 +56,14 @@ export interface AcpRegistryAdapterV2Options {
5656 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];
5757 readonly settings: AcpRegistrySettings;
5858 readonly environment: NodeJS.ProcessEnv;
59 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
6059 readonly selfInvocation: SelfInvocation;
61 readonly resolver: Pick<AcpRegistrySupport.AcpRegistryCatalog["Service"], "resolve">;
62 readonly runtimeCoordinator?: AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator["Service"];
6360 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
6461 readonly makeRuntime?: (
6562 input: AcpAdapterV2RuntimeInput,
6663 ) => Effect.Effect<
6764 AcpSessionRuntime.AcpSessionRuntime["Service"],
6865 EffectAcpErrors.AcpError,
69 Crypto.Crypto | Scope.Scope
66 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
7067 >;
7168 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
7269}
......@@ -131,17 +128,20 @@ export function acpRegistryPromptFailure(agentId: string, cause: unknown) {
131128 });
132129}
133130
134function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {
131function makeAcpRegistryRuntime(
132 options: AcpRegistryAdapterV2Options,
133 catalog: AcpRegistrySupport.AcpRegistryCatalog["Service"],
134) {
135135 return (
136136 input: AcpAdapterV2RuntimeInput,
137137 ): Effect.Effect<
138138 AcpSessionRuntime.AcpSessionRuntime["Service"],
139139 EffectAcpErrors.AcpError,
140 Crypto.Crypto | Scope.Scope
140 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
141141 > =>
142142 Effect.gen(function* () {
143143 const { processEnvironment, ...runtimeInput } = input;
144 const resolved = yield* options.resolver
144 const resolved = yield* catalog
145145 .resolve(options.settings, input.cwd, options.environment)
146146 .pipe(
147147 Effect.mapError(
......@@ -163,11 +163,7 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {
163163 env: { ...resolved.spawn.env, ...processEnvironment },
164164 },
165165 ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),
166 }).pipe(
167 Layer.provide(
168 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, options.childProcessSpawner),
169 ),
170 ),
166 }),
171167 );
172168 return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
173169 Effect.provide(context),
......@@ -178,7 +174,10 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {
178174export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(function* (
179175 options: AcpRegistryAdapterV2Options,
180176) {
181 const runtimeCoordinator = options.runtimeCoordinator;
177 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
178 const runtimeCoordinator = Option.getOrUndefined(
179 yield* Effect.serviceOption(AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator),
180 );
182181 const registryAgentId = options.settings.source === "local" ? "" : options.settings.agentId;
183182 const startupKey =
184183 options.settings.source === "local" ? `local:${options.instanceId}` : registryAgentId;
......@@ -203,7 +202,7 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
203202 extractSubagentUpdate: extractDevinSubagentUpdate,
204203 }
205204 : {}),
206 makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options),
205 makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options, catalog),
207206 ...(runtimeCoordinator === undefined
208207 ? {}
209208 : {
......@@ -243,7 +242,6 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
243242 ...(isDevin
244243 ? {
245244 clientTerminals: {
246 childProcessSpawner: options.childProcessSpawner,
247245 environment: options.environment,
248246 shellCommands: true,
249247 },
......@@ -275,22 +273,12 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<
275273 function* (input: ProviderAdapterDriverCreateInput<AcpRegistrySettings>) {
276274 const hostEnvironment = yield* HostProcessEnvironment;
277275 const selfInvocation = yield* resolveSelfInvocation();
278 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
279276 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
280277 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
281 const resolver = yield* AcpRegistrySupport.AcpRegistryCatalog;
282 const runtimeCoordinator = yield* Effect.serviceOption(
283 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
284 );
285278 return yield* makeAcpRegistryAdapterV2({
286279 instanceId: input.instanceId,
287280 settings: { ...input.config, enabled: input.enabled },
288281 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
289 childProcessSpawner,
290 resolver,
291 ...(Option.isSome(runtimeCoordinator)
292 ? { runtimeCoordinator: runtimeCoordinator.value }
293 : {}),
294282 selfInvocation,
295283 nativeLogging: (threadId) =>
296284 makeNativeLogger({
packages/provider-acp-registry/src/server/auth.ts+1-1
......@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";
1212import * as Option from "effect/Option";
1313import * as Queue from "effect/Queue";
1414import * as Scope from "effect/Scope";
15import { ChildProcessSpawner } from "effect/process";
15import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1616import type * as AcpSchema from "effect-acp/compat";
1717
1818import * as PtyAdapter from "@t3tools/shared/PtyAdapter";
packages/provider-acp-registry/src/server/driver.ts+1-1
......@@ -19,7 +19,7 @@ import * as Ref from "effect/Ref";
1919import * as Result from "effect/Result";
2020import * as Schema from "effect/Schema";
2121import * as Semaphore from "effect/Semaphore";
22import { ChildProcessSpawner } from "effect/process";
22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2323
2424import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
2525import { AcpRegistryAdapterV2Driver, type AcpRegistryAdapterV2DriverEnv } from "./adapter.ts";
packages/provider-acp-registry/src/server/probe.ts+1-1
......@@ -19,7 +19,7 @@ import * as Layer from "effect/Layer";
1919import * as Option from "effect/Option";
2020import * as Ref from "effect/Ref";
2121import * as Result from "effect/Result";
22import { ChildProcessSpawner } from "effect/process";
22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2323import type * as EffectAcpErrors from "effect-acp/errors";
2424import type * as EffectAcpSchema from "effect-acp/compat";
2525
packages/provider-acp/src/server/AcpSessionRuntime.ts+9-4
......@@ -1055,11 +1055,11 @@ export function windowsTaskkillResultIsSuccess(exitCode: number, _output?: strin
10551055}
10561056
10571057export const terminateWindowsProcessTreeWithTaskkill = (
1058 spawner: ChildProcessSpawner.ChildProcessSpawner["Service"],
10591058 pid: number,
1060): Effect.Effect<void, AcpProcessGroupTerminationError> =>
1059): Effect.Effect<void, AcpProcessGroupTerminationError, ChildProcessSpawner.ChildProcessSpawner> =>
10611060 Effect.scoped(
10621061 Effect.gen(function* () {
1062 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
10631063 const taskkill = yield* spawner.spawn(
10641064 ChildProcess.make("taskkill", ["/PID", String(pid), "/T", "/F"]),
10651065 );
......@@ -1659,7 +1659,10 @@ export const make = (
16591659 );
16601660 const terminateWindowsProcessTree =
16611661 options.windowsProcessTreeTerminator ??
1662 ((pid: number) => terminateWindowsProcessTreeWithTaskkill(spawner, pid));
1662 ((pid: number) =>
1663 terminateWindowsProcessTreeWithTaskkill(pid).pipe(
1664 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1665 ));
16631666 const terminatePosixProcessTree = (grace: Duration.Input, platformOverride?: NodeJS.Platform) =>
16641667 Effect.gen(function* () {
16651668 const platform = platformOverride ?? options.processGroupPlatform;
......@@ -1728,7 +1731,9 @@ export const make = (
17281731 const hostPlatform = yield* HostProcessPlatform;
17291732 const forceTerminateOwnedProcessGroup =
17301733 hostPlatform === "win32"
1731 ? terminateWindowsProcessTreeWithTaskkill(spawner, Number(child.pid))
1734 ? terminateWindowsProcessTreeWithTaskkill(Number(child.pid)).pipe(
1735 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1736 )
17321737 : linuxCgroupLease !== undefined
17331738 ? terminateLinuxCgroupLease(linuxCgroupLease)
17341739 : options.ownDescendantProcessGroups === true
packages/provider-acp/src/server/adapter.ts+10-7
......@@ -50,7 +50,7 @@ import * as Result from "effect/Result";
5050import * as Scope from "effect/Scope";
5151import * as Semaphore from "effect/Semaphore";
5252import * as Stream from "effect/Stream";
53import type { ChildProcessSpawner } from "effect/process";
53import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
5454import * as EffectAcpErrors from "effect-acp/errors";
5555import type * as EffectAcpProtocol from "effect-acp/protocol";
5656import type * as EffectAcpSchema from "effect-acp/compat";
......@@ -239,7 +239,7 @@ export interface AcpAdapterV2Flavor {
239239 ) => Effect.Effect<
240240 AcpSessionRuntime.AcpSessionRuntime["Service"],
241241 EffectAcpErrors.AcpError,
242 Crypto.Crypto | Scope.Scope
242 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
243243 >;
244244 readonly resolveModelId?: (selection: ModelSelection) => string | undefined;
245245 /**
......@@ -485,11 +485,10 @@ export interface AcpAdapterV2Options {
485485 /**
486486 * Opts the session into the ACP client `terminal` capability. Agents run
487487 * commands themselves unless an adapter sets this; with it, sessions
488 * advertise `terminal: true` and run agent-created terminals through this
489 * spawner with the provider instance's environment. Devin sets it.
488 * advertise `terminal: true` and run agent-created terminals through the
489 * adapter's spawner with the provider instance's environment. Devin sets it.
490490 */
491491 readonly clientTerminals?: {
492 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
493492 readonly environment?: NodeJS.ProcessEnv;
494493 readonly shellCommands?: boolean;
495494 };
......@@ -1486,6 +1485,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
14861485) {
14871486 const crypto = yield* Crypto.Crypto;
14881487 const fileSystem = yield* FileSystem.FileSystem;
1488 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
14891489 const idAllocator = yield* IdAllocator.IdAllocatorV2;
14901490 const host = yield* ProviderHost.ProviderHost;
14911491 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
......@@ -1586,7 +1586,6 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
15861586 options.clientTerminals === undefined
15871587 ? undefined
15881588 : yield* makeAcpClientTerminals({
1589 spawner: options.clientTerminals.childProcessSpawner,
15901589 defaultCwd: input.runtimePolicy.cwd ?? process.cwd(),
15911590 environment: options.clientTerminals.environment,
15921591 shellCommands: options.clientTerminals.shellCommands,
......@@ -1607,7 +1606,9 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
16071606 ? pendingTerminalEnvironment.environment
16081607 : undefined;
16091608 },
1610 });
1609 }).pipe(
1610 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
1611 );
16111612 if (clientTerminals !== undefined) {
16121613 yield* Scope.addFinalizer(sessionScope, clientTerminals.disposeAll);
16131614 }
......@@ -6018,6 +6019,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
60186019 .pipe(
60196020 Effect.provideService(Scope.Scope, runtimeScope),
60206021 Effect.provideService(Crypto.Crypto, crypto),
6022 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
60216023 );
60226024 });
60236025
......@@ -6096,6 +6098,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
60966098 .pipe(
60976099 Effect.provideService(Scope.Scope, replacementScope),
60986100 Effect.provideService(Crypto.Crypto, crypto),
6101 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
60996102 );
61006103 // Session setup may publish commands before it returns. Buffer those
61016104 // notifications, but do not expose request or extension handlers
packages/provider-acp/src/server/clientTerminals.test.ts-6
......@@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
22import { describe, expect, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as Result from "effect/Result";
5import { ChildProcessSpawner } from "effect/process";
65
76import {
87 makeAcpClientTerminals,
......@@ -12,9 +11,7 @@ import {
1211
1312const withTerminals = <A, E>(use: (terminals: AcpClientTerminals) => Effect.Effect<A, E>) =>
1413 Effect.gen(function* () {
15 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1614 const terminals = yield* makeAcpClientTerminals({
17 spawner,
1815 defaultCwd: process.cwd(),
1916 });
2017 return yield* use(terminals).pipe(Effect.ensuring(terminals.disposeAll));
......@@ -49,9 +46,7 @@ describe("AcpClientTerminals", () => {
4946
5047 it.effect("resolves terminal environment from the requesting ACP session", () =>
5148 Effect.gen(function* () {
52 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
5349 const terminals = yield* makeAcpClientTerminals({
54 spawner,
5550 defaultCwd: process.cwd(),
5651 environmentForSession: (sessionId) => ({
5752 T3_TEST_ACP_SESSION_TOKEN: sessionId === "session-a" ? "token-a" : "token-b",
......@@ -78,7 +73,6 @@ describe("AcpClientTerminals", () => {
7873 it.effect("runs Devin shell source with arguments, pipes, and session fallback variables", () =>
7974 Effect.gen(function* () {
8075 const terminals = yield* makeAcpClientTerminals({
81 spawner: yield* ChildProcessSpawner.ChildProcessSpawner,
8276 defaultCwd: process.cwd(),
8377 shellCommands: true,
8478 environmentForSession: () => ({ T3_ACP_MCP_NODE: "mailbox-probe" }),
packages/provider-acp/src/server/clientTerminals.ts+229-240
......@@ -6,7 +6,8 @@ import * as Fiber from "effect/Fiber";
66import * as Scope from "effect/Scope";
77import * as Semaphore from "effect/Semaphore";
88import * as Stream from "effect/Stream";
9import { ChildProcess, ChildProcessSpawner } from "effect/process";
9import * as ChildProcess from "effect/process/ChildProcess";
10import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1011import * as EffectAcpErrors from "effect-acp/errors";
1112import type * as EffectAcpSchema from "effect-acp/compat";
1213import * as NodeBuffer from "node:buffer";
......@@ -137,7 +138,6 @@ export interface AcpClientTerminals {
137138export interface AcpClientTerminalsOptions {
138139 /** Devin sends shell source in command, rather than an executable plus args. */
139140 readonly shellCommands?: boolean | undefined;
140 readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
141141 readonly defaultCwd: string;
142142 readonly environment?: NodeJS.ProcessEnv | undefined;
143143 readonly environmentForSession?:
......@@ -161,272 +161,261 @@ function acpTerminalCommand(input: {
161161 });
162162}
163163
164export const makeAcpClientTerminals = (
164export const makeAcpClientTerminals = Effect.fn("makeAcpClientTerminals")(function* (
165165 options: AcpClientTerminalsOptions,
166): Effect.Effect<AcpClientTerminals> =>
167 Effect.gen(function* () {
168 const creationPermit = yield* Semaphore.make(1);
169 const terminals = new Map<string, ManagedAcpTerminal>();
170 let nextTerminalNumber = 1;
166): Effect.fn.Return<AcpClientTerminals, never, ChildProcessSpawner.ChildProcessSpawner> {
167 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
168 const creationPermit = yield* Semaphore.make(1);
169 const terminals = new Map<string, ManagedAcpTerminal>();
170 let nextTerminalNumber = 1;
171171
172 const enforceTotalOutputLimit = (): void => {
173 let excess =
174 Array.from(terminals.values()).reduce(
175 (total, terminal) => total + terminal.buffer.bytes,
176 0,
177 ) - MAX_RETAINED_OUTPUT_BYTES;
172 const enforceTotalOutputLimit = (): void => {
173 let excess =
174 Array.from(terminals.values()).reduce((total, terminal) => total + terminal.buffer.bytes, 0) -
175 MAX_RETAINED_OUTPUT_BYTES;
176 if (excess <= 0) return;
177 for (const terminal of terminals.values()) {
178 const before = terminal.buffer.bytes;
179 trimOutputStart(terminal.buffer, excess);
180 excess -= before - terminal.buffer.bytes;
178181 if (excess <= 0) return;
179 for (const terminal of terminals.values()) {
180 const before = terminal.buffer.bytes;
181 trimOutputStart(terminal.buffer, excess);
182 excess -= before - terminal.buffer.bytes;
183 if (excess <= 0) return;
184 }
185 };
182 }
183 };
186184
187 const pruneReleasedSnapshots = (): void => {
188 const released = Array.from(terminals.values()).filter((terminal) => terminal.released);
189 let retainedBytes = released.reduce((total, terminal) => total + terminal.buffer.bytes, 0);
190 while (
191 released.length > MAX_RETAINED_TERMINALS ||
192 retainedBytes > MAX_RETAINED_OUTPUT_BYTES
193 ) {
194 const terminal = released.shift();
195 if (terminal === undefined) break;
196 retainedBytes -= terminal.buffer.bytes;
197 terminals.delete(terminal.terminalId);
198 }
199 };
185 const pruneReleasedSnapshots = (): void => {
186 const released = Array.from(terminals.values()).filter((terminal) => terminal.released);
187 let retainedBytes = released.reduce((total, terminal) => total + terminal.buffer.bytes, 0);
188 while (released.length > MAX_RETAINED_TERMINALS || retainedBytes > MAX_RETAINED_OUTPUT_BYTES) {
189 const terminal = released.shift();
190 if (terminal === undefined) break;
191 retainedBytes -= terminal.buffer.bytes;
192 terminals.delete(terminal.terminalId);
193 }
194 };
200195
201 const requireTerminal = (
202 sessionId: string,
203 terminalId: string,
204 operation: string,
205 ): Effect.Effect<ManagedAcpTerminal, EffectAcpErrors.AcpRequestError> => {
206 const terminal = terminals.get(terminalId);
207 if (terminal === undefined || terminal.sessionId !== sessionId || terminal.released) {
208 return Effect.fail(
209 terminalRequestError(`ACP ${operation} received an unknown terminal ID '${terminalId}'.`),
210 );
211 }
212 return Effect.succeed(terminal);
213 };
196 const requireTerminal = (
197 sessionId: string,
198 terminalId: string,
199 operation: string,
200 ): Effect.Effect<ManagedAcpTerminal, EffectAcpErrors.AcpRequestError> => {
201 const terminal = terminals.get(terminalId);
202 if (terminal === undefined || terminal.sessionId !== sessionId || terminal.released) {
203 return Effect.fail(
204 terminalRequestError(`ACP ${operation} received an unknown terminal ID '${terminalId}'.`),
205 );
206 }
207 return Effect.succeed(terminal);
208 };
214209
215 const create: AcpClientTerminals["create"] = (request) =>
216 creationPermit.withPermit(
217 Effect.uninterruptibleMask((restore) =>
218 Effect.gen(function* () {
219 const liveCount = Array.from(terminals.values()).filter(
220 (terminal) => !terminal.released && terminal.exitStatus === undefined,
221 ).length;
222 const unreleasedCount = Array.from(terminals.values()).filter(
223 (terminal) => !terminal.released,
224 ).length;
225 if (unreleasedCount >= MAX_UNRELEASED_TERMINALS) {
226 return yield* terminalRequestError(
227 `ACP terminal/create exceeded the limit of ${MAX_UNRELEASED_TERMINALS} unreleased terminals.`,
228 );
229 }
230 if (liveCount >= MAX_LIVE_TERMINALS) {
231 return yield* terminalRequestError(
232 `ACP terminal/create exceeded the limit of ${MAX_LIVE_TERMINALS} concurrent terminals.`,
233 );
234 }
235 const requestEnvironment = Object.fromEntries(
236 (request.env ?? []).map((variable) => [variable.name, variable.value] as const),
210 const create: AcpClientTerminals["create"] = (request) =>
211 creationPermit.withPermit(
212 Effect.uninterruptibleMask((restore) =>
213 Effect.gen(function* () {
214 const liveCount = Array.from(terminals.values()).filter(
215 (terminal) => !terminal.released && terminal.exitStatus === undefined,
216 ).length;
217 const unreleasedCount = Array.from(terminals.values()).filter(
218 (terminal) => !terminal.released,
219 ).length;
220 if (unreleasedCount >= MAX_UNRELEASED_TERMINALS) {
221 return yield* terminalRequestError(
222 `ACP terminal/create exceeded the limit of ${MAX_UNRELEASED_TERMINALS} unreleased terminals.`,
223 );
224 }
225 if (liveCount >= MAX_LIVE_TERMINALS) {
226 return yield* terminalRequestError(
227 `ACP terminal/create exceeded the limit of ${MAX_LIVE_TERMINALS} concurrent terminals.`,
237228 );
238 const sessionEnvironment = options.environmentForSession?.(request.sessionId);
239 const environment =
240 options.environment === undefined &&
241 sessionEnvironment === undefined &&
242 (request.env?.length ?? 0) === 0
243 ? undefined
244 : { ...options.environment, ...sessionEnvironment, ...requestEnvironment };
245 // Each terminal owns a scope so the spawned process is reliably reaped:
246 // closing the scope kills a still-running command and frees the handle.
247 const terminalScope = yield* Scope.make();
248 const child = yield* restore(
249 options.spawner
250 .spawn(
251 acpTerminalCommand({
252 request,
253 shellCommands: options.shellCommands,
254 defaultCwd: options.defaultCwd,
255 environment,
256 forceKillAfter: options.forceKillAfter,
257 }),
258 )
259 .pipe(
260 Effect.provideService(Scope.Scope, terminalScope),
261 Effect.mapError((cause) =>
262 terminalRequestError(
263 `Could not start terminal command '${request.command}'.`,
264 cause,
265 ),
229 }
230 const requestEnvironment = Object.fromEntries(
231 (request.env ?? []).map((variable) => [variable.name, variable.value] as const),
232 );
233 const sessionEnvironment = options.environmentForSession?.(request.sessionId);
234 const environment =
235 options.environment === undefined &&
236 sessionEnvironment === undefined &&
237 (request.env?.length ?? 0) === 0
238 ? undefined
239 : { ...options.environment, ...sessionEnvironment, ...requestEnvironment };
240 // Each terminal owns a scope so the spawned process is reliably reaped:
241 // closing the scope kills a still-running command and frees the handle.
242 const terminalScope = yield* Scope.make();
243 const child = yield* restore(
244 spawner
245 .spawn(
246 acpTerminalCommand({
247 request,
248 shellCommands: options.shellCommands,
249 defaultCwd: options.defaultCwd,
250 environment,
251 forceKillAfter: options.forceKillAfter,
252 }),
253 )
254 .pipe(
255 Effect.provideService(Scope.Scope, terminalScope),
256 Effect.mapError((cause) =>
257 terminalRequestError(
258 `Could not start terminal command '${request.command}'.`,
259 cause,
266260 ),
267261 ),
268 ).pipe(
269 Effect.onExit((exit) =>
270 Exit.isFailure(exit) ? Scope.close(terminalScope, Exit.void) : Effect.void,
271262 ),
272 );
263 ).pipe(
264 Effect.onExit((exit) =>
265 Exit.isFailure(exit) ? Scope.close(terminalScope, Exit.void) : Effect.void,
266 ),
267 );
273268
274 const terminalId = `t3-term-${nextTerminalNumber}`;
275 nextTerminalNumber += 1;
276 const record: ManagedAcpTerminal = {
277 terminalId,
278 sessionId: request.sessionId,
279 commandLine: [request.command, ...(request.args ?? [])].join(" "),
280 buffer: {
281 chunks: [],
282 bytes: 0,
283 truncated: false,
284 limit: Math.min(
285 request.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT,
286 MAX_OUTPUT_BYTE_LIMIT,
287 ),
288 },
289 exit: yield* Deferred.make<EffectAcpSchema.WaitForTerminalExitResponse>(),
290 kill: child.kill().pipe(Effect.ignore),
291 dispose: Scope.close(terminalScope, Exit.void).pipe(Effect.ignore),
292 exitStatus: undefined,
293 released: false,
294 };
295 terminals.set(terminalId, record);
269 const terminalId = `t3-term-${nextTerminalNumber}`;
270 nextTerminalNumber += 1;
271 const record: ManagedAcpTerminal = {
272 terminalId,
273 sessionId: request.sessionId,
274 commandLine: [request.command, ...(request.args ?? [])].join(" "),
275 buffer: {
276 chunks: [],
277 bytes: 0,
278 truncated: false,
279 limit: Math.min(
280 request.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT,
281 MAX_OUTPUT_BYTE_LIMIT,
282 ),
283 },
284 exit: yield* Deferred.make<EffectAcpSchema.WaitForTerminalExitResponse>(),
285 kill: child.kill().pipe(Effect.ignore),
286 dispose: Scope.close(terminalScope, Exit.void).pipe(Effect.ignore),
287 exitStatus: undefined,
288 released: false,
289 };
290 terminals.set(terminalId, record);
296291
297 const pump = (stream: typeof child.stdout) =>
298 stream.pipe(
299 Stream.runForEach((chunk) =>
300 Effect.sync(() => {
301 appendOutput(record.buffer, chunk);
302 enforceTotalOutputLimit();
303 }),
304 ),
305 Effect.ignore,
306 );
307 const stdoutPump = yield* Effect.forkDetach(pump(child.stdout));
308 const stderrPump = yield* Effect.forkDetach(pump(child.stderr));
309 yield* Effect.forkDetach(
310 child.exitCode.pipe(
311 Effect.match({
312 onFailure: (error) => ({ exitCode: null, signal: terminalExitSignal(error) }),
313 onSuccess: (code) => ({ exitCode: Number(code), signal: null }),
292 const pump = (stream: typeof child.stdout) =>
293 stream.pipe(
294 Stream.runForEach((chunk) =>
295 Effect.sync(() => {
296 appendOutput(record.buffer, chunk);
297 enforceTotalOutputLimit();
314298 }),
315 Effect.flatMap((status) =>
316 Effect.all([Fiber.join(stdoutPump), Fiber.join(stderrPump)], {
317 discard: true,
318 }).pipe(
319 Effect.andThen(
320 Effect.sync(() => {
321 record.exitStatus = status;
322 }),
323 ),
324 Effect.andThen(Deferred.succeed(record.exit, status)),
299 ),
300 Effect.ignore,
301 );
302 const stdoutPump = yield* Effect.forkDetach(pump(child.stdout));
303 const stderrPump = yield* Effect.forkDetach(pump(child.stderr));
304 yield* Effect.forkDetach(
305 child.exitCode.pipe(
306 Effect.match({
307 onFailure: (error) => ({ exitCode: null, signal: terminalExitSignal(error) }),
308 onSuccess: (code) => ({ exitCode: Number(code), signal: null }),
309 }),
310 Effect.flatMap((status) =>
311 Effect.all([Fiber.join(stdoutPump), Fiber.join(stderrPump)], {
312 discard: true,
313 }).pipe(
314 Effect.andThen(
315 Effect.sync(() => {
316 record.exitStatus = status;
317 }),
325318 ),
319 Effect.andThen(Deferred.succeed(record.exit, status)),
326320 ),
327321 ),
328 );
329 return { terminalId };
330 }),
331 ),
332 );
333
334 const output: AcpClientTerminals["output"] = (request) =>
335 requireTerminal(request.sessionId, request.terminalId, "terminal/output").pipe(
336 Effect.map((terminal) => ({
337 output: readOutput(terminal.buffer),
338 truncated: terminal.buffer.truncated,
339 ...(terminal.exitStatus === undefined ? {} : { exitStatus: terminal.exitStatus }),
340 })),
341 );
342
343 const waitForExit: AcpClientTerminals["waitForExit"] = (request) =>
344 requireTerminal(request.sessionId, request.terminalId, "terminal/wait_for_exit").pipe(
345 Effect.flatMap((terminal) => Deferred.await(terminal.exit)),
346 );
347
348 const kill: AcpClientTerminals["kill"] = (request) =>
349 requireTerminal(request.sessionId, request.terminalId, "terminal/kill").pipe(
350 Effect.flatMap((terminal) => terminal.kill),
351 Effect.as({}),
352 );
353
354 const release: AcpClientTerminals["release"] = (request) =>
355 Effect.uninterruptible(
356 requireTerminal(request.sessionId, request.terminalId, "terminal/release").pipe(
357 Effect.flatMap((terminal) =>
358 Effect.sync(() => {
359 terminal.released = true;
360 }).pipe(
361 Effect.andThen(terminal.dispose),
362 Effect.andThen(Deferred.await(terminal.exit)),
363 Effect.andThen(Effect.sync(pruneReleasedSnapshots)),
364322 ),
365 ),
366 Effect.as({}),
367 ),
368 );
323 );
324 return { terminalId };
325 }),
326 ),
327 );
369328
370 const readOutputSnapshot: AcpClientTerminals["readOutputSnapshot"] = (
371 sessionId,
372 terminalId,
373 ) => {
374 const terminal = terminals.get(terminalId);
375 if (terminal === undefined || terminal.sessionId !== sessionId) return undefined;
376 return {
329 const output: AcpClientTerminals["output"] = (request) =>
330 requireTerminal(request.sessionId, request.terminalId, "terminal/output").pipe(
331 Effect.map((terminal) => ({
377332 output: readOutput(terminal.buffer),
378333 truncated: terminal.buffer.truncated,
379 exitStatus: terminal.exitStatus,
380 };
381 };
334 ...(terminal.exitStatus === undefined ? {} : { exitStatus: terminal.exitStatus }),
335 })),
336 );
382337
383 const readCommandLine: AcpClientTerminals["readCommandLine"] = (terminalId) =>
384 terminals.get(terminalId)?.commandLine;
338 const waitForExit: AcpClientTerminals["waitForExit"] = (request) =>
339 requireTerminal(request.sessionId, request.terminalId, "terminal/wait_for_exit").pipe(
340 Effect.flatMap((terminal) => Deferred.await(terminal.exit)),
341 );
385342
386 const disposeAll: AcpClientTerminals["disposeAll"] = Effect.uninterruptible(
387 Effect.suspend(() =>
388 Effect.forEach(
389 Array.from(terminals.values()),
390 (terminal) =>
391 Effect.sync(() => {
392 terminal.released = true;
393 }).pipe(
394 Effect.andThen(terminal.dispose),
395 Effect.andThen(Deferred.await(terminal.exit)),
396 ),
397 { concurrency: "unbounded", discard: true },
398 ).pipe(Effect.andThen(Effect.sync(pruneReleasedSnapshots))),
399 ),
343 const kill: AcpClientTerminals["kill"] = (request) =>
344 requireTerminal(request.sessionId, request.terminalId, "terminal/kill").pipe(
345 Effect.flatMap((terminal) => terminal.kill),
346 Effect.as({}),
400347 );
401348
402 // Terminal failures surface to the agent as JSON-RPC errors and to the
403 // user only as a failed tool item, so log them for server-side diagnosis.
404 const logged =
405 <Request, Response>(
406 operation: string,
407 handler: (request: Request) => Effect.Effect<Response, EffectAcpErrors.AcpRequestError>,
408 ) =>
409 (request: Request) =>
410 handler(request).pipe(
411 Effect.tapError((error) =>
412 Effect.logWarning("ACP client terminal operation failed", {
413 operation,
414 detail: error.message,
415 }),
349 const release: AcpClientTerminals["release"] = (request) =>
350 Effect.uninterruptible(
351 requireTerminal(request.sessionId, request.terminalId, "terminal/release").pipe(
352 Effect.flatMap((terminal) =>
353 Effect.sync(() => {
354 terminal.released = true;
355 }).pipe(
356 Effect.andThen(terminal.dispose),
357 Effect.andThen(Deferred.await(terminal.exit)),
358 Effect.andThen(Effect.sync(pruneReleasedSnapshots)),
416359 ),
417 );
360 ),
361 Effect.as({}),
362 ),
363 );
418364
365 const readOutputSnapshot: AcpClientTerminals["readOutputSnapshot"] = (sessionId, terminalId) => {
366 const terminal = terminals.get(terminalId);
367 if (terminal === undefined || terminal.sessionId !== sessionId) return undefined;
419368 return {
420 create: logged("terminal/create", create),
421 output: logged("terminal/output", output),
422 waitForExit: logged("terminal/wait_for_exit", waitForExit),
423 kill: logged("terminal/kill", kill),
424 release: logged("terminal/release", release),
425 readOutputSnapshot,
426 readCommandLine,
427 disposeAll,
369 output: readOutput(terminal.buffer),
370 truncated: terminal.buffer.truncated,
371 exitStatus: terminal.exitStatus,
428372 };
429 });
373 };
374
375 const readCommandLine: AcpClientTerminals["readCommandLine"] = (terminalId) =>
376 terminals.get(terminalId)?.commandLine;
377
378 const disposeAll: AcpClientTerminals["disposeAll"] = Effect.uninterruptible(
379 Effect.suspend(() =>
380 Effect.forEach(
381 Array.from(terminals.values()),
382 (terminal) =>
383 Effect.sync(() => {
384 terminal.released = true;
385 }).pipe(Effect.andThen(terminal.dispose), Effect.andThen(Deferred.await(terminal.exit))),
386 { concurrency: "unbounded", discard: true },
387 ).pipe(Effect.andThen(Effect.sync(pruneReleasedSnapshots))),
388 ),
389 );
390
391 // Terminal failures surface to the agent as JSON-RPC errors and to the
392 // user only as a failed tool item, so log them for server-side diagnosis.
393 const logged =
394 <Request, Response>(
395 operation: string,
396 handler: (request: Request) => Effect.Effect<Response, EffectAcpErrors.AcpRequestError>,
397 ) =>
398 (request: Request) =>
399 handler(request).pipe(
400 Effect.tapError((error) =>
401 Effect.logWarning("ACP client terminal operation failed", {
402 operation,
403 detail: error.message,
404 }),
405 ),
406 );
407
408 return {
409 create: logged("terminal/create", create),
410 output: logged("terminal/output", output),
411 waitForExit: logged("terminal/wait_for_exit", waitForExit),
412 kill: logged("terminal/kill", kill),
413 release: logged("terminal/release", release),
414 readOutputSnapshot,
415 readCommandLine,
416 disposeAll,
417 };
418});
430419
431420/**
432421 * Rewrites embedded `terminal` tool-call content into plain text content so
packages/provider-grok/src/server/acpCliProbe.test.ts+5-12
......@@ -15,20 +15,15 @@ import * as Deferred from "effect/Deferred";
1515import * as Fiber from "effect/Fiber";
1616import * as FileSystem from "effect/FileSystem";
1717import * as Stream from "effect/Stream";
18import { ChildProcessSpawner } from "effect/process";
1918import { describe, expect } from "vite-plus/test";
2019
2120import { makeGrokAcpRuntime } from "./acpSupport.ts";
2221
23const makeProbeRuntime = Effect.gen(function* () {
24 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
25 return yield* makeGrokAcpRuntime({
26 grokSettings: { binaryPath: "grok" },
27 environment: process.env,
28 childProcessSpawner,
29 cwd: process.cwd(),
30 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
31 });
22const makeProbeRuntime = makeGrokAcpRuntime({
23 grokSettings: { binaryPath: "grok" },
24 environment: process.env,
25 cwd: process.cwd(),
26 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
3227});
3328
3429describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () => {
......@@ -78,11 +73,9 @@ describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () =
7873 Effect.gen(function* () {
7974 const fileSystem = yield* FileSystem.FileSystem;
8075 const cwd = yield* fileSystem.makeTempDirectoryScoped();
81 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
8276 const runtime = yield* makeGrokAcpRuntime({
8377 grokSettings: { binaryPath: "grok" },
8478 environment: process.env,
85 childProcessSpawner,
8679 cwd,
8780 runtimeMode: "approval-required",
8881 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
packages/provider-grok/src/server/acpSupport.ts+2-7
......@@ -31,7 +31,6 @@ interface GrokAcpRuntimeInput extends Omit<
3131 AcpSessionRuntime.AcpSessionRuntimeOptions,
3232 "authMethodId" | "clientCapabilities" | "spawn"
3333> {
34 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
3534 readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;
3635 readonly environment?: NodeJS.ProcessEnv;
3736 readonly runtimeMode?: RuntimeMode;
......@@ -157,7 +156,7 @@ export const makeGrokAcpRuntime = (
157156): Effect.Effect<
158157 AcpSessionRuntime.AcpSessionRuntime["Service"],
159158 EffectAcpErrors.AcpError,
160 Crypto.Crypto | Scope.Scope
159 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
161160> =>
162161 Effect.gen(function* () {
163162 const processGroupPlatform = yield* HostProcessPlatform.pipe(
......@@ -176,11 +175,7 @@ export const makeGrokAcpRuntime = (
176175 cancelMeta: { ...input.cancelMeta, ...GROK_ACP_CANCEL_META },
177176 initializeMeta: GROK_ACP_INITIALIZE_META,
178177 ...grokAcpRuntimeProcessOwnership(processGroupPlatform),
179 }).pipe(
180 Layer.provide(
181 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner),
182 ),
183 ),
178 }),
184179 );
185180 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
186181 Effect.provide(acpContext),
packages/provider-grok/src/server/adapter.ts+6-12
......@@ -20,7 +20,7 @@ import * as Path from "effect/Path";
2020import * as Layer from "effect/Layer";
2121import * as Schema from "effect/Schema";
2222import type * as Scope from "effect/Scope";
23import { ChildProcessSpawner } from "effect/process";
23import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2424import * as EffectAcpErrors from "effect-acp/errors";
2525
2626import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
......@@ -78,6 +78,7 @@ export const GROK_PROVIDER = ProviderDriverKind.make("grok");
7878const GROK_DRIVER_KIND = GROK_PROVIDER;
7979export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);
8080const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
81const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
8182
8283export const GrokProviderCapabilitiesV2 = {
8384 ...AcpProviderCapabilitiesV2,
......@@ -122,7 +123,7 @@ export interface GrokAdapterV2Options {
122123 ) => Effect.Effect<
123124 AcpSessionRuntime.AcpSessionRuntime["Service"],
124125 EffectAcpErrors.AcpError,
125 Crypto.Crypto | Scope.Scope
126 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
126127 >;
127128 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
128129}
......@@ -226,11 +227,7 @@ export function grokLaunchRuntimeMode(
226227}
227228
228229/** The flavor runs Grok's launcher through the adapter's spawner. */
229export function makeGrokAcpAdapterFlavor(
230 options: GrokAdapterV2Options & {
231 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
232 },
233): AcpAdapterV2Flavor {
230export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor {
234231 return {
235232 driver: GROK_PROVIDER,
236233 runtimeHarness: "Grok",
......@@ -281,7 +278,6 @@ export function makeGrokAcpAdapterFlavor(
281278 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
282279 grokSettings: options.settings,
283280 environment: options.environment,
284 childProcessSpawner: options.childProcessSpawner,
285281 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
286282 })),
287283 // In its Auto mode Grok decides routine actions itself and only asks about
......@@ -292,7 +288,7 @@ export function makeGrokAcpAdapterFlavor(
292288 promptFailure: (cause) =>
293289 makeProviderFailure({
294290 cause,
295 ...(Schema.is(EffectAcpErrors.AcpRequestError)(cause)
291 ...(isAcpRequestError(cause)
296292 ? {
297293 // Grok's own failure text rides on the cause; makeProviderFailure
298294 // redacts and bounds it before it reaches the user.
......@@ -333,11 +329,9 @@ export function makeGrokAcpAdapterFlavor(
333329export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
334330 options: GrokAdapterV2Options,
335331) {
336 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
337 const flavor = makeGrokAcpAdapterFlavor({ ...options, childProcessSpawner });
338332 return yield* makeAcpAdapterV2({
339333 instanceId: options.instanceId,
340 flavor,
334 flavor: makeGrokAcpAdapterFlavor(options),
341335 selfInvocation: options.selfInvocation,
342336 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
343337 ...(options.continuationRequests === undefined
packages/provider-grok/src/server/driver.test.ts+1-1
......@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Layer from "effect/Layer";
88import * as Path from "effect/Path";
9import { HttpClient } from "effect/http";
9import * as HttpClient from "effect/http/HttpClient";
1010import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1111
1212import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
packages/provider-grok/src/server/driver.ts+3-3
......@@ -5,8 +5,8 @@ import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
77import * as Schema from "effect/Schema";
8import { HttpClient } from "effect/http";
9import { ChildProcessSpawner } from "effect/process";
8import * as HttpClient from "effect/http/HttpClient";
9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1010
1111import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
1212import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
......@@ -191,7 +191,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
191191 new ProviderDriverError({
192192 driver: DRIVER_KIND,
193193 instanceId,
194 detail: `Failed to build Grok snapshot: ${cause.message ?? String(cause)}`,
194 detail: "Failed to build Grok snapshot.",
195195 cause,
196196 }),
197197 ),
packages/provider-grok/src/server/skills.test.ts+1-1
......@@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest";
22import * as Effect from "effect/Effect";
33import * as Sink from "effect/Sink";
44import * as Stream from "effect/Stream";
5import { ChildProcessSpawner } from "effect/process";
5import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
66
77import { discoverGrokSkills } from "./skills.ts";
88
packages/provider-grok/src/server/skills.ts+1-1
......@@ -20,7 +20,7 @@ import type { GrokSettings } from "../settings.ts";
2020import * as Effect from "effect/Effect";
2121import * as Option from "effect/Option";
2222import * as Schema from "effect/Schema";
23import { ChildProcess } from "effect/process";
23import * as ChildProcess from "effect/process/ChildProcess";
2424import { resolveSpawnCommand } from "@t3tools/shared/shell";
2525
2626import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe";
packages/provider-grok/src/server/status.ts+3-4
......@@ -16,8 +16,9 @@ import * as Exit from "effect/Exit";
1616import * as Option from "effect/Option";
1717import * as Result from "effect/Result";
1818import * as Schema from "effect/Schema";
19import { HttpClient } from "effect/http";
20import { ChildProcess, ChildProcessSpawner } from "effect/process";
19import * as HttpClient from "effect/http/HttpClient";
20import * as ChildProcess from "effect/process/ChildProcess";
21import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2122import { createModelCapabilities } from "@t3tools/shared/model";
2223import { resolveSpawnCommand } from "@t3tools/shared/shell";
2324
......@@ -356,11 +357,9 @@ const discoverGrokMetadataViaAcpInitialize = (
356357 environment: NodeJS.ProcessEnv,
357358) =>
358359 Effect.gen(function* () {
359 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
360360 const acp = yield* makeGrokAcpRuntime({
361361 grokSettings,
362362 environment,
363 childProcessSpawner,
364363 cwd: process.cwd(),
365364 clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
366365 });
packages/provider-grok/src/server/textGeneration.ts+5-3
......@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
33import * as Option from "effect/Option";
44import * as Ref from "effect/Ref";
55import * as Schema from "effect/Schema";
6import { ChildProcessSpawner } from "effect/process";
6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
77import type * as EffectAcpErrors from "effect-acp/errors";
88
99import { TextGenerationError } from "@t3tools/contracts";
......@@ -37,10 +37,12 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi
3737 const runtime = yield* makeGrokAcpRuntime({
3838 grokSettings,
3939 environment,
40 childProcessSpawner: commandSpawner,
4140 cwd,
4241 clientInfo: { name: "t3-code-git-text", version: "0.0.0" },
43 }).pipe(Effect.provideService(Crypto.Crypto, crypto));
42 }).pipe(
43 Effect.provideService(Crypto.Crypto, crypto),
44 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, commandSpawner),
45 );
4446
4547 yield* runtime.handleSessionUpdate((notification) => {
4648 const update = notification.update;
packages/provider-grok/src/server/usageLimits.ts+3-1
......@@ -6,7 +6,9 @@ import * as FileSystem from "effect/FileSystem";
66import * as Option from "effect/Option";
77import * as Path from "effect/Path";
88import * as Schema from "effect/Schema";
9import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import * as HttpClientResponse from "effect/http/HttpClientResponse";
1012import {
1113 clampPercent,
1214 makeUnavailableUsageLimits,