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";...@@ -21,7 +21,6 @@ import * as FileSystem from "effect/FileSystem";
21import * as Layer from "effect/Layer";21import * as Layer from "effect/Layer";
22import * as Path from "effect/Path";22import * as Path from "effect/Path";
23import * as Schema from "effect/Schema";23import * as Schema from "effect/Schema";
24import { ChildProcessSpawner } from "effect/process";
25import type * as EffectAcpProtocol from "effect-acp/protocol";24import type * as EffectAcpProtocol from "effect-acp/protocol";
2625
27import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";26import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
...@@ -443,7 +442,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s...@@ -443,7 +442,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
443 const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" };442 const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" };
444 const layerRegistry = ProviderAdapterRegistry.layerFromAdaptersEffect(443 const layerRegistry = ProviderAdapterRegistry.layerFromAdaptersEffect(
445 Effect.gen(function* () {444 Effect.gen(function* () {
446 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
447 const environment = yield* HostProcessEnvironment;445 const environment = yield* HostProcessEnvironment;
448 const adapter = yield* makeGrokAdapterV2({446 const adapter = yield* makeGrokAdapterV2({
449 instanceId: GROK_DEFAULT_INSTANCE_ID,447 instanceId: GROK_DEFAULT_INSTANCE_ID,
...@@ -460,7 +458,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s...@@ -460,7 +458,6 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
460 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,458 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
461 grokSettings: settings,459 grokSettings: settings,
462 environment,460 environment,
463 childProcessSpawner,
464 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),461 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
465 }),462 }),
466 });463 });
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+23-15
...@@ -48,7 +48,7 @@ import * as Sink from "effect/Sink";...@@ -48,7 +48,7 @@ import * as Sink from "effect/Sink";
48import * as Stream from "effect/Stream";48import * as Stream from "effect/Stream";
49import * as Scope from "effect/Scope";49import * as Scope from "effect/Scope";
50import * as TestClock from "effect/testing/TestClock";50import * as TestClock from "effect/testing/TestClock";
51import { ChildProcessSpawner } from "effect/process";51import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
52import * as EffectAcpErrors from "effect-acp/errors";52import * as EffectAcpErrors from "effect-acp/errors";
53import type * as EffectAcpProtocol from "effect-acp/protocol";53import type * as EffectAcpProtocol from "effect-acp/protocol";
54import type * as EffectAcpSchema from "effect-acp/compat";54import type * as EffectAcpSchema from "effect-acp/compat";
...@@ -1150,7 +1150,7 @@ describe("AcpAdapterV2", () => {...@@ -1150,7 +1150,7 @@ describe("AcpAdapterV2", () => {
1150 const adapter = yield* makeAcpAdapterV2({1150 const adapter = yield* makeAcpAdapterV2({
1151 instanceId,1151 instanceId,
1152 // Production Devin runs commands through client terminals.1152 // Production Devin runs commands through client terminals.
1153 clientTerminals: { childProcessSpawner, shellCommands: true },1153 clientTerminals: { shellCommands: true },
1154 selfInvocation: yield* resolveSelfInvocation(),1154 selfInvocation: yield* resolveSelfInvocation(),
1155 flavor: {1155 flavor: {
1156 driver: ACP_TEST_DRIVER,1156 driver: ACP_TEST_DRIVER,
...@@ -2190,7 +2190,7 @@ describe("AcpAdapterV2", () => {...@@ -2190,7 +2190,7 @@ describe("AcpAdapterV2", () => {
2190 makeRuntime,2190 makeRuntime,
2191 },2191 },
2192 selfInvocation,2192 selfInvocation,
2193 clientTerminals: { childProcessSpawner },2193 clientTerminals: {},
2194 });2194 });
2195 const sourceThreadId = ThreadId.make("thread-acp-native-fork-source");2195 const sourceThreadId = ThreadId.make("thread-acp-native-fork-source");
2196 const targetThreadId = ThreadId.make("thread-acp-native-fork-target");2196 const targetThreadId = ThreadId.make("thread-acp-native-fork-target");
...@@ -2476,7 +2476,7 @@ describe("AcpAdapterV2", () => {...@@ -2476,7 +2476,7 @@ describe("AcpAdapterV2", () => {
2476 }).pipe(Effect.andThen(makeRuntime(input))),2476 }).pipe(Effect.andThen(makeRuntime(input))),
2477 },2477 },
2478 selfInvocation,2478 selfInvocation,
2479 clientTerminals: { childProcessSpawner },2479 clientTerminals: {},
2480 });2480 });
2481 const threadId = ThreadId.make("thread-acp-unknown-permission-grant");2481 const threadId = ThreadId.make("thread-acp-unknown-permission-grant");
2482 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({2482 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
...@@ -12045,9 +12045,11 @@ describe("AcpAdapterV2", () => {...@@ -12045,9 +12045,11 @@ describe("AcpAdapterV2", () => {
12045 Effect.gen(function* () {12045 Effect.gen(function* () {
12046 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =12046 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
12047 [];12047 [];
12048 yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(12048 yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12049 makeTaskkillSpawner({ exitCode: 0, commands }),12049 Effect.provideService(
12050 4321,12050 ChildProcessSpawner.ChildProcessSpawner,
12051 makeTaskkillSpawner({ exitCode: 0, commands }),
12052 ),
12051 );12053 );
12052 assert.deepEqual(commands, [{ command: "taskkill", args: ["/PID", "4321", "/T", "/F"] }]);12054 assert.deepEqual(commands, [{ command: "taskkill", args: ["/PID", "4321", "/T", "/F"] }]);
1205312055
...@@ -12058,10 +12060,13 @@ describe("AcpAdapterV2", () => {...@@ -12058,10 +12060,13 @@ describe("AcpAdapterV2", () => {
12058 { exitCode: 128, output: "ERROR: Access is denied." },12060 { exitCode: 128, output: "ERROR: Access is denied." },
12059 { exitCode: 1, output: "generic failure" },12061 { exitCode: 1, output: "generic failure" },
12060 ]) {12062 ]) {
12061 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(12063 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12062 makeTaskkillSpawner(fixture),12064 Effect.provideService(
12063 4321,12065 ChildProcessSpawner.ChildProcessSpawner,
12064 ).pipe(Effect.exit);12066 makeTaskkillSpawner(fixture),
12067 ),
12068 Effect.exit,
12069 );
12065 if (Exit.isSuccess(failed)) assert.fail(`taskkill ${fixture.exitCode} must fail`);12070 if (Exit.isSuccess(failed)) assert.fail(`taskkill ${fixture.exitCode} must fail`);
12066 const error = Cause.squash(failed.cause);12071 const error = Cause.squash(failed.cause);
12067 assert.instanceOf(error, AcpSessionRuntime.AcpProcessGroupTerminationError);12072 assert.instanceOf(error, AcpSessionRuntime.AcpProcessGroupTerminationError);
...@@ -12082,10 +12087,13 @@ describe("AcpAdapterV2", () => {...@@ -12082,10 +12087,13 @@ describe("AcpAdapterV2", () => {
12082 { outputFailure: true },12087 { outputFailure: true },
12083 { exitFailure: true },12088 { exitFailure: true },
12084 ]) {12089 ]) {
12085 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(12090 const failed = yield* AcpSessionRuntime.terminateWindowsProcessTreeWithTaskkill(4321).pipe(
12086 makeTaskkillSpawner(fixture),12091 Effect.provideService(
12087 4321,12092 ChildProcessSpawner.ChildProcessSpawner,
12088 ).pipe(Effect.exit);12093 makeTaskkillSpawner(fixture),
12094 ),
12095 Effect.exit,
12096 );
12089 if (Exit.isSuccess(failed)) assert.fail("taskkill infrastructure failure must fail");12097 if (Exit.isSuccess(failed)) assert.fail("taskkill infrastructure failure must fail");
12090 const error = Cause.squash(failed.cause);12098 const error = Cause.squash(failed.cause);
12091 assert.instanceOf(error, AcpSessionRuntime.AcpProcessGroupTerminationError);12099 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";...@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11import type * as Scope from "effect/Scope";11import type * as Scope from "effect/Scope";
12import * as Stream from "effect/Stream";12import * as Stream from "effect/Stream";
13import { ChildProcessSpawner } from "effect/process";13import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
14import * as EffectAcpErrors from "effect-acp/errors";14import * as EffectAcpErrors from "effect-acp/errors";
1515
16import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";16import * 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";...@@ -9,8 +9,9 @@ import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";9import * as Option from "effect/Option";
10import * as Path from "effect/Path";10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
12import { HttpClient, HttpClientResponse } from "effect/http";12import * as HttpClient from "effect/http/HttpClient";
13import { ChildProcessSpawner } from "effect/process";13import * as HttpClientResponse from "effect/http/HttpClientResponse";
14import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
14import * as EffectAcpErrors from "effect-acp/errors";15import * as EffectAcpErrors from "effect-acp/errors";
1516
16import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";17import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
...@@ -22,6 +23,7 @@ import type {...@@ -22,6 +23,7 @@ import type {
22 AcpRegistryLiveConfiguration,23 AcpRegistryLiveConfiguration,
23} from "@t3tools/provider-acp-registry/testing";24} from "@t3tools/provider-acp-registry/testing";
24import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";25import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
26import * as AcpRegistryRuntimeCoordinator from "@t3tools/provider-acp-registry/server/AcpRegistryRuntimeCoordinator";
25import { ACP_SESSION_MODE_OPTION_ID } from "@t3tools/provider-acp/server/sessionConfig";27import { ACP_SESSION_MODE_OPTION_ID } from "@t3tools/provider-acp/server/sessionConfig";
26import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";28import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
27import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";29import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
...@@ -82,6 +84,11 @@ const layerRegistry = Layer.succeed(...@@ -82,6 +84,11 @@ const layerRegistry = Layer.succeed(
82 ),84 ),
83);85);
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
85const layerTest = Layer.mergeAll(92const layerTest = Layer.mergeAll(
86 NodeServices.layer,93 NodeServices.layer,
87 IdAllocator.layer,94 IdAllocator.layer,
...@@ -197,8 +204,6 @@ describe("AcpRegistryAdapterV2", () => {...@@ -197,8 +204,6 @@ describe("AcpRegistryAdapterV2", () => {
197 authMethodId: "test",204 authMethodId: "test",
198 }),205 }),
199 environment: {},206 environment: {},
200 childProcessSpawner,
201 resolver: { resolve: () => Effect.die("the runtime is injected") },
202 makeRuntime: makeAcpReplayRuntime({207 makeRuntime: makeAcpReplayRuntime({
203 transcript,208 transcript,
204 statusPath,209 statusPath,
...@@ -208,7 +213,7 @@ describe("AcpRegistryAdapterV2", () => {...@@ -208,7 +213,7 @@ describe("AcpRegistryAdapterV2", () => {
208 childProcessSpawner,213 childProcessSpawner,
209 fileSystem,214 fileSystem,
210 }),215 }),
211 });216 }).pipe(Effect.provide(layerInjectedRuntimeCatalog));
212 yield* adapter217 yield* adapter
213 .openSession({218 .openSession({
214 threadId: ThreadId.make("thread-acp-registry-mode-pick"),219 threadId: ThreadId.make("thread-acp-registry-mode-pick"),
...@@ -279,8 +284,6 @@ describe("AcpRegistryAdapterV2", () => {...@@ -279,8 +284,6 @@ describe("AcpRegistryAdapterV2", () => {
279 instanceId,284 instanceId,
280 settings: yield* decodeAcpRegistryAdapterSettings({ agentId, authMethodId: "test" }),285 settings: yield* decodeAcpRegistryAdapterSettings({ agentId, authMethodId: "test" }),
281 environment: {},286 environment: {},
282 childProcessSpawner,
283 resolver: { resolve: () => Effect.die("the runtime is injected") },
284 makeRuntime: (input) =>287 makeRuntime: (input) =>
285 Effect.gen(function* () {288 Effect.gen(function* () {
286 clientCapabilities = input.clientCapabilities;289 clientCapabilities = input.clientCapabilities;
...@@ -305,7 +308,7 @@ describe("AcpRegistryAdapterV2", () => {...@@ -305,7 +308,7 @@ describe("AcpRegistryAdapterV2", () => {
305 Effect.provide(context),308 Effect.provide(context),
306 );309 );
307 }),310 }),
308 });311 }).pipe(Effect.provide(layerInjectedRuntimeCatalog));
309 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({312 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
310 runtimeMode: "full-access",313 runtimeMode: "full-access",
311 interactionMode: "default",314 interactionMode: "default",
...@@ -333,7 +336,6 @@ describe("AcpRegistryAdapterV2", () => {...@@ -333,7 +336,6 @@ describe("AcpRegistryAdapterV2", () => {
333336
334 it.effect("opens a real ACP child process resolved from registry configuration", () =>337 it.effect("opens a real ACP child process resolved from registry configuration", () =>
335 Effect.gen(function* () {338 Effect.gen(function* () {
336 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
337 const path = yield* Path.Path;339 const path = yield* Path.Path;
338 const host = yield* ProviderHost.ProviderHost;340 const host = yield* ProviderHost.ProviderHost;
339 const mockAgentPath = yield* path.fromFileUrl(341 const mockAgentPath = yield* path.fromFileUrl(
...@@ -365,55 +367,59 @@ describe("AcpRegistryAdapterV2", () => {...@@ -365,55 +367,59 @@ describe("AcpRegistryAdapterV2", () => {
365 T3_ACP_SESSION_LIFECYCLE: "1",367 T3_ACP_SESSION_LIFECYCLE: "1",
366 T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS: "750",368 T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS: "750",
367 },369 },
368 childProcessSpawner,370 }).pipe(
369 runtimeCoordinator: {371 Effect.provide(
370 withForegroundStartup: (agentId, effect) =>372 Layer.mergeAll(
371 Effect.acquireUseRelease(373 Layer.mock(AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator)({
372 Effect.sync(() => {374 withForegroundStartup: (agentId, effect) =>
373 assert.equal(agentId, "fixture-agent");375 Effect.acquireUseRelease(
374 startupActive = true;376 Effect.sync(() => {
375 startupCount += 1;377 assert.equal(agentId, "fixture-agent");
376 }),378 startupActive = true;
377 () => effect,379 startupCount += 1;
378 () =>380 }),
379 Effect.sync(() => {381 () => effect,
380 startupActive = false;382 () =>
381 }),383 Effect.sync(() => {
382 ),384 startupActive = false;
383 runBackgroundProbe: (_agentId, effect) => effect.pipe(Effect.map(Option.some)),385 }),
384 withSessionMutation: (effect) => effect,386 ),
385 clearAvailableCommands: () => Effect.void,387 runBackgroundProbe: (_agentId, effect) => effect.pipe(Effect.map(Option.some)),
386 publishAvailableCommands: (publishedInstanceId, commands) =>388 withSessionMutation: (effect) => effect,
387 Deferred.succeed(commandsPublished, {389 clearAvailableCommands: () => Effect.void,
388 instanceId: publishedInstanceId,390 publishAvailableCommands: (publishedInstanceId, commands) =>
389 commands,391 Deferred.succeed(commandsPublished, {
390 }).pipe(Effect.asVoid),392 instanceId: publishedInstanceId,
391 getAvailableCommands: () => Effect.succeed(Option.none()),393 commands,
392 watchAvailableCommands: () => Effect.never,394 }).pipe(Effect.asVoid),
393 clearLiveConfiguration: () => Effect.void,395 getAvailableCommands: () => Effect.succeed(Option.none()),
394 publishLiveConfiguration: (_publishedInstanceId, configuration) =>396 watchAvailableCommands: () => Effect.never,
395 Deferred.succeed(configurationPublished, configuration).pipe(Effect.asVoid),397 clearLiveConfiguration: () => Effect.void,
396 getLiveConfiguration: () => Effect.succeed(Option.none()),398 publishLiveConfiguration: (_publishedInstanceId, configuration) =>
397 watchLiveConfiguration: () => Effect.never,399 Deferred.succeed(configurationPublished, configuration).pipe(Effect.asVoid),
398 requestUrlAuthentication: () => Effect.succeed(false),400 getLiveConfiguration: () => Effect.succeed(Option.none()),
399 acceptUrlAuthentication: () => Effect.succeed(false),401 watchLiveConfiguration: () => Effect.never,
400 getUrlAuthAction: () => Effect.succeed(Option.none()),402 requestUrlAuthentication: () => Effect.succeed(false),
401 watchUrlAuthAction: () => Effect.never,403 acceptUrlAuthentication: () => Effect.succeed(false),
402 },404 getUrlAuthAction: () => Effect.succeed(Option.none()),
403 resolver: {405 watchUrlAuthAction: () => Effect.never,
404 resolve: (configuredSettings, cwd, environment) =>406 }),
405 Effect.sync(() => assert.isTrue(startupActive)).pipe(407 Layer.mock(AcpRegistrySupport.AcpRegistryCatalog)({
406 Effect.andThen(resolver.resolve(configuredSettings, cwd, environment)),408 resolve: (configuredSettings, cwd, environment) =>
407 Effect.map((resolved) => ({409 Effect.sync(() => assert.isTrue(startupActive)).pipe(
408 ...resolved,410 Effect.andThen(resolver.resolve(configuredSettings, cwd, environment)),
409 spawn: {411 Effect.map((resolved) => ({
410 ...resolved.spawn,412 ...resolved,
411 args: [mockAgentPath],413 spawn: {
412 },414 ...resolved.spawn,
413 })),415 args: [mockAgentPath],
414 ),416 },
415 },417 })),
416 });418 ),
419 }),
420 ),
421 ),
422 );
417 const threadId = ThreadId.make("thread-acp-registry-fixture");423 const threadId = ThreadId.make("thread-acp-registry-fixture");
418 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({424 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
419 runtimeMode: "full-access",425 runtimeMode: "full-access",
apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts+12-6
...@@ -6,7 +6,7 @@ import * as FileSystem from "effect/FileSystem";...@@ -6,7 +6,7 @@ import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";7import * as Path from "effect/Path";
8import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
9import { ChildProcessSpawner } from "effect/process";9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1010
11import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";11import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
12import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";12import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
...@@ -25,6 +25,7 @@ import {...@@ -25,6 +25,7 @@ import {
25 ACP_REGISTRY_PROVIDER,25 ACP_REGISTRY_PROVIDER,
26 makeAcpRegistryAdapterV2,26 makeAcpRegistryAdapterV2,
27} from "@t3tools/provider-acp-registry/testing";27} from "@t3tools/provider-acp-registry/testing";
28import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
2829
29const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({30const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({
30 agentId: "replay-agent",31 agentId: "replay-agent",
...@@ -56,10 +57,6 @@ function layerAcpRegistryProviderAdapterRegistryReplay(...@@ -56,10 +57,6 @@ function layerAcpRegistryProviderAdapterRegistryReplay(
56 instanceId: ACP_REGISTRY_DEFAULT_INSTANCE_ID,57 instanceId: ACP_REGISTRY_DEFAULT_INSTANCE_ID,
57 settings: REPLAY_SETTINGS,58 settings: REPLAY_SETTINGS,
58 environment: {},59 environment: {},
59 childProcessSpawner,
60 resolver: {
61 resolve: () => Effect.die("ACP registry resolver must not run during replay"),
62 },
63 selfInvocation: yield* resolveSelfInvocation(),60 selfInvocation: yield* resolveSelfInvocation(),
64 makeRuntime: makeAcpReplayRuntime({61 makeRuntime: makeAcpReplayRuntime({
65 transcript,62 transcript,
...@@ -74,7 +71,16 @@ function layerAcpRegistryProviderAdapterRegistryReplay(...@@ -74,7 +71,16 @@ function layerAcpRegistryProviderAdapterRegistryReplay(
74 return [adapter];71 return [adapter];
75 }),72 }),
76 ).pipe(73 ).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 ),
78 // Held inbound lines must not outlive the scenario and wedge teardown.84 // Held inbound lines must not outlive the scenario and wedge teardown.
79 Layer.merge(85 Layer.merge(
80 Layer.effectDiscard(86 Layer.effectDiscard(
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts+1-1
...@@ -17,7 +17,7 @@ import * as Effect from "effect/Effect";...@@ -17,7 +17,7 @@ import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";17import * as Layer from "effect/Layer";
18import * as PlatformError from "effect/PlatformError";18import * as PlatformError from "effect/PlatformError";
19import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
20import { ChildProcessSpawner } from "effect/process";20import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
21import type * as EffectAcpSchema from "effect-acp/compat";21import type * as EffectAcpSchema from "effect-acp/compat";
2222
23import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";23import * 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";...@@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
8import { ChildProcessSpawner } from "effect/process";8import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
9import { HostProcessPlatform } from "@t3tools/shared/hostProcess";9import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
10import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";10import { 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...@@ -2253,13 +2253,14 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2253 options.managedBinaryToolsDir !== undefined2253 options.managedBinaryToolsDir !== undefined
2254 ) {2254 ) {
2255 const managedDirectories = yield* acpRegistryManagedBinaryDirectories({2255 const managedDirectories = yield* acpRegistryManagedBinaryDirectories({
2256 fileSystem,
2257 path,
2258 cacheDir: options.managedBinaryCacheDir,2256 cacheDir: options.managedBinaryCacheDir,
2259 toolsDir: options.managedBinaryToolsDir,2257 toolsDir: options.managedBinaryToolsDir,
2260 platform,2258 platform,
2261 architecture,2259 architecture,
2262 });2260 }).pipe(
2261 Effect.provideService(FileSystem.FileSystem, fileSystem),
2262 Effect.provideService(Path.Path, path),
2263 );
2263 if (managedDirectories.length > 0) {2264 if (managedDirectories.length > 0) {
2264 const delimiter = platform === "win32" ? ";" : ":";2265 const delimiter = platform === "win32" ? ";" : ":";
2265 const pathKey =2266 const pathKey =
packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts+4-6
...@@ -19,9 +19,11 @@ import * as Path from "effect/Path";...@@ -19,9 +19,11 @@ import * as Path from "effect/Path";
19import * as Fiber from "effect/Fiber";19import * as Fiber from "effect/Fiber";
20import * as Layer from "effect/Layer";20import * as Layer from "effect/Layer";
21import * as Schema from "effect/Schema";21import * 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";
23import * as TestClock from "effect/testing/TestClock";24import * 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
26import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";28import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
27import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";29import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
...@@ -1713,8 +1715,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {...@@ -1713,8 +1715,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {
1713 );1715 );
17141716
1715 const directories = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({1717 const directories = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({
1716 fileSystem,
1717 path,
1718 cacheDir,1718 cacheDir,
1719 toolsDir: path.join(cacheDir, "tools"),1719 toolsDir: path.join(cacheDir, "tools"),
1720 platform: "linux",1720 platform: "linux",
...@@ -1727,8 +1727,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {...@@ -1727,8 +1727,6 @@ describe("acpRegistryManagedBinaryDirectories", () => {
1727 ]);1727 ]);
17281728
1729 const missing = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({1729 const missing = yield* AcpRegistrySupport.acpRegistryManagedBinaryDirectories({
1730 fileSystem,
1731 path,
1732 cacheDir: path.join(cacheDir, "does-not-exist"),1730 cacheDir: path.join(cacheDir, "does-not-exist"),
1733 toolsDir: path.join(cacheDir, "does-not-exist", "tools"),1731 toolsDir: path.join(cacheDir, "does-not-exist", "tools"),
1734 platform: "linux",1732 platform: "linux",
packages/provider-acp-registry/src/server/AcpRegistrySupport.ts+31-27
...@@ -39,8 +39,11 @@ import * as Schema from "effect/Schema";...@@ -39,8 +39,11 @@ import * as Schema from "effect/Schema";
39import * as Semaphore from "effect/Semaphore";39import * as Semaphore from "effect/Semaphore";
40import * as Stream from "effect/Stream";40import * as Stream from "effect/Stream";
41import { sha256 } from "@noble/hashes/sha2";41import { sha256 } from "@noble/hashes/sha2";
42import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";42import * as HttpClient from "effect/http/HttpClient";
43import { ChildProcess, ChildProcessSpawner } from "effect/process";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
45import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";48import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";
46import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";49import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
...@@ -308,26 +311,26 @@ function withPreferredPath(...@@ -308,26 +311,26 @@ function withPreferredPath(
308 * managed binary agents are both available by name. Best effort: unreadable311 * managed binary agents are both available by name. Best effort: unreadable
309 * directories resolve to no entries.312 * directories resolve to no entries.
310 */313 */
311export const acpRegistryManagedBinaryDirectories = (input: {314export const acpRegistryManagedBinaryDirectories = Effect.fn("acpRegistryManagedBinaryDirectories")(
312 readonly fileSystem: FileSystem.FileSystem;315 function* (input: {
313 readonly path: Path.Path;316 readonly cacheDir: string;
314 readonly cacheDir: string;317 readonly toolsDir: string;
315 readonly toolsDir: string;318 readonly platform: NodeJS.Platform;
316 readonly platform: NodeJS.Platform;319 readonly architecture: NodeJS.Architecture;
317 readonly architecture: NodeJS.Architecture;320 }): Effect.fn.Return<ReadonlyArray<string>, never, FileSystem.FileSystem | Path.Path> {
318}): Effect.Effect<ReadonlyArray<string>> =>321 const fileSystem = yield* FileSystem.FileSystem;
319 Effect.gen(function* () {322 const path = yield* Path.Path;
320 const target = resolveAcpRegistryPlatformTarget(input.platform, input.architecture);323 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");
322 const installsDirectory = input.toolsDir;325 const installsDirectory = input.toolsDir;
323 const packageReceiptsDirectory = input.path.join(registryDirectory, "package-installs");326 const packageReceiptsDirectory = path.join(registryDirectory, "package-installs");
324 const listDirectories = (directory: string) =>327 const listDirectories = (directory: string) =>
325 input.fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): Array<string> => []));328 fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): Array<string> => []));
326 const directories: Array<string> = [];329 const directories: Array<string> = [];
327330
328 for (const receiptFile of (yield* listDirectories(packageReceiptsDirectory)).toSorted()) {331 for (const receiptFile of (yield* listDirectories(packageReceiptsDirectory)).toSorted()) {
329 const receipt = yield* input.fileSystem332 const receipt = yield* fileSystem
330 .readFileString(input.path.join(packageReceiptsDirectory, receiptFile))333 .readFileString(path.join(packageReceiptsDirectory, receiptFile))
331 .pipe(334 .pipe(
332 Effect.map(decodePackageInstallReceipt),335 Effect.map(decodePackageInstallReceipt),
333 Effect.orElseSucceed(() => Option.none()),336 Effect.orElseSucceed(() => Option.none()),
...@@ -335,15 +338,15 @@ export const acpRegistryManagedBinaryDirectories = (input: {...@@ -335,15 +338,15 @@ export const acpRegistryManagedBinaryDirectories = (input: {
335 if (338 if (
336 Option.isSome(receipt) &&339 Option.isSome(receipt) &&
337 receipt.value.binDirectory ===340 receipt.value.binDirectory ===
338 input.path.join(341 path.join(
339 installsDirectory,342 installsDirectory,
340 receipt.value.agentId,343 receipt.value.agentId,
341 encodeURIComponent(receipt.value.agentVersion),344 encodeURIComponent(receipt.value.agentVersion),
342 receipt.value.distribution === "npx" ? "npm" : "python",345 receipt.value.distribution === "npx" ? "npm" : "python",
343 ...(receipt.value.distribution === "npx" && input.platform === "win32" ? [] : ["bin"]),346 ...(receipt.value.distribution === "npx" && input.platform === "win32" ? [] : ["bin"]),
344 ) &&347 ) &&
345 input.path.dirname(receipt.value.executablePath) === receipt.value.binDirectory &&348 path.dirname(receipt.value.executablePath) === receipt.value.binDirectory &&
346 (yield* input.fileSystem349 (yield* fileSystem
347 .exists(receipt.value.executablePath)350 .exists(receipt.value.executablePath)
348 .pipe(Effect.orElseSucceed(() => false)))351 .pipe(Effect.orElseSucceed(() => false)))
349 ) {352 ) {
...@@ -352,8 +355,8 @@ export const acpRegistryManagedBinaryDirectories = (input: {...@@ -352,8 +355,8 @@ export const acpRegistryManagedBinaryDirectories = (input: {
352 }355 }
353356
354 if (target === undefined) return Array.from(new Set(directories));357 if (target === undefined) return Array.from(new Set(directories));
355 const cachedAgents = yield* input.fileSystem358 const cachedAgents = yield* fileSystem
356 .readFileString(input.path.join(registryDirectory, "registry.json"))359 .readFileString(path.join(registryDirectory, "registry.json"))
357 .pipe(360 .pipe(
358 Effect.flatMap(decodeJson),361 Effect.flatMap(decodeJson),
359 Effect.flatMap(decodeRegistryIndexEnvelope),362 Effect.flatMap(decodeRegistryIndexEnvelope),
...@@ -372,12 +375,12 @@ export const acpRegistryManagedBinaryDirectories = (input: {...@@ -372,12 +375,12 @@ export const acpRegistryManagedBinaryDirectories = (input: {
372 ),375 ),
373 );376 );
374 for (const agent of (yield* listDirectories(installsDirectory)).toSorted()) {377 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(
376 (left, right) => right.localeCompare(left, undefined, { numeric: true }),379 (left, right) => right.localeCompare(left, undefined, { numeric: true }),
377 );380 );
378 for (const version of versions) {381 for (const version of versions) {
379 const installRoot = input.path.join(installsDirectory, agent, version, target);382 const installRoot = path.join(installsDirectory, agent, version, target);
380 if (yield* input.fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) {383 if (yield* fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) {
381 const binaryTarget = cachedAgentByInstall.get(`${agent}\0${version}`)?.distribution384 const binaryTarget = cachedAgentByInstall.get(`${agent}\0${version}`)?.distribution
382 .binary?.[target];385 .binary?.[target];
383 const commandSegments =386 const commandSegments =
...@@ -385,15 +388,16 @@ export const acpRegistryManagedBinaryDirectories = (input: {...@@ -385,15 +388,16 @@ export const acpRegistryManagedBinaryDirectories = (input: {
385 const directory =388 const directory =
386 commandSegments === undefined389 commandSegments === undefined
387 ? installRoot390 ? installRoot
388 : input.path.join(installRoot, ...commandSegments.slice(0, -1));391 : path.join(installRoot, ...commandSegments.slice(0, -1));
389 if (yield* input.fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) {392 if (yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) {
390 directories.push(directory);393 directories.push(directory);
391 }394 }
392 }395 }
393 }396 }
394 }397 }
395 return Array.from(new Set(directories));398 return Array.from(new Set(directories));
396 });399 },
400);
397401
398export interface ResolvedAcpRegistryDistribution {402export interface ResolvedAcpRegistryDistribution {
399 readonly kind: AcpRegistryDistributionKind;403 readonly kind: AcpRegistryDistributionKind;
packages/provider-acp-registry/src/server/adapter.ts+14-26
...@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";...@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
17import type * as Scope from "effect/Scope";17import type * as Scope from "effect/Scope";
18import { ChildProcessSpawner } from "effect/process";18import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
19import * as EffectAcpErrors from "effect-acp/errors";19import * as EffectAcpErrors from "effect-acp/errors";
2020
21import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";21import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
...@@ -56,17 +56,14 @@ export interface AcpRegistryAdapterV2Options {...@@ -56,17 +56,14 @@ export interface AcpRegistryAdapterV2Options {
56 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];56 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];
57 readonly settings: AcpRegistrySettings;57 readonly settings: AcpRegistrySettings;
58 readonly environment: NodeJS.ProcessEnv;58 readonly environment: NodeJS.ProcessEnv;
59 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
60 readonly selfInvocation: SelfInvocation;59 readonly selfInvocation: SelfInvocation;
61 readonly resolver: Pick<AcpRegistrySupport.AcpRegistryCatalog["Service"], "resolve">;
62 readonly runtimeCoordinator?: AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator["Service"];
63 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];60 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
64 readonly makeRuntime?: (61 readonly makeRuntime?: (
65 input: AcpAdapterV2RuntimeInput,62 input: AcpAdapterV2RuntimeInput,
66 ) => Effect.Effect<63 ) => Effect.Effect<
67 AcpSessionRuntime.AcpSessionRuntime["Service"],64 AcpSessionRuntime.AcpSessionRuntime["Service"],
68 EffectAcpErrors.AcpError,65 EffectAcpErrors.AcpError,
69 Crypto.Crypto | Scope.Scope66 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
70 >;67 >;
71 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;68 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
72}69}
...@@ -131,17 +128,20 @@ export function acpRegistryPromptFailure(agentId: string, cause: unknown) {...@@ -131,17 +128,20 @@ export function acpRegistryPromptFailure(agentId: string, cause: unknown) {
131 });128 });
132}129}
133130
134function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {131function makeAcpRegistryRuntime(
132 options: AcpRegistryAdapterV2Options,
133 catalog: AcpRegistrySupport.AcpRegistryCatalog["Service"],
134) {
135 return (135 return (
136 input: AcpAdapterV2RuntimeInput,136 input: AcpAdapterV2RuntimeInput,
137 ): Effect.Effect<137 ): Effect.Effect<
138 AcpSessionRuntime.AcpSessionRuntime["Service"],138 AcpSessionRuntime.AcpSessionRuntime["Service"],
139 EffectAcpErrors.AcpError,139 EffectAcpErrors.AcpError,
140 Crypto.Crypto | Scope.Scope140 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
141 > =>141 > =>
142 Effect.gen(function* () {142 Effect.gen(function* () {
143 const { processEnvironment, ...runtimeInput } = input;143 const { processEnvironment, ...runtimeInput } = input;
144 const resolved = yield* options.resolver144 const resolved = yield* catalog
145 .resolve(options.settings, input.cwd, options.environment)145 .resolve(options.settings, input.cwd, options.environment)
146 .pipe(146 .pipe(
147 Effect.mapError(147 Effect.mapError(
...@@ -163,11 +163,7 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {...@@ -163,11 +163,7 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {
163 env: { ...resolved.spawn.env, ...processEnvironment },163 env: { ...resolved.spawn.env, ...processEnvironment },
164 },164 },
165 ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),165 ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),
166 }).pipe(166 }),
167 Layer.provide(
168 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, options.childProcessSpawner),
169 ),
170 ),
171 );167 );
172 return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(168 return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
173 Effect.provide(context),169 Effect.provide(context),
...@@ -178,7 +174,10 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {...@@ -178,7 +174,10 @@ function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) {
178export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(function* (174export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(function* (
179 options: AcpRegistryAdapterV2Options,175 options: AcpRegistryAdapterV2Options,
180) {176) {
181 const runtimeCoordinator = options.runtimeCoordinator;177 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
178 const runtimeCoordinator = Option.getOrUndefined(
179 yield* Effect.serviceOption(AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator),
180 );
182 const registryAgentId = options.settings.source === "local" ? "" : options.settings.agentId;181 const registryAgentId = options.settings.source === "local" ? "" : options.settings.agentId;
183 const startupKey =182 const startupKey =
184 options.settings.source === "local" ? `local:${options.instanceId}` : registryAgentId;183 options.settings.source === "local" ? `local:${options.instanceId}` : registryAgentId;
...@@ -203,7 +202,7 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu...@@ -203,7 +202,7 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
203 extractSubagentUpdate: extractDevinSubagentUpdate,202 extractSubagentUpdate: extractDevinSubagentUpdate,
204 }203 }
205 : {}),204 : {}),
206 makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options),205 makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options, catalog),
207 ...(runtimeCoordinator === undefined206 ...(runtimeCoordinator === undefined
208 ? {}207 ? {}
209 : {208 : {
...@@ -243,7 +242,6 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu...@@ -243,7 +242,6 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
243 ...(isDevin242 ...(isDevin
244 ? {243 ? {
245 clientTerminals: {244 clientTerminals: {
246 childProcessSpawner: options.childProcessSpawner,
247 environment: options.environment,245 environment: options.environment,
248 shellCommands: true,246 shellCommands: true,
249 },247 },
...@@ -275,22 +273,12 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<...@@ -275,22 +273,12 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<
275 function* (input: ProviderAdapterDriverCreateInput<AcpRegistrySettings>) {273 function* (input: ProviderAdapterDriverCreateInput<AcpRegistrySettings>) {
276 const hostEnvironment = yield* HostProcessEnvironment;274 const hostEnvironment = yield* HostProcessEnvironment;
277 const selfInvocation = yield* resolveSelfInvocation();275 const selfInvocation = yield* resolveSelfInvocation();
278 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
279 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;276 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
280 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();277 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
281 const resolver = yield* AcpRegistrySupport.AcpRegistryCatalog;
282 const runtimeCoordinator = yield* Effect.serviceOption(
283 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
284 );
285 return yield* makeAcpRegistryAdapterV2({278 return yield* makeAcpRegistryAdapterV2({
286 instanceId: input.instanceId,279 instanceId: input.instanceId,
287 settings: { ...input.config, enabled: input.enabled },280 settings: { ...input.config, enabled: input.enabled },
288 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),281 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
289 childProcessSpawner,
290 resolver,
291 ...(Option.isSome(runtimeCoordinator)
292 ? { runtimeCoordinator: runtimeCoordinator.value }
293 : {}),
294 selfInvocation,282 selfInvocation,
295 nativeLogging: (threadId) =>283 nativeLogging: (threadId) =>
296 makeNativeLogger({284 makeNativeLogger({
packages/provider-acp-registry/src/server/auth.ts+1-1
...@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";...@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";12import * as Option from "effect/Option";
13import * as Queue from "effect/Queue";13import * as Queue from "effect/Queue";
14import * as Scope from "effect/Scope";14import * as Scope from "effect/Scope";
15import { ChildProcessSpawner } from "effect/process";15import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
16import type * as AcpSchema from "effect-acp/compat";16import type * as AcpSchema from "effect-acp/compat";
1717
18import * as PtyAdapter from "@t3tools/shared/PtyAdapter";18import * 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";...@@ -19,7 +19,7 @@ import * as Ref from "effect/Ref";
19import * as Result from "effect/Result";19import * as Result from "effect/Result";
20import * as Schema from "effect/Schema";20import * as Schema from "effect/Schema";
21import * as Semaphore from "effect/Semaphore";21import * as Semaphore from "effect/Semaphore";
22import { ChildProcessSpawner } from "effect/process";22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2323
24import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";24import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
25import { AcpRegistryAdapterV2Driver, type AcpRegistryAdapterV2DriverEnv } from "./adapter.ts";25import { 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";...@@ -19,7 +19,7 @@ import * as Layer from "effect/Layer";
19import * as Option from "effect/Option";19import * as Option from "effect/Option";
20import * as Ref from "effect/Ref";20import * as Ref from "effect/Ref";
21import * as Result from "effect/Result";21import * as Result from "effect/Result";
22import { ChildProcessSpawner } from "effect/process";22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
23import type * as EffectAcpErrors from "effect-acp/errors";23import type * as EffectAcpErrors from "effect-acp/errors";
24import type * as EffectAcpSchema from "effect-acp/compat";24import 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...@@ -1055,11 +1055,11 @@ export function windowsTaskkillResultIsSuccess(exitCode: number, _output?: strin
1055}1055}
10561056
1057export const terminateWindowsProcessTreeWithTaskkill = (1057export const terminateWindowsProcessTreeWithTaskkill = (
1058 spawner: ChildProcessSpawner.ChildProcessSpawner["Service"],
1059 pid: number,1058 pid: number,
1060): Effect.Effect<void, AcpProcessGroupTerminationError> =>1059): Effect.Effect<void, AcpProcessGroupTerminationError, ChildProcessSpawner.ChildProcessSpawner> =>
1061 Effect.scoped(1060 Effect.scoped(
1062 Effect.gen(function* () {1061 Effect.gen(function* () {
1062 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1063 const taskkill = yield* spawner.spawn(1063 const taskkill = yield* spawner.spawn(
1064 ChildProcess.make("taskkill", ["/PID", String(pid), "/T", "/F"]),1064 ChildProcess.make("taskkill", ["/PID", String(pid), "/T", "/F"]),
1065 );1065 );
...@@ -1659,7 +1659,10 @@ export const make = (...@@ -1659,7 +1659,10 @@ export const make = (
1659 );1659 );
1660 const terminateWindowsProcessTree =1660 const terminateWindowsProcessTree =
1661 options.windowsProcessTreeTerminator ??1661 options.windowsProcessTreeTerminator ??
1662 ((pid: number) => terminateWindowsProcessTreeWithTaskkill(spawner, pid));1662 ((pid: number) =>
1663 terminateWindowsProcessTreeWithTaskkill(pid).pipe(
1664 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1665 ));
1663 const terminatePosixProcessTree = (grace: Duration.Input, platformOverride?: NodeJS.Platform) =>1666 const terminatePosixProcessTree = (grace: Duration.Input, platformOverride?: NodeJS.Platform) =>
1664 Effect.gen(function* () {1667 Effect.gen(function* () {
1665 const platform = platformOverride ?? options.processGroupPlatform;1668 const platform = platformOverride ?? options.processGroupPlatform;
...@@ -1728,7 +1731,9 @@ export const make = (...@@ -1728,7 +1731,9 @@ export const make = (
1728 const hostPlatform = yield* HostProcessPlatform;1731 const hostPlatform = yield* HostProcessPlatform;
1729 const forceTerminateOwnedProcessGroup =1732 const forceTerminateOwnedProcessGroup =
1730 hostPlatform === "win32"1733 hostPlatform === "win32"
1731 ? terminateWindowsProcessTreeWithTaskkill(spawner, Number(child.pid))1734 ? terminateWindowsProcessTreeWithTaskkill(Number(child.pid)).pipe(
1735 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1736 )
1732 : linuxCgroupLease !== undefined1737 : linuxCgroupLease !== undefined
1733 ? terminateLinuxCgroupLease(linuxCgroupLease)1738 ? terminateLinuxCgroupLease(linuxCgroupLease)
1734 : options.ownDescendantProcessGroups === true1739 : options.ownDescendantProcessGroups === true
packages/provider-acp/src/server/adapter.ts+10-7
...@@ -50,7 +50,7 @@ import * as Result from "effect/Result";...@@ -50,7 +50,7 @@ import * as Result from "effect/Result";
50import * as Scope from "effect/Scope";50import * as Scope from "effect/Scope";
51import * as Semaphore from "effect/Semaphore";51import * as Semaphore from "effect/Semaphore";
52import * as Stream from "effect/Stream";52import * as Stream from "effect/Stream";
53import type { ChildProcessSpawner } from "effect/process";53import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
54import * as EffectAcpErrors from "effect-acp/errors";54import * as EffectAcpErrors from "effect-acp/errors";
55import type * as EffectAcpProtocol from "effect-acp/protocol";55import type * as EffectAcpProtocol from "effect-acp/protocol";
56import type * as EffectAcpSchema from "effect-acp/compat";56import type * as EffectAcpSchema from "effect-acp/compat";
...@@ -239,7 +239,7 @@ export interface AcpAdapterV2Flavor {...@@ -239,7 +239,7 @@ export interface AcpAdapterV2Flavor {
239 ) => Effect.Effect<239 ) => Effect.Effect<
240 AcpSessionRuntime.AcpSessionRuntime["Service"],240 AcpSessionRuntime.AcpSessionRuntime["Service"],
241 EffectAcpErrors.AcpError,241 EffectAcpErrors.AcpError,
242 Crypto.Crypto | Scope.Scope242 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
243 >;243 >;
244 readonly resolveModelId?: (selection: ModelSelection) => string | undefined;244 readonly resolveModelId?: (selection: ModelSelection) => string | undefined;
245 /**245 /**
...@@ -485,11 +485,10 @@ export interface AcpAdapterV2Options {...@@ -485,11 +485,10 @@ export interface AcpAdapterV2Options {
485 /**485 /**
486 * Opts the session into the ACP client `terminal` capability. Agents run486 * Opts the session into the ACP client `terminal` capability. Agents run
487 * commands themselves unless an adapter sets this; with it, sessions487 * commands themselves unless an adapter sets this; with it, sessions
488 * advertise `terminal: true` and run agent-created terminals through this488 * advertise `terminal: true` and run agent-created terminals through the
489 * spawner with the provider instance's environment. Devin sets it.489 * adapter's spawner with the provider instance's environment. Devin sets it.
490 */490 */
491 readonly clientTerminals?: {491 readonly clientTerminals?: {
492 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
493 readonly environment?: NodeJS.ProcessEnv;492 readonly environment?: NodeJS.ProcessEnv;
494 readonly shellCommands?: boolean;493 readonly shellCommands?: boolean;
495 };494 };
...@@ -1486,6 +1485,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (...@@ -1486,6 +1485,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
1486) {1485) {
1487 const crypto = yield* Crypto.Crypto;1486 const crypto = yield* Crypto.Crypto;
1488 const fileSystem = yield* FileSystem.FileSystem;1487 const fileSystem = yield* FileSystem.FileSystem;
1488 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1489 const idAllocator = yield* IdAllocator.IdAllocatorV2;1489 const idAllocator = yield* IdAllocator.IdAllocatorV2;
1490 const host = yield* ProviderHost.ProviderHost;1490 const host = yield* ProviderHost.ProviderHost;
1491 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;1491 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
...@@ -1586,7 +1586,6 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (...@@ -1586,7 +1586,6 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
1586 options.clientTerminals === undefined1586 options.clientTerminals === undefined
1587 ? undefined1587 ? undefined
1588 : yield* makeAcpClientTerminals({1588 : yield* makeAcpClientTerminals({
1589 spawner: options.clientTerminals.childProcessSpawner,
1590 defaultCwd: input.runtimePolicy.cwd ?? process.cwd(),1589 defaultCwd: input.runtimePolicy.cwd ?? process.cwd(),
1591 environment: options.clientTerminals.environment,1590 environment: options.clientTerminals.environment,
1592 shellCommands: options.clientTerminals.shellCommands,1591 shellCommands: options.clientTerminals.shellCommands,
...@@ -1607,7 +1606,9 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (...@@ -1607,7 +1606,9 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
1607 ? pendingTerminalEnvironment.environment1606 ? pendingTerminalEnvironment.environment
1608 : undefined;1607 : undefined;
1609 },1608 },
1610 });1609 }).pipe(
1610 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
1611 );
1611 if (clientTerminals !== undefined) {1612 if (clientTerminals !== undefined) {
1612 yield* Scope.addFinalizer(sessionScope, clientTerminals.disposeAll);1613 yield* Scope.addFinalizer(sessionScope, clientTerminals.disposeAll);
1613 }1614 }
...@@ -6018,6 +6019,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (...@@ -6018,6 +6019,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
6018 .pipe(6019 .pipe(
6019 Effect.provideService(Scope.Scope, runtimeScope),6020 Effect.provideService(Scope.Scope, runtimeScope),
6020 Effect.provideService(Crypto.Crypto, crypto),6021 Effect.provideService(Crypto.Crypto, crypto),
6022 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
6021 );6023 );
6022 });6024 });
60236025
...@@ -6096,6 +6098,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (...@@ -6096,6 +6098,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* (
6096 .pipe(6098 .pipe(
6097 Effect.provideService(Scope.Scope, replacementScope),6099 Effect.provideService(Scope.Scope, replacementScope),
6098 Effect.provideService(Crypto.Crypto, crypto),6100 Effect.provideService(Crypto.Crypto, crypto),
6101 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
6099 );6102 );
6100 // Session setup may publish commands before it returns. Buffer those6103 // Session setup may publish commands before it returns. Buffer those
6101 // notifications, but do not expose request or extension handlers6104 // 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";...@@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Result from "effect/Result";4import * as Result from "effect/Result";
5import { ChildProcessSpawner } from "effect/process";
65
7import {6import {
8 makeAcpClientTerminals,7 makeAcpClientTerminals,
...@@ -12,9 +11,7 @@ import {...@@ -12,9 +11,7 @@ import {
1211
13const withTerminals = <A, E>(use: (terminals: AcpClientTerminals) => Effect.Effect<A, E>) =>12const withTerminals = <A, E>(use: (terminals: AcpClientTerminals) => Effect.Effect<A, E>) =>
14 Effect.gen(function* () {13 Effect.gen(function* () {
15 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
16 const terminals = yield* makeAcpClientTerminals({14 const terminals = yield* makeAcpClientTerminals({
17 spawner,
18 defaultCwd: process.cwd(),15 defaultCwd: process.cwd(),
19 });16 });
20 return yield* use(terminals).pipe(Effect.ensuring(terminals.disposeAll));17 return yield* use(terminals).pipe(Effect.ensuring(terminals.disposeAll));
...@@ -49,9 +46,7 @@ describe("AcpClientTerminals", () => {...@@ -49,9 +46,7 @@ describe("AcpClientTerminals", () => {
4946
50 it.effect("resolves terminal environment from the requesting ACP session", () =>47 it.effect("resolves terminal environment from the requesting ACP session", () =>
51 Effect.gen(function* () {48 Effect.gen(function* () {
52 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
53 const terminals = yield* makeAcpClientTerminals({49 const terminals = yield* makeAcpClientTerminals({
54 spawner,
55 defaultCwd: process.cwd(),50 defaultCwd: process.cwd(),
56 environmentForSession: (sessionId) => ({51 environmentForSession: (sessionId) => ({
57 T3_TEST_ACP_SESSION_TOKEN: sessionId === "session-a" ? "token-a" : "token-b",52 T3_TEST_ACP_SESSION_TOKEN: sessionId === "session-a" ? "token-a" : "token-b",
...@@ -78,7 +73,6 @@ describe("AcpClientTerminals", () => {...@@ -78,7 +73,6 @@ describe("AcpClientTerminals", () => {
78 it.effect("runs Devin shell source with arguments, pipes, and session fallback variables", () =>73 it.effect("runs Devin shell source with arguments, pipes, and session fallback variables", () =>
79 Effect.gen(function* () {74 Effect.gen(function* () {
80 const terminals = yield* makeAcpClientTerminals({75 const terminals = yield* makeAcpClientTerminals({
81 spawner: yield* ChildProcessSpawner.ChildProcessSpawner,
82 defaultCwd: process.cwd(),76 defaultCwd: process.cwd(),
83 shellCommands: true,77 shellCommands: true,
84 environmentForSession: () => ({ T3_ACP_MCP_NODE: "mailbox-probe" }),78 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";...@@ -6,7 +6,8 @@ import * as Fiber from "effect/Fiber";
6import * as Scope from "effect/Scope";6import * as Scope from "effect/Scope";
7import * as Semaphore from "effect/Semaphore";7import * as Semaphore from "effect/Semaphore";
8import * as Stream from "effect/Stream";8import * 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";
10import * as EffectAcpErrors from "effect-acp/errors";11import * as EffectAcpErrors from "effect-acp/errors";
11import type * as EffectAcpSchema from "effect-acp/compat";12import type * as EffectAcpSchema from "effect-acp/compat";
12import * as NodeBuffer from "node:buffer";13import * as NodeBuffer from "node:buffer";
...@@ -137,7 +138,6 @@ export interface AcpClientTerminals {...@@ -137,7 +138,6 @@ export interface AcpClientTerminals {
137export interface AcpClientTerminalsOptions {138export interface AcpClientTerminalsOptions {
138 /** Devin sends shell source in command, rather than an executable plus args. */139 /** Devin sends shell source in command, rather than an executable plus args. */
139 readonly shellCommands?: boolean | undefined;140 readonly shellCommands?: boolean | undefined;
140 readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
141 readonly defaultCwd: string;141 readonly defaultCwd: string;
142 readonly environment?: NodeJS.ProcessEnv | undefined;142 readonly environment?: NodeJS.ProcessEnv | undefined;
143 readonly environmentForSession?:143 readonly environmentForSession?:
...@@ -161,272 +161,261 @@ function acpTerminalCommand(input: {...@@ -161,272 +161,261 @@ function acpTerminalCommand(input: {
161 });161 });
162}162}
163163
164export const makeAcpClientTerminals = (164export const makeAcpClientTerminals = Effect.fn("makeAcpClientTerminals")(function* (
165 options: AcpClientTerminalsOptions,165 options: AcpClientTerminalsOptions,
166): Effect.Effect<AcpClientTerminals> =>166): Effect.fn.Return<AcpClientTerminals, never, ChildProcessSpawner.ChildProcessSpawner> {
167 Effect.gen(function* () {167 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
168 const creationPermit = yield* Semaphore.make(1);168 const creationPermit = yield* Semaphore.make(1);
169 const terminals = new Map<string, ManagedAcpTerminal>();169 const terminals = new Map<string, ManagedAcpTerminal>();
170 let nextTerminalNumber = 1;170 let nextTerminalNumber = 1;
171171
172 const enforceTotalOutputLimit = (): void => {172 const enforceTotalOutputLimit = (): void => {
173 let excess =173 let excess =
174 Array.from(terminals.values()).reduce(174 Array.from(terminals.values()).reduce((total, terminal) => total + terminal.buffer.bytes, 0) -
175 (total, terminal) => total + terminal.buffer.bytes,175 MAX_RETAINED_OUTPUT_BYTES;
176 0,176 if (excess <= 0) return;
177 ) - MAX_RETAINED_OUTPUT_BYTES;177 for (const terminal of terminals.values()) {
178 const before = terminal.buffer.bytes;
179 trimOutputStart(terminal.buffer, excess);
180 excess -= before - terminal.buffer.bytes;
178 if (excess <= 0) return;181 if (excess <= 0) return;
179 for (const terminal of terminals.values()) {182 }
180 const before = terminal.buffer.bytes;183 };
181 trimOutputStart(terminal.buffer, excess);
182 excess -= before - terminal.buffer.bytes;
183 if (excess <= 0) return;
184 }
185 };
186184
187 const pruneReleasedSnapshots = (): void => {185 const pruneReleasedSnapshots = (): void => {
188 const released = Array.from(terminals.values()).filter((terminal) => terminal.released);186 const released = Array.from(terminals.values()).filter((terminal) => terminal.released);
189 let retainedBytes = released.reduce((total, terminal) => total + terminal.buffer.bytes, 0);187 let retainedBytes = released.reduce((total, terminal) => total + terminal.buffer.bytes, 0);
190 while (188 while (released.length > MAX_RETAINED_TERMINALS || retainedBytes > MAX_RETAINED_OUTPUT_BYTES) {
191 released.length > MAX_RETAINED_TERMINALS ||189 const terminal = released.shift();
192 retainedBytes > MAX_RETAINED_OUTPUT_BYTES190 if (terminal === undefined) break;
193 ) {191 retainedBytes -= terminal.buffer.bytes;
194 const terminal = released.shift();192 terminals.delete(terminal.terminalId);
195 if (terminal === undefined) break;193 }
196 retainedBytes -= terminal.buffer.bytes;194 };
197 terminals.delete(terminal.terminalId);
198 }
199 };
200195
201 const requireTerminal = (196 const requireTerminal = (
202 sessionId: string,197 sessionId: string,
203 terminalId: string,198 terminalId: string,
204 operation: string,199 operation: string,
205 ): Effect.Effect<ManagedAcpTerminal, EffectAcpErrors.AcpRequestError> => {200 ): Effect.Effect<ManagedAcpTerminal, EffectAcpErrors.AcpRequestError> => {
206 const terminal = terminals.get(terminalId);201 const terminal = terminals.get(terminalId);
207 if (terminal === undefined || terminal.sessionId !== sessionId || terminal.released) {202 if (terminal === undefined || terminal.sessionId !== sessionId || terminal.released) {
208 return Effect.fail(203 return Effect.fail(
209 terminalRequestError(`ACP ${operation} received an unknown terminal ID '${terminalId}'.`),204 terminalRequestError(`ACP ${operation} received an unknown terminal ID '${terminalId}'.`),
210 );205 );
211 }206 }
212 return Effect.succeed(terminal);207 return Effect.succeed(terminal);
213 };208 };
214209
215 const create: AcpClientTerminals["create"] = (request) =>210 const create: AcpClientTerminals["create"] = (request) =>
216 creationPermit.withPermit(211 creationPermit.withPermit(
217 Effect.uninterruptibleMask((restore) =>212 Effect.uninterruptibleMask((restore) =>
218 Effect.gen(function* () {213 Effect.gen(function* () {
219 const liveCount = Array.from(terminals.values()).filter(214 const liveCount = Array.from(terminals.values()).filter(
220 (terminal) => !terminal.released && terminal.exitStatus === undefined,215 (terminal) => !terminal.released && terminal.exitStatus === undefined,
221 ).length;216 ).length;
222 const unreleasedCount = Array.from(terminals.values()).filter(217 const unreleasedCount = Array.from(terminals.values()).filter(
223 (terminal) => !terminal.released,218 (terminal) => !terminal.released,
224 ).length;219 ).length;
225 if (unreleasedCount >= MAX_UNRELEASED_TERMINALS) {220 if (unreleasedCount >= MAX_UNRELEASED_TERMINALS) {
226 return yield* terminalRequestError(221 return yield* terminalRequestError(
227 `ACP terminal/create exceeded the limit of ${MAX_UNRELEASED_TERMINALS} unreleased terminals.`,222 `ACP terminal/create exceeded the limit of ${MAX_UNRELEASED_TERMINALS} unreleased terminals.`,
228 );223 );
229 }224 }
230 if (liveCount >= MAX_LIVE_TERMINALS) {225 if (liveCount >= MAX_LIVE_TERMINALS) {
231 return yield* terminalRequestError(226 return yield* terminalRequestError(
232 `ACP terminal/create exceeded the limit of ${MAX_LIVE_TERMINALS} concurrent terminals.`,227 `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),
237 );228 );
238 const sessionEnvironment = options.environmentForSession?.(request.sessionId);229 }
239 const environment =230 const requestEnvironment = Object.fromEntries(
240 options.environment === undefined &&231 (request.env ?? []).map((variable) => [variable.name, variable.value] as const),
241 sessionEnvironment === undefined &&232 );
242 (request.env?.length ?? 0) === 0233 const sessionEnvironment = options.environmentForSession?.(request.sessionId);
243 ? undefined234 const environment =
244 : { ...options.environment, ...sessionEnvironment, ...requestEnvironment };235 options.environment === undefined &&
245 // Each terminal owns a scope so the spawned process is reliably reaped:236 sessionEnvironment === undefined &&
246 // closing the scope kills a still-running command and frees the handle.237 (request.env?.length ?? 0) === 0
247 const terminalScope = yield* Scope.make();238 ? undefined
248 const child = yield* restore(239 : { ...options.environment, ...sessionEnvironment, ...requestEnvironment };
249 options.spawner240 // Each terminal owns a scope so the spawned process is reliably reaped:
250 .spawn(241 // closing the scope kills a still-running command and frees the handle.
251 acpTerminalCommand({242 const terminalScope = yield* Scope.make();
252 request,243 const child = yield* restore(
253 shellCommands: options.shellCommands,244 spawner
254 defaultCwd: options.defaultCwd,245 .spawn(
255 environment,246 acpTerminalCommand({
256 forceKillAfter: options.forceKillAfter,247 request,
257 }),248 shellCommands: options.shellCommands,
258 )249 defaultCwd: options.defaultCwd,
259 .pipe(250 environment,
260 Effect.provideService(Scope.Scope, terminalScope),251 forceKillAfter: options.forceKillAfter,
261 Effect.mapError((cause) =>252 }),
262 terminalRequestError(253 )
263 `Could not start terminal command '${request.command}'.`,254 .pipe(
264 cause,255 Effect.provideService(Scope.Scope, terminalScope),
265 ),256 Effect.mapError((cause) =>
257 terminalRequestError(
258 `Could not start terminal command '${request.command}'.`,
259 cause,
266 ),260 ),
267 ),261 ),
268 ).pipe(
269 Effect.onExit((exit) =>
270 Exit.isFailure(exit) ? Scope.close(terminalScope, Exit.void) : Effect.void,
271 ),262 ),
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}`;269 const terminalId = `t3-term-${nextTerminalNumber}`;
275 nextTerminalNumber += 1;270 nextTerminalNumber += 1;
276 const record: ManagedAcpTerminal = {271 const record: ManagedAcpTerminal = {
277 terminalId,272 terminalId,
278 sessionId: request.sessionId,273 sessionId: request.sessionId,
279 commandLine: [request.command, ...(request.args ?? [])].join(" "),274 commandLine: [request.command, ...(request.args ?? [])].join(" "),
280 buffer: {275 buffer: {
281 chunks: [],276 chunks: [],
282 bytes: 0,277 bytes: 0,
283 truncated: false,278 truncated: false,
284 limit: Math.min(279 limit: Math.min(
285 request.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT,280 request.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT,
286 MAX_OUTPUT_BYTE_LIMIT,281 MAX_OUTPUT_BYTE_LIMIT,
287 ),282 ),
288 },283 },
289 exit: yield* Deferred.make<EffectAcpSchema.WaitForTerminalExitResponse>(),284 exit: yield* Deferred.make<EffectAcpSchema.WaitForTerminalExitResponse>(),
290 kill: child.kill().pipe(Effect.ignore),285 kill: child.kill().pipe(Effect.ignore),
291 dispose: Scope.close(terminalScope, Exit.void).pipe(Effect.ignore),286 dispose: Scope.close(terminalScope, Exit.void).pipe(Effect.ignore),
292 exitStatus: undefined,287 exitStatus: undefined,
293 released: false,288 released: false,
294 };289 };
295 terminals.set(terminalId, record);290 terminals.set(terminalId, record);
296291
297 const pump = (stream: typeof child.stdout) =>292 const pump = (stream: typeof child.stdout) =>
298 stream.pipe(293 stream.pipe(
299 Stream.runForEach((chunk) =>294 Stream.runForEach((chunk) =>
300 Effect.sync(() => {295 Effect.sync(() => {
301 appendOutput(record.buffer, chunk);296 appendOutput(record.buffer, chunk);
302 enforceTotalOutputLimit();297 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 }),
314 }),298 }),
315 Effect.flatMap((status) =>299 ),
316 Effect.all([Fiber.join(stdoutPump), Fiber.join(stderrPump)], {300 Effect.ignore,
317 discard: true,301 );
318 }).pipe(302 const stdoutPump = yield* Effect.forkDetach(pump(child.stdout));
319 Effect.andThen(303 const stderrPump = yield* Effect.forkDetach(pump(child.stderr));
320 Effect.sync(() => {304 yield* Effect.forkDetach(
321 record.exitStatus = status;305 child.exitCode.pipe(
322 }),306 Effect.match({
323 ),307 onFailure: (error) => ({ exitCode: null, signal: terminalExitSignal(error) }),
324 Effect.andThen(Deferred.succeed(record.exit, status)),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 }),
325 ),318 ),
319 Effect.andThen(Deferred.succeed(record.exit, status)),
326 ),320 ),
327 ),321 ),
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)),
364 ),322 ),
365 ),323 );
366 Effect.as({}),324 return { terminalId };
367 ),325 }),
368 );326 ),
327 );
369328
370 const readOutputSnapshot: AcpClientTerminals["readOutputSnapshot"] = (329 const output: AcpClientTerminals["output"] = (request) =>
371 sessionId,330 requireTerminal(request.sessionId, request.terminalId, "terminal/output").pipe(
372 terminalId,331 Effect.map((terminal) => ({
373 ) => {
374 const terminal = terminals.get(terminalId);
375 if (terminal === undefined || terminal.sessionId !== sessionId) return undefined;
376 return {
377 output: readOutput(terminal.buffer),332 output: readOutput(terminal.buffer),
378 truncated: terminal.buffer.truncated,333 truncated: terminal.buffer.truncated,
379 exitStatus: terminal.exitStatus,334 ...(terminal.exitStatus === undefined ? {} : { exitStatus: terminal.exitStatus }),
380 };335 })),
381 };336 );
382337
383 const readCommandLine: AcpClientTerminals["readCommandLine"] = (terminalId) =>338 const waitForExit: AcpClientTerminals["waitForExit"] = (request) =>
384 terminals.get(terminalId)?.commandLine;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(343 const kill: AcpClientTerminals["kill"] = (request) =>
387 Effect.suspend(() =>344 requireTerminal(request.sessionId, request.terminalId, "terminal/kill").pipe(
388 Effect.forEach(345 Effect.flatMap((terminal) => terminal.kill),
389 Array.from(terminals.values()),346 Effect.as({}),
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 ),
400 );347 );
401348
402 // Terminal failures surface to the agent as JSON-RPC errors and to the349 const release: AcpClientTerminals["release"] = (request) =>
403 // user only as a failed tool item, so log them for server-side diagnosis.350 Effect.uninterruptible(
404 const logged =351 requireTerminal(request.sessionId, request.terminalId, "terminal/release").pipe(
405 <Request, Response>(352 Effect.flatMap((terminal) =>
406 operation: string,353 Effect.sync(() => {
407 handler: (request: Request) => Effect.Effect<Response, EffectAcpErrors.AcpRequestError>,354 terminal.released = true;
408 ) =>355 }).pipe(
409 (request: Request) =>356 Effect.andThen(terminal.dispose),
410 handler(request).pipe(357 Effect.andThen(Deferred.await(terminal.exit)),
411 Effect.tapError((error) =>358 Effect.andThen(Effect.sync(pruneReleasedSnapshots)),
412 Effect.logWarning("ACP client terminal operation failed", {
413 operation,
414 detail: error.message,
415 }),
416 ),359 ),
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;
419 return {368 return {
420 create: logged("terminal/create", create),369 output: readOutput(terminal.buffer),
421 output: logged("terminal/output", output),370 truncated: terminal.buffer.truncated,
422 waitForExit: logged("terminal/wait_for_exit", waitForExit),371 exitStatus: terminal.exitStatus,
423 kill: logged("terminal/kill", kill),
424 release: logged("terminal/release", release),
425 readOutputSnapshot,
426 readCommandLine,
427 disposeAll,
428 };372 };
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
431/**420/**
432 * Rewrites embedded `terminal` tool-call content into plain text content so421 * 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";...@@ -15,20 +15,15 @@ import * as Deferred from "effect/Deferred";
15import * as Fiber from "effect/Fiber";15import * as Fiber from "effect/Fiber";
16import * as FileSystem from "effect/FileSystem";16import * as FileSystem from "effect/FileSystem";
17import * as Stream from "effect/Stream";17import * as Stream from "effect/Stream";
18import { ChildProcessSpawner } from "effect/process";
19import { describe, expect } from "vite-plus/test";18import { describe, expect } from "vite-plus/test";
2019
21import { makeGrokAcpRuntime } from "./acpSupport.ts";20import { makeGrokAcpRuntime } from "./acpSupport.ts";
2221
23const makeProbeRuntime = Effect.gen(function* () {22const makeProbeRuntime = makeGrokAcpRuntime({
24 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;23 grokSettings: { binaryPath: "grok" },
25 return yield* makeGrokAcpRuntime({24 environment: process.env,
26 grokSettings: { binaryPath: "grok" },25 cwd: process.cwd(),
27 environment: process.env,26 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
28 childProcessSpawner,
29 cwd: process.cwd(),
30 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
31 });
32});27});
3328
34describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () => {29describe.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", () =...@@ -78,11 +73,9 @@ describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () =
78 Effect.gen(function* () {73 Effect.gen(function* () {
79 const fileSystem = yield* FileSystem.FileSystem;74 const fileSystem = yield* FileSystem.FileSystem;
80 const cwd = yield* fileSystem.makeTempDirectoryScoped();75 const cwd = yield* fileSystem.makeTempDirectoryScoped();
81 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
82 const runtime = yield* makeGrokAcpRuntime({76 const runtime = yield* makeGrokAcpRuntime({
83 grokSettings: { binaryPath: "grok" },77 grokSettings: { binaryPath: "grok" },
84 environment: process.env,78 environment: process.env,
85 childProcessSpawner,
86 cwd,79 cwd,
87 runtimeMode: "approval-required",80 runtimeMode: "approval-required",
88 clientInfo: { name: "t3-grok-probe", version: "0.0.0" },81 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<...@@ -31,7 +31,6 @@ interface GrokAcpRuntimeInput extends Omit<
31 AcpSessionRuntime.AcpSessionRuntimeOptions,31 AcpSessionRuntime.AcpSessionRuntimeOptions,
32 "authMethodId" | "clientCapabilities" | "spawn"32 "authMethodId" | "clientCapabilities" | "spawn"
33> {33> {
34 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
35 readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;34 readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;
36 readonly environment?: NodeJS.ProcessEnv;35 readonly environment?: NodeJS.ProcessEnv;
37 readonly runtimeMode?: RuntimeMode;36 readonly runtimeMode?: RuntimeMode;
...@@ -157,7 +156,7 @@ export const makeGrokAcpRuntime = (...@@ -157,7 +156,7 @@ export const makeGrokAcpRuntime = (
157): Effect.Effect<156): Effect.Effect<
158 AcpSessionRuntime.AcpSessionRuntime["Service"],157 AcpSessionRuntime.AcpSessionRuntime["Service"],
159 EffectAcpErrors.AcpError,158 EffectAcpErrors.AcpError,
160 Crypto.Crypto | Scope.Scope159 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
161> =>160> =>
162 Effect.gen(function* () {161 Effect.gen(function* () {
163 const processGroupPlatform = yield* HostProcessPlatform.pipe(162 const processGroupPlatform = yield* HostProcessPlatform.pipe(
...@@ -176,11 +175,7 @@ export const makeGrokAcpRuntime = (...@@ -176,11 +175,7 @@ export const makeGrokAcpRuntime = (
176 cancelMeta: { ...input.cancelMeta, ...GROK_ACP_CANCEL_META },175 cancelMeta: { ...input.cancelMeta, ...GROK_ACP_CANCEL_META },
177 initializeMeta: GROK_ACP_INITIALIZE_META,176 initializeMeta: GROK_ACP_INITIALIZE_META,
178 ...grokAcpRuntimeProcessOwnership(processGroupPlatform),177 ...grokAcpRuntimeProcessOwnership(processGroupPlatform),
179 }).pipe(178 }),
180 Layer.provide(
181 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner),
182 ),
183 ),
184 );179 );
185 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(180 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
186 Effect.provide(acpContext),181 Effect.provide(acpContext),
packages/provider-grok/src/server/adapter.ts+6-12
...@@ -20,7 +20,7 @@ import * as Path from "effect/Path";...@@ -20,7 +20,7 @@ import * as Path from "effect/Path";
20import * as Layer from "effect/Layer";20import * as Layer from "effect/Layer";
21import * as Schema from "effect/Schema";21import * as Schema from "effect/Schema";
22import type * as Scope from "effect/Scope";22import type * as Scope from "effect/Scope";
23import { ChildProcessSpawner } from "effect/process";23import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
24import * as EffectAcpErrors from "effect-acp/errors";24import * as EffectAcpErrors from "effect-acp/errors";
2525
26import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";26import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
...@@ -78,6 +78,7 @@ export const GROK_PROVIDER = ProviderDriverKind.make("grok");...@@ -78,6 +78,7 @@ export const GROK_PROVIDER = ProviderDriverKind.make("grok");
78const GROK_DRIVER_KIND = GROK_PROVIDER;78const GROK_DRIVER_KIND = GROK_PROVIDER;
79export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);79export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);
80const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});80const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
81const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
8182
82export const GrokProviderCapabilitiesV2 = {83export const GrokProviderCapabilitiesV2 = {
83 ...AcpProviderCapabilitiesV2,84 ...AcpProviderCapabilitiesV2,
...@@ -122,7 +123,7 @@ export interface GrokAdapterV2Options {...@@ -122,7 +123,7 @@ export interface GrokAdapterV2Options {
122 ) => Effect.Effect<123 ) => Effect.Effect<
123 AcpSessionRuntime.AcpSessionRuntime["Service"],124 AcpSessionRuntime.AcpSessionRuntime["Service"],
124 EffectAcpErrors.AcpError,125 EffectAcpErrors.AcpError,
125 Crypto.Crypto | Scope.Scope126 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
126 >;127 >;
127 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;128 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
128}129}
...@@ -226,11 +227,7 @@ export function grokLaunchRuntimeMode(...@@ -226,11 +227,7 @@ export function grokLaunchRuntimeMode(
226}227}
227228
228/** The flavor runs Grok's launcher through the adapter's spawner. */229/** The flavor runs Grok's launcher through the adapter's spawner. */
229export function makeGrokAcpAdapterFlavor(230export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor {
230 options: GrokAdapterV2Options & {
231 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
232 },
233): AcpAdapterV2Flavor {
234 return {231 return {
235 driver: GROK_PROVIDER,232 driver: GROK_PROVIDER,
236 runtimeHarness: "Grok",233 runtimeHarness: "Grok",
...@@ -281,7 +278,6 @@ export function makeGrokAcpAdapterFlavor(...@@ -281,7 +278,6 @@ export function makeGrokAcpAdapterFlavor(
281 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,278 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
282 grokSettings: options.settings,279 grokSettings: options.settings,
283 environment: options.environment,280 environment: options.environment,
284 childProcessSpawner: options.childProcessSpawner,
285 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),281 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
286 })),282 })),
287 // In its Auto mode Grok decides routine actions itself and only asks about283 // In its Auto mode Grok decides routine actions itself and only asks about
...@@ -292,7 +288,7 @@ export function makeGrokAcpAdapterFlavor(...@@ -292,7 +288,7 @@ export function makeGrokAcpAdapterFlavor(
292 promptFailure: (cause) =>288 promptFailure: (cause) =>
293 makeProviderFailure({289 makeProviderFailure({
294 cause,290 cause,
295 ...(Schema.is(EffectAcpErrors.AcpRequestError)(cause)291 ...(isAcpRequestError(cause)
296 ? {292 ? {
297 // Grok's own failure text rides on the cause; makeProviderFailure293 // Grok's own failure text rides on the cause; makeProviderFailure
298 // redacts and bounds it before it reaches the user.294 // redacts and bounds it before it reaches the user.
...@@ -333,11 +329,9 @@ export function makeGrokAcpAdapterFlavor(...@@ -333,11 +329,9 @@ export function makeGrokAcpAdapterFlavor(
333export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (329export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
334 options: GrokAdapterV2Options,330 options: GrokAdapterV2Options,
335) {331) {
336 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
337 const flavor = makeGrokAcpAdapterFlavor({ ...options, childProcessSpawner });
338 return yield* makeAcpAdapterV2({332 return yield* makeAcpAdapterV2({
339 instanceId: options.instanceId,333 instanceId: options.instanceId,
340 flavor,334 flavor: makeGrokAcpAdapterFlavor(options),
341 selfInvocation: options.selfInvocation,335 selfInvocation: options.selfInvocation,
342 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),336 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
343 ...(options.continuationRequests === undefined337 ...(options.continuationRequests === undefined
packages/provider-grok/src/server/driver.test.ts+1-1
...@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";...@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Path from "effect/Path";8import * as Path from "effect/Path";
9import { HttpClient } from "effect/http";9import * as HttpClient from "effect/http/HttpClient";
10import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";10import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1111
12import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";12import * 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";...@@ -5,8 +5,8 @@ import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
8import { HttpClient } from "effect/http";8import * as HttpClient from "effect/http/HttpClient";
9import { ChildProcessSpawner } from "effect/process";9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1010
11import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";11import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
12import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";12import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
...@@ -191,7 +191,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {...@@ -191,7 +191,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
191 new ProviderDriverError({191 new ProviderDriverError({
192 driver: DRIVER_KIND,192 driver: DRIVER_KIND,
193 instanceId,193 instanceId,
194 detail: `Failed to build Grok snapshot: ${cause.message ?? String(cause)}`,194 detail: "Failed to build Grok snapshot.",
195 cause,195 cause,
196 }),196 }),
197 ),197 ),
packages/provider-grok/src/server/skills.test.ts+1-1
...@@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest";...@@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Sink from "effect/Sink";3import * as Sink from "effect/Sink";
4import * as Stream from "effect/Stream";4import * as Stream from "effect/Stream";
5import { ChildProcessSpawner } from "effect/process";5import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
66
7import { discoverGrokSkills } from "./skills.ts";7import { discoverGrokSkills } from "./skills.ts";
88
packages/provider-grok/src/server/skills.ts+1-1
...@@ -20,7 +20,7 @@ import type { GrokSettings } from "../settings.ts";...@@ -20,7 +20,7 @@ import type { GrokSettings } from "../settings.ts";
20import * as Effect from "effect/Effect";20import * as Effect from "effect/Effect";
21import * as Option from "effect/Option";21import * as Option from "effect/Option";
22import * as Schema from "effect/Schema";22import * as Schema from "effect/Schema";
23import { ChildProcess } from "effect/process";23import * as ChildProcess from "effect/process/ChildProcess";
24import { resolveSpawnCommand } from "@t3tools/shared/shell";24import { resolveSpawnCommand } from "@t3tools/shared/shell";
2525
26import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe";26import { 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";...@@ -16,8 +16,9 @@ import * as Exit from "effect/Exit";
16import * as Option from "effect/Option";16import * as Option from "effect/Option";
17import * as Result from "effect/Result";17import * as Result from "effect/Result";
18import * as Schema from "effect/Schema";18import * as Schema from "effect/Schema";
19import { HttpClient } from "effect/http";19import * as HttpClient from "effect/http/HttpClient";
20import { ChildProcess, ChildProcessSpawner } from "effect/process";20import * as ChildProcess from "effect/process/ChildProcess";
21import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
21import { createModelCapabilities } from "@t3tools/shared/model";22import { createModelCapabilities } from "@t3tools/shared/model";
22import { resolveSpawnCommand } from "@t3tools/shared/shell";23import { resolveSpawnCommand } from "@t3tools/shared/shell";
2324
...@@ -356,11 +357,9 @@ const discoverGrokMetadataViaAcpInitialize = (...@@ -356,11 +357,9 @@ const discoverGrokMetadataViaAcpInitialize = (
356 environment: NodeJS.ProcessEnv,357 environment: NodeJS.ProcessEnv,
357) =>358) =>
358 Effect.gen(function* () {359 Effect.gen(function* () {
359 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
360 const acp = yield* makeGrokAcpRuntime({360 const acp = yield* makeGrokAcpRuntime({
361 grokSettings,361 grokSettings,
362 environment,362 environment,
363 childProcessSpawner,
364 cwd: process.cwd(),363 cwd: process.cwd(),
365 clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },364 clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
366 });365 });
packages/provider-grok/src/server/textGeneration.ts+5-3
...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";3import * as Option from "effect/Option";
4import * as Ref from "effect/Ref";4import * as Ref from "effect/Ref";
5import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
6import { ChildProcessSpawner } from "effect/process";6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
7import type * as EffectAcpErrors from "effect-acp/errors";7import type * as EffectAcpErrors from "effect-acp/errors";
88
9import { TextGenerationError } from "@t3tools/contracts";9import { TextGenerationError } from "@t3tools/contracts";
...@@ -37,10 +37,12 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi...@@ -37,10 +37,12 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi
37 const runtime = yield* makeGrokAcpRuntime({37 const runtime = yield* makeGrokAcpRuntime({
38 grokSettings,38 grokSettings,
39 environment,39 environment,
40 childProcessSpawner: commandSpawner,
41 cwd,40 cwd,
42 clientInfo: { name: "t3-code-git-text", version: "0.0.0" },41 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
45 yield* runtime.handleSessionUpdate((notification) => {47 yield* runtime.handleSessionUpdate((notification) => {
46 const update = notification.update;48 const update = notification.update;
packages/provider-grok/src/server/usageLimits.ts+3-1
...@@ -6,7 +6,9 @@ import * as FileSystem from "effect/FileSystem";...@@ -6,7 +6,9 @@ import * as FileSystem from "effect/FileSystem";
6import * as Option from "effect/Option";6import * as Option from "effect/Option";
7import * as Path from "effect/Path";7import * as Path from "effect/Path";
8import * as Schema from "effect/Schema";8import * 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";
10import {12import {
11 clampPercent,13 clampPercent,
12 makeUnavailableUsageLimits,14 makeUnavailableUsageLimits,