1import {
2 AcpRegistryListProvidersResult,
3 AcpRegistryOperationError,
4 AcpRegistryListSessionsResult,
5 AcpRegistryProbeResult,
6 type AcpRegistryProbeAuthMethod,
7 type AcpRegistryProbeModel,
8 type AcpRegistryUrlAuthAction,
9 type ProviderInstanceId,
10 type ServerProviderSkill,
11 type ServerProviderSlashCommand,
12} from "@t3tools/contracts";
13import { AcpRegistrySettings } from "../settings.ts";
14import * as Crypto from "effect/Crypto";
15import * as Deferred from "effect/Deferred";
16import * as Duration from "effect/Duration";
17import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";
19import * as Option from "effect/Option";
20import * as Ref from "effect/Ref";
21import * as Result from "effect/Result";
22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
23import type * as EffectAcpErrors from "effect-acp/errors";
24import type * as EffectAcpSchema from "effect-acp/compat";
25
26import * as PtyAdapter from "@t3tools/shared/PtyAdapter";
27import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
28import { parseSessionModeState } from "@t3tools/provider-acp/server/runtimeModel";
29import { acpProviderOptionDescriptors } from "@t3tools/provider-acp/server/sessionConfig";
30import * as AcpRegistryRuntimeCoordinator from "./AcpRegistryRuntimeCoordinator.ts";
31import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
32
33const MAX_AUTH_METHODS = 32;
34const MAX_MODELS = 256;
35const MAX_ID_LENGTH = 128;
36const MAX_NAME_LENGTH = 160;
37const MAX_DESCRIPTION_LENGTH = 1_024;
38const MAX_COMMANDS = 128;
39const MAX_COMMAND_LINE_LENGTH = 2_048;
40// Covers a cold package command launch and ACP initialization after registry
41// preparation globally installs the exact package.
42const PROBE_TIMEOUT_SECONDS = 60;
43const PROBE_TIMEOUT = `${PROBE_TIMEOUT_SECONDS} seconds`;
44const COMMAND_ADVERTISEMENT_GRACE = "500 millis";
45
46const boundedText = (value: string, maximumLength: number): string =>
47 value.trim().slice(0, maximumLength);
48
49const boundedOpaqueValue = (value: string, maximumLength: number): string | undefined =>
50 value.length > 0 && value === value.trim() && value.length <= maximumLength ? value : undefined;
51
52export function normalizeAcpRegistryWebUrl(value: string): string | undefined {
53 if (value.length > 2_048 || !URL.canParse(value)) return undefined;
54 const url = new URL(value);
55 return url.protocol === "https:" || url.protocol === "http:" ? url.href : undefined;
56}
57
58function modelConfigOptions(
59 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined,
60): ReadonlyArray<EffectAcpSchema.SessionConfigSelectOption> {
61 return (configOptions ?? []).flatMap((option) => {
62 if (option.category !== "model" || option.type !== "select") return [];
63 return option.options.flatMap((candidate) =>
64 "value" in candidate ? [candidate] : candidate.options,
65 );
66 });
67}
68
69function normalizeModels(
70 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined,
71): ReadonlyArray<AcpRegistryProbeModel> {
72 // Model discovery is the model config option's base models; ACP has no
73 // other portable model inventory.
74 const candidates = modelConfigOptions(configOptions).map((model) => ({
75 id: model.value,
76 name: model.name,
77 description: model.description ?? null,
78 }));
79 const seen = new Set<string>();
80 const models: Array<AcpRegistryProbeModel> = [];
81 for (const candidate of candidates) {
82 const id = boundedOpaqueValue(candidate.id, MAX_ID_LENGTH);
83 if (id === undefined || seen.has(id)) continue;
84 seen.add(id);
85 models.push({
86 id,
87 name: boundedText(candidate.name, MAX_NAME_LENGTH) || id,
88 description:
89 candidate.description === null
90 ? null
91 : boundedText(candidate.description, MAX_DESCRIPTION_LENGTH) || null,
92 });
93 if (models.length === MAX_MODELS) break;
94 }
95 return models;
96}
97
98/** Agent spawn recipe used to compose runnable terminal-auth command lines. */
99export interface AcpRegistryAuthSpawnContext {
100 readonly command: string;
101 readonly args: ReadonlyArray<string>;
102}
103
104const SHELL_SAFE_TOKEN = /^[A-Za-z0-9_@%+=:,./-]+$/u;
105
106function shellDisplayToken(token: string): string {
107 return SHELL_SAFE_TOKEN.test(token) ? token : `'${token.replaceAll("'", `'\\''`)}'`;
108}
109
110function terminalAuthCommand(
111 method: Extract<EffectAcpSchema.AuthMethod, { readonly type: "terminal" }>,
112 spawn: AcpRegistryAuthSpawnContext,
113): string | undefined {
114 const environmentPrefix = Object.entries(method.env ?? {}).map(
115 ([name, value]) => `${name}=${shellDisplayToken(value)}`,
116 );
117 const command = [spawn.command, ...spawn.args, ...(method.args ?? [])].map(shellDisplayToken);
118 const displayCommand = [...environmentPrefix, ...command].join(" ");
119 return displayCommand.length <= MAX_COMMAND_LINE_LENGTH ? displayCommand : undefined;
120}
121
122export function normalizeAcpRegistryAuthMethods(
123 methods: ReadonlyArray<EffectAcpSchema.AuthMethod> | undefined,
124 spawn?: AcpRegistryAuthSpawnContext,
125): ReadonlyArray<AcpRegistryProbeAuthMethod> {
126 const normalized: Array<AcpRegistryProbeAuthMethod> = [];
127 for (const method of methods ?? []) {
128 const id = boundedOpaqueValue(method.id, MAX_ID_LENGTH);
129 if (id === undefined) continue;
130 const type = method.type ?? "agent";
131 const envVarNames =
132 type === "env_var" && "vars" in method
133 ? method.vars
134 .flatMap((variable) => {
135 const name = boundedOpaqueValue(variable.name, MAX_ID_LENGTH);
136 return name === undefined ? [] : [name];
137 })
138 .slice(0, 16)
139 : [];
140 const command =
141 spawn !== undefined && "type" in method && method.type === "terminal"
142 ? terminalAuthCommand(method, spawn)
143 : undefined;
144 const link =
145 type === "env_var" && "link" in method && method.link
146 ? normalizeAcpRegistryWebUrl(method.link)
147 : undefined;
148 normalized.push({
149 id,
150 name: boundedText(method.name, MAX_NAME_LENGTH) || id,
151 description:
152 method.description == null
153 ? null
154 : boundedText(method.description, MAX_DESCRIPTION_LENGTH) || null,
155 type,
156 ...(command === undefined ? {} : { command }),
157 ...(envVarNames.length > 0 ? { envVarNames } : {}),
158 ...(link === undefined ? {} : { link }),
159 });
160 if (normalized.length === MAX_AUTH_METHODS) break;
161 }
162 return normalized;
163}
164
165export interface AcpRegistryAvailableCommands {
166 readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
167 readonly skills: ReadonlyArray<ServerProviderSkill>;
168}
169
170export interface AcpRegistryLiveConfiguration {
171 readonly models: ReadonlyArray<AcpRegistryProbeModel>;
172 readonly currentModelId: string | null;
173 readonly configOptions: AcpRegistryProbeResult["configOptions"];
174}
175
176/** Normalizes volatile session configuration using the same bounds as discovery probes. */
177export function normalizeAcpRegistryLiveConfiguration(
178 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
179 modeState?: Parameters<typeof acpProviderOptionDescriptors>[0]["modeState"],
180): AcpRegistryLiveConfiguration {
181 const modelOption = configOptions.find(
182 (option) => option.category === "model" && option.type === "select",
183 );
184 const boundedCurrentModelId =
185 modelOption?.type === "select"
186 ? (boundedOpaqueValue(modelOption.currentValue, MAX_ID_LENGTH) ?? null)
187 : null;
188 const models = normalizeModels(configOptions);
189 return {
190 models,
191 currentModelId: models.some((model) => model.id === boundedCurrentModelId)
192 ? boundedCurrentModelId
193 : null,
194 configOptions: acpProviderOptionDescriptors({ configOptions, modeState }),
195 };
196}
197
198const emptyAcpRegistryAvailableCommands = (): AcpRegistryAvailableCommands => ({
199 slashCommands: [],
200 skills: [],
201});
202
203/** Splits the latest ACP command advertisement into T3's `/` and `$` menus. */
204export function normalizeAcpRegistryCommands(
205 commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
206): AcpRegistryAvailableCommands {
207 const seen = new Set<string>();
208 const slashCommands: Array<ServerProviderSlashCommand> = [];
209 const skills: Array<ServerProviderSkill> = [];
210 let accepted = 0;
211 for (const command of commands) {
212 const name = boundedOpaqueValue(command.name, MAX_ID_LENGTH);
213 if (name === undefined) continue;
214 const key = name.toLowerCase();
215 if (seen.has(key)) continue;
216 seen.add(key);
217 const description = boundedText(command.description, MAX_DESCRIPTION_LENGTH);
218 const hint = command.input ? boundedText(command.input.hint, MAX_DESCRIPTION_LENGTH) : "";
219 if (name.startsWith("$")) {
220 const skillName = boundedOpaqueValue(name.slice(1), MAX_ID_LENGTH);
221 if (skillName === undefined) continue;
222 skills.push({
223 name: skillName,
224 ...(description ? { description } : {}),
225 path: `acp://skill/${encodeURIComponent(skillName)}`,
226 scope: "agent",
227 enabled: true,
228 });
229 } else {
230 slashCommands.push({
231 name,
232 ...(description ? { description } : {}),
233 ...(hint ? { input: { hint } } : {}),
234 });
235 }
236 accepted += 1;
237 if (accepted === MAX_COMMANDS) break;
238 }
239 return { slashCommands, skills };
240}
241
242/** Builds the bounded wire result from a successfully created disposable ACP session. */
243export function acpRegistryProbeResult(
244 instanceId: ProviderInstanceId,
245 started: AcpSessionRuntime.AcpSessionRuntimeStartResult,
246 icon: string | null = null,
247 spawn?: AcpRegistryAuthSpawnContext,
248): AcpRegistryProbeResult {
249 const liveConfiguration = normalizeAcpRegistryLiveConfiguration(
250 started.sessionSetupResult.configOptions ?? [],
251 parseSessionModeState(started.sessionSetupResult),
252 );
253 return AcpRegistryProbeResult.make({
254 instanceId,
255 ready: true,
256 icon,
257 authMethods: normalizeAcpRegistryAuthMethods(started.initializeResult.authMethods, spawn),
258 sessionManagement: {
259 canList: started.initializeResult.agentCapabilities?.sessionCapabilities?.list != null,
260 canLoad: started.initializeResult.agentCapabilities?.loadSession === true,
261 canResume: started.initializeResult.agentCapabilities?.sessionCapabilities?.resume != null,
262 canLogout: started.initializeResult.agentCapabilities?.auth?.logout != null,
263 canDelete: started.initializeResult.agentCapabilities?.sessionCapabilities?.delete != null,
264 canConfigureProviders: started.initializeResult.agentCapabilities?.providers != null,
265 },
266 ...liveConfiguration,
267 });
268}
269
270export function acpRegistryProbeFailure(
271 error: EffectAcpErrors.AcpError,
272 authMethods: ReadonlyArray<AcpRegistryProbeAuthMethod> = [],
273 authAction?: AcpRegistryUrlAuthAction,
274): AcpRegistryOperationError {
275 const detail = error._tag === "AcpTransportError" && error.detail ? error.detail : error.message;
276 const authenticationFailed =
277 (error._tag === "AcpRequestError" && error.code === -32000) ||
278 /\b(?:authenticat(?:e|ed|es|ing|ion)|credentials?|log(?:[\s-]+)?in)\b/iu.test(detail);
279 return new AcpRegistryOperationError({
280 reason: authenticationFailed ? "authentication_failed" : "probe_failed",
281 message: authenticationFailed
282 ? "The ACP agent could not complete authentication."
283 : "The ACP agent could not create a test session.",
284 cause: error,
285 ...(authMethods.length > 0 ? { authMethods } : {}),
286 ...(authAction === undefined ? {} : { authAction }),
287 });
288}
289
290export interface AcpRegistryConfigurationProbeInput {
291 readonly instanceId: ProviderInstanceId;
292 readonly settings: AcpRegistrySettings;
293 readonly cwd: string;
294 readonly environment: NodeJS.ProcessEnv;
295}
296
297type AcpRegistryManagementInput = AcpRegistryConfigurationProbeInput;
298
299export interface AcpRegistryConfigurationProbeResult {
300 readonly probe: AcpRegistryProbeResult;
301 readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
302 readonly skills: ReadonlyArray<ServerProviderSkill>;
303}
304
305export type AcpRegistryConfigurationProbe<Requirements = never> = (
306 input: AcpRegistryConfigurationProbeInput,
307) => Effect.Effect<AcpRegistryConfigurationProbeResult, AcpRegistryOperationError, Requirements>;
308
309/** Starts and immediately disposes an ACP session for one already-decoded configuration. */
310export const probeAcpRegistryConfiguration = Effect.fn("AcpRegistryProbe.probeConfiguration")(
311 function* (
312 input: AcpRegistryConfigurationProbeInput,
313 ): Effect.fn.Return<
314 AcpRegistryConfigurationProbeResult,
315 AcpRegistryOperationError,
316 AcpRegistrySupport.AcpRegistryCatalog | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto
317 > {
318 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
319 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
320 const crypto = yield* Crypto.Crypto;
321 const pty = yield* Effect.serviceOption(PtyAdapter.PtyAdapter);
322
323 const result = yield* Effect.gen(function* () {
324 // Resolution may install a missing registry package. Keep it inside the
325 // probe deadline so a provider refresh cannot inherit the installer's
326 // much longer timeout.
327 const resolved = yield* catalog
328 .resolve(input.settings, input.cwd, input.environment)
329 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError));
330 const authMethodsRef = yield* Ref.make<ReadonlyArray<AcpRegistryProbeAuthMethod>>([]);
331 const authActionRef = yield* Ref.make<AcpRegistryUrlAuthAction | undefined>(undefined);
332 const runtimeCoordinator = yield* Effect.serviceOption(
333 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
334 );
335 const runtimeContext = yield* Layer.build(
336 AcpSessionRuntime.layer({
337 spawn: resolved.spawn,
338 cwd: input.cwd,
339 clientCapabilities: {
340 auth: { terminal: Option.isSome(pty) },
341 elicitation: { url: {} },
342 fs: { readTextFile: false, writeTextFile: false },
343 terminal: false,
344 },
345 clientInfo: { name: "t3-code-provider-test", version: "0.0.0" },
346 authenticateOnAuthRequired: false,
347 onInitialized: (initializeResult) =>
348 Ref.set(
349 authMethodsRef,
350 normalizeAcpRegistryAuthMethods(initializeResult.authMethods, {
351 command: resolved.spawn.command,
352 args: resolved.spawn.args,
353 }),
354 ),
355 ...(input.settings.authMethodId ? { authMethodId: input.settings.authMethodId } : {}),
356 }).pipe(
357 Layer.provide(
358 Layer.mergeAll(
359 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
360 Layer.succeed(Crypto.Crypto, crypto),
361 ),
362 ),
363 ),
364 );
365 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
366 Effect.provide(runtimeContext),
367 );
368 const commandsRef = yield* Ref.make<AcpRegistryAvailableCommands>(
369 emptyAcpRegistryAvailableCommands(),
370 );
371 const commandsAdvertised = yield* Deferred.make<void>();
372 yield* runtime.handleElicitation((request) =>
373 Effect.gen(function* () {
374 if (
375 request.mode !== "url" ||
376 !("url" in request) ||
377 !("elicitationId" in request) ||
378 typeof request.url !== "string" ||
379 typeof request.elicitationId !== "string"
380 ) {
381 return { action: "decline" } as const;
382 }
383 const url = normalizeAcpRegistryWebUrl(request.url);
384 const elicitationId = boundedOpaqueValue(request.elicitationId, MAX_ID_LENGTH);
385 if (url === undefined || elicitationId === undefined) {
386 return { action: "decline" } as const;
387 }
388 const action: AcpRegistryUrlAuthAction = {
389 elicitationId,
390 url,
391 message: boundedText(request.message, MAX_DESCRIPTION_LENGTH),
392 };
393 yield* Ref.set(authActionRef, action);
394 const accepted = yield* Option.match(runtimeCoordinator, {
395 onNone: () => Effect.succeed(false),
396 onSome: (coordinator) => coordinator.requestUrlAuthentication(input.instanceId, action),
397 });
398 return accepted ? ({ action: "accept" } as const) : ({ action: "decline" } as const);
399 }),
400 );
401 yield* runtime.handleSessionUpdate((notification) => {
402 const update = notification.update;
403 return update.sessionUpdate === "available_commands_update"
404 ? Ref.set(commandsRef, normalizeAcpRegistryCommands(update.availableCommands)).pipe(
405 Effect.andThen(Deferred.succeed(commandsAdvertised, undefined)),
406 Effect.asVoid,
407 )
408 : Effect.void;
409 });
410 const startResult = yield* Effect.result(runtime.start());
411 if (Result.isFailure(startResult)) {
412 return yield* acpRegistryProbeFailure(
413 startResult.failure,
414 yield* Ref.get(authMethodsRef),
415 yield* Ref.get(authActionRef),
416 );
417 }
418 const started = startResult.success;
419 yield* Deferred.await(commandsAdvertised).pipe(
420 Effect.timeoutOption(COMMAND_ADVERTISEMENT_GRACE),
421 );
422 return { resolved, started, commands: yield* Ref.get(commandsRef) };
423 }).pipe(
424 Effect.scoped,
425 Effect.timeoutOrElse({
426 duration: PROBE_TIMEOUT,
427 orElse: () =>
428 Effect.fail(
429 new AcpRegistryOperationError({
430 reason: "probe_failed",
431 message: `The ACP agent did not resolve and create a test session within ${PROBE_TIMEOUT_SECONDS} seconds. Package installation or agent startup may be slow; this check retries on the next provider refresh.`,
432 }),
433 ),
434 }),
435 Effect.mapError((error) =>
436 error._tag === "AcpRegistryOperationError" ? error : acpRegistryProbeFailure(error),
437 ),
438 );
439 return {
440 probe: acpRegistryProbeResult(
441 input.instanceId,
442 result.started,
443 result.resolved.agent?.icon ?? null,
444 {
445 command: result.resolved.spawn.command,
446 args: result.resolved.spawn.args,
447 },
448 ),
449 slashCommands: result.commands.slashCommands,
450 skills: result.commands.skills,
451 };
452 },
453);
454
455const MANAGEMENT_TIMEOUT = Duration.seconds(60);
456
457const makeAcpRegistryManagementRuntime = Effect.fn("AcpRegistryProbe.makeManagementRuntime")(
458 function* (input: AcpRegistryManagementInput) {
459 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
460 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
461 const crypto = yield* Crypto.Crypto;
462 const runtimeCoordinator = yield* Effect.serviceOption(
463 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
464 );
465 const resolved = yield* catalog
466 .resolve(input.settings, input.cwd, input.environment)
467 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError));
468 const runtimeContext = yield* Layer.build(
469 AcpSessionRuntime.layer({
470 spawn: resolved.spawn,
471 cwd: input.cwd,
472 clientCapabilities: {
473 auth: { terminal: false },
474 elicitation: { url: {} },
475 fs: { readTextFile: false, writeTextFile: false },
476 terminal: false,
477 },
478 clientInfo: { name: "t3-code-session-manager", version: "0.0.0" },
479 authenticateOnAuthRequired: false,
480 ...(input.settings.authMethodId ? { authMethodId: input.settings.authMethodId } : {}),
481 }).pipe(
482 Layer.provide(
483 Layer.mergeAll(
484 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
485 Layer.succeed(Crypto.Crypto, crypto),
486 ),
487 ),
488 ),
489 );
490 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
491 Effect.provide(runtimeContext),
492 );
493 yield* runtime.handleElicitation((request) =>
494 Effect.gen(function* () {
495 if (
496 request.mode !== "url" ||
497 !("url" in request) ||
498 !("elicitationId" in request) ||
499 typeof request.url !== "string" ||
500 typeof request.elicitationId !== "string"
501 ) {
502 return { action: "decline" } as const;
503 }
504 const url = normalizeAcpRegistryWebUrl(request.url);
505 const elicitationId = boundedOpaqueValue(request.elicitationId, MAX_ID_LENGTH);
506 if (url === undefined || elicitationId === undefined) {
507 return { action: "decline" } as const;
508 }
509 const action: AcpRegistryUrlAuthAction = {
510 elicitationId,
511 url,
512 message: boundedText(request.message, MAX_DESCRIPTION_LENGTH),
513 };
514 const accepted = yield* Option.match(runtimeCoordinator, {
515 onNone: () => Effect.succeed(false),
516 onSome: (coordinator) => coordinator.requestUrlAuthentication(input.instanceId, action),
517 });
518 return accepted ? ({ action: "accept" } as const) : ({ action: "decline" } as const);
519 }),
520 );
521 return runtime;
522 },
523);
524
525function managementFailure(
526 operation:
527 | "list"
528 | "delete"
529 | "providers_list"
530 | "providers_set"
531 | "providers_disable"
532 | "logout",
533 error: EffectAcpErrors.AcpError | AcpRegistryOperationError,
534): AcpRegistryOperationError {
535 if (error._tag === "AcpRegistryOperationError") return error;
536 if (error._tag === "AcpRequestError" && error.code === -32601) {
537 if (operation === "list") {
538 return new AcpRegistryOperationError({
539 reason: "session_list_unsupported",
540 message: "The ACP agent does not advertise session listing.",
541 cause: error,
542 });
543 }
544 if (operation === "delete") {
545 return new AcpRegistryOperationError({
546 reason: "session_delete_unsupported",
547 message: "The ACP agent does not advertise session deletion.",
548 cause: error,
549 });
550 }
551 if (operation.startsWith("providers_")) {
552 return new AcpRegistryOperationError({
553 reason: "providers_unsupported",
554 message: "The ACP agent does not advertise provider configuration.",
555 cause: error,
556 });
557 }
558 return new AcpRegistryOperationError({
559 reason: "logout_unsupported",
560 message: "The ACP agent does not advertise logout.",
561 cause: error,
562 });
563 }
564 const classified = acpRegistryProbeFailure(error);
565 if (classified.reason === "authentication_failed") return classified;
566 const failure =
567 operation === "delete"
568 ? { reason: "session_delete_failed" as const, message: "Could not delete the ACP session." }
569 : operation === "providers_list"
570 ? { reason: "providers_list_failed" as const, message: "Could not list ACP providers." }
571 : operation === "providers_set" || operation === "providers_disable"
572 ? {
573 reason: "provider_configuration_failed" as const,
574 message: "Could not update the ACP provider configuration.",
575 }
576 : undefined;
577 return failure === undefined
578 ? classified
579 : new AcpRegistryOperationError({ ...failure, cause: error });
580}
581
582export const listAcpRegistrySessions = Effect.fn("AcpRegistryProbe.listSessions")(
583 function* (input: AcpRegistryManagementInput & { readonly cursor?: string }) {
584 const runtime = yield* makeAcpRegistryManagementRuntime(input);
585 const initialized = yield* runtime.initialize();
586 const listed = yield* runtime.listSessions(input.cursor);
587 const sessions = listed.sessions.slice(0, 256).flatMap((session) => {
588 const sessionId = boundedOpaqueValue(session.sessionId, 1_024);
589 const cwd = boundedOpaqueValue(session.cwd, 4_096);
590 if (sessionId === undefined || cwd === undefined) return [];
591 return [
592 {
593 sessionId,
594 cwd,
595 additionalDirectories: (session.additionalDirectories ?? [])
596 .flatMap((directory) => {
597 const normalized = boundedOpaqueValue(directory, 4_096);
598 return normalized === undefined ? [] : [normalized];
599 })
600 .slice(0, 32),
601 title:
602 session.title === undefined || session.title === null
603 ? null
604 : boundedText(session.title, 1_024) || null,
605 updatedAt:
606 session.updatedAt === undefined || session.updatedAt === null
607 ? null
608 : boundedText(session.updatedAt, 128) || null,
609 importedThreadId: null,
610 },
611 ];
612 });
613 return AcpRegistryListSessionsResult.make({
614 sessions,
615 nextCursor:
616 listed.nextCursor === undefined || listed.nextCursor === null
617 ? null
618 : boundedText(listed.nextCursor, 2_048) || null,
619 canLoad: initialized.agentCapabilities?.loadSession === true,
620 canResume: initialized.agentCapabilities?.sessionCapabilities?.resume != null,
621 canDelete: initialized.agentCapabilities?.sessionCapabilities?.delete != null,
622 });
623 },
624 (effect) =>
625 effect.pipe(
626 Effect.scoped,
627 Effect.timeoutOrElse({
628 duration: MANAGEMENT_TIMEOUT,
629 orElse: () =>
630 Effect.fail(
631 new AcpRegistryOperationError({
632 reason: "probe_failed",
633 message: "The ACP session list request timed out.",
634 }),
635 ),
636 }),
637 Effect.mapError((error) => managementFailure("list", error)),
638 ),
639);
640
641export const deleteAcpRegistrySession = Effect.fn("AcpRegistryProbe.deleteSession")(
642 function* (input: AcpRegistryManagementInput & { readonly sessionId: string }) {
643 const runtime = yield* makeAcpRegistryManagementRuntime(input);
644 yield* runtime.deleteSession(input.sessionId);
645 },
646 (effect) =>
647 effect.pipe(
648 Effect.scoped,
649 Effect.timeoutOrElse({
650 duration: MANAGEMENT_TIMEOUT,
651 orElse: () =>
652 Effect.fail(
653 new AcpRegistryOperationError({
654 reason: "session_delete_failed",
655 message: "The ACP session delete request timed out.",
656 }),
657 ),
658 }),
659 Effect.mapError((error) => managementFailure("delete", error)),
660 ),
661);
662
663export const listAcpRegistryProviders = Effect.fn("AcpRegistryProbe.listProviders")(
664 function* (input: AcpRegistryManagementInput) {
665 const runtime = yield* makeAcpRegistryManagementRuntime(input);
666 const listed = yield* runtime.listProviders;
667 return AcpRegistryListProvidersResult.make({
668 providers: listed.providers.slice(0, 64).flatMap((provider) => {
669 const providerId = boundedOpaqueValue(provider.providerId, 256);
670 if (providerId === undefined) return [];
671 const supported = provider.supported
672 .flatMap((protocol) => {
673 const value = boundedOpaqueValue(protocol, 64);
674 return value === undefined ? [] : [value];
675 })
676 .slice(0, 16);
677 const current = provider.current;
678 const currentApiType =
679 current == null ? undefined : boundedOpaqueValue(current.apiType, 64);
680 const currentBaseUrl =
681 current == null ? undefined : normalizeAcpRegistryWebUrl(current.baseUrl);
682 return [
683 {
684 providerId,
685 supported,
686 required: provider.required,
687 current:
688 currentApiType === undefined || currentBaseUrl === undefined
689 ? null
690 : { apiType: currentApiType, baseUrl: currentBaseUrl },
691 },
692 ];
693 }),
694 });
695 },
696 (effect) =>
697 effect.pipe(
698 Effect.scoped,
699 Effect.timeoutOrElse({
700 duration: MANAGEMENT_TIMEOUT,
701 orElse: () =>
702 Effect.fail(
703 new AcpRegistryOperationError({
704 reason: "providers_list_failed",
705 message: "The ACP provider list request timed out.",
706 }),
707 ),
708 }),
709 Effect.mapError((error) => managementFailure("providers_list", error)),
710 ),
711);
712
713export const setAcpRegistryProvider = Effect.fn("AcpRegistryProbe.setProvider")(
714 function* (input: AcpRegistryManagementInput & EffectAcpSchema.SetProviderRequest) {
715 const runtime = yield* makeAcpRegistryManagementRuntime(input);
716 yield* runtime.setProvider({
717 providerId: input.providerId,
718 apiType: input.apiType,
719 baseUrl: input.baseUrl,
720 ...(input.headers === undefined ? {} : { headers: input.headers }),
721 });
722 },
723 (effect) =>
724 effect.pipe(
725 Effect.scoped,
726 Effect.timeoutOrElse({
727 duration: MANAGEMENT_TIMEOUT,
728 orElse: () =>
729 Effect.fail(
730 new AcpRegistryOperationError({
731 reason: "provider_configuration_failed",
732 message: "The ACP provider configuration request timed out.",
733 }),
734 ),
735 }),
736 Effect.mapError((error) => managementFailure("providers_set", error)),
737 ),
738);
739
740export const disableAcpRegistryProvider = Effect.fn("AcpRegistryProbe.disableProvider")(
741 function* (input: AcpRegistryManagementInput & { readonly providerId: string }) {
742 const runtime = yield* makeAcpRegistryManagementRuntime(input);
743 yield* runtime.disableProvider(input.providerId);
744 },
745 (effect) =>
746 effect.pipe(
747 Effect.scoped,
748 Effect.timeoutOrElse({
749 duration: MANAGEMENT_TIMEOUT,
750 orElse: () =>
751 Effect.fail(
752 new AcpRegistryOperationError({
753 reason: "provider_configuration_failed",
754 message: "The ACP provider disable request timed out.",
755 }),
756 ),
757 }),
758 Effect.mapError((error) => managementFailure("providers_disable", error)),
759 ),
760);
761
762export const logoutAcpRegistry = Effect.fn("AcpRegistryProbe.logout")(
763 function* (input: AcpRegistryManagementInput) {
764 const runtime = yield* makeAcpRegistryManagementRuntime(input);
765 yield* runtime.logout;
766 },
767 (effect) =>
768 effect.pipe(
769 Effect.scoped,
770 Effect.timeoutOrElse({
771 duration: MANAGEMENT_TIMEOUT,
772 orElse: () =>
773 Effect.fail(
774 new AcpRegistryOperationError({
775 reason: "probe_failed",
776 message: "The ACP logout request timed out.",
777 }),
778 ),
779 }),
780 Effect.mapError((error) => managementFailure("logout", error)),
781 ),
782);