@agen-ai/provider-acp 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +480 -2
  3. package/dist/auth/controller.d.ts +23 -0
  4. package/dist/auth/controller.js +170 -0
  5. package/dist/auth/methods.d.ts +12 -0
  6. package/dist/auth/methods.js +51 -0
  7. package/dist/capabilities.d.ts +4 -0
  8. package/dist/capabilities.js +46 -0
  9. package/dist/configuration/catalog.d.ts +35 -0
  10. package/dist/configuration/catalog.js +222 -0
  11. package/dist/configuration/control.d.ts +19 -0
  12. package/dist/configuration/control.js +109 -0
  13. package/dist/configuration/errors.d.ts +6 -0
  14. package/dist/configuration/errors.js +15 -0
  15. package/dist/configuration.d.ts +3 -0
  16. package/dist/configuration.js +9 -0
  17. package/dist/connection/acquire.d.ts +6 -0
  18. package/dist/connection/acquire.js +30 -0
  19. package/dist/connection/contracts.d.ts +28 -0
  20. package/dist/connection/contracts.js +0 -0
  21. package/dist/connection/errors.d.ts +5 -0
  22. package/dist/connection/errors.js +11 -0
  23. package/dist/connection/initialize.d.ts +16 -0
  24. package/dist/connection/initialize.js +225 -0
  25. package/dist/connection/negotiation.d.ts +6 -0
  26. package/dist/connection/negotiation.js +43 -0
  27. package/dist/contracts.d.ts +62 -0
  28. package/dist/contracts.js +0 -0
  29. package/dist/driver.d.ts +4 -0
  30. package/dist/driver.js +44 -0
  31. package/dist/environment/commands.d.ts +17 -0
  32. package/dist/environment/commands.js +104 -0
  33. package/dist/host/contracts.d.ts +108 -0
  34. package/dist/host/contracts.js +0 -0
  35. package/dist/host/interactions.d.ts +22 -0
  36. package/dist/host/interactions.js +146 -0
  37. package/dist/host/leases.d.ts +18 -0
  38. package/dist/host/leases.js +69 -0
  39. package/dist/host/lifecycle.d.ts +9 -0
  40. package/dist/host/lifecycle.js +56 -0
  41. package/dist/host/router.d.ts +47 -0
  42. package/dist/host/router.js +165 -0
  43. package/dist/host/terminals.d.ts +15 -0
  44. package/dist/host/terminals.js +94 -0
  45. package/dist/host/validation.d.ts +13 -0
  46. package/dist/host/validation.js +69 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.js +10 -0
  49. package/dist/instance.d.ts +20 -0
  50. package/dist/instance.js +248 -0
  51. package/dist/limits.d.ts +14 -0
  52. package/dist/limits.js +42 -0
  53. package/dist/mcp.d.ts +9 -0
  54. package/dist/mcp.js +62 -0
  55. package/dist/node/contracts.d.ts +15 -0
  56. package/dist/node/contracts.js +0 -0
  57. package/dist/node/index.d.ts +6 -0
  58. package/dist/node/index.js +20 -0
  59. package/dist/node/launch.d.ts +10 -0
  60. package/dist/node/launch.js +36 -0
  61. package/dist/node/process.d.ts +4 -0
  62. package/dist/node/process.js +98 -0
  63. package/dist/session/completion.d.ts +4 -0
  64. package/dist/session/completion.js +59 -0
  65. package/dist/session/create.d.ts +21 -0
  66. package/dist/session/create.js +27 -0
  67. package/dist/session/errors.d.ts +6 -0
  68. package/dist/session/errors.js +15 -0
  69. package/dist/session/files.d.ts +8 -0
  70. package/dist/session/files.js +35 -0
  71. package/dist/session/mapping.d.ts +36 -0
  72. package/dist/session/mapping.js +231 -0
  73. package/dist/session/media.d.ts +5 -0
  74. package/dist/session/media.js +22 -0
  75. package/dist/session/native.d.ts +7 -0
  76. package/dist/session/native.js +34 -0
  77. package/dist/session/opening.d.ts +22 -0
  78. package/dist/session/opening.js +67 -0
  79. package/dist/session/plans.d.ts +5 -0
  80. package/dist/session/plans.js +20 -0
  81. package/dist/session/queue.d.ts +32 -0
  82. package/dist/session/queue.js +105 -0
  83. package/dist/session/requests/elicitation.d.ts +14 -0
  84. package/dist/session/requests/elicitation.js +24 -0
  85. package/dist/session/requests/forms.d.ts +31 -0
  86. package/dist/session/requests/forms.js +241 -0
  87. package/dist/session/requests/pending.d.ts +46 -0
  88. package/dist/session/requests/pending.js +79 -0
  89. package/dist/session/requests/resolution.d.ts +5 -0
  90. package/dist/session/requests/resolution.js +32 -0
  91. package/dist/session/resources.d.ts +30 -0
  92. package/dist/session/resources.js +99 -0
  93. package/dist/session/session.d.ts +43 -0
  94. package/dist/session/session.js +479 -0
  95. package/dist/session/turn.d.ts +33 -0
  96. package/dist/session/turn.js +0 -0
  97. package/package.json +58 -3
@@ -0,0 +1,170 @@
1
+ import {
2
+ createAgentAuthenticationOutput,
3
+ throwIfAgentOperationAborted
4
+ } from "@agen-ai/agent-runtime";
5
+ import { AcpProviderConnectionError } from "../connection/errors.js";
6
+ import { awaitHost, hostDeadline } from "../host/lifecycle.js";
7
+ import { AcpHostLeases } from "../host/leases.js";
8
+ import { hostExitStatus } from "../host/validation.js";
9
+ function createAcpAuthentication(input) {
10
+ let active;
11
+ const terminals = new AcpHostLeases(input.limits.hostOperationTimeoutMs);
12
+ const port = input.methods.size === 0 ? Object.freeze({ kind: "unsupported" }) : Object.freeze({
13
+ kind: "supported",
14
+ start: (context) => start(context),
15
+ async cancel(context) {
16
+ throwIfAgentOperationAborted(context.signal);
17
+ if (!active || active.id !== context.attemptId || active.terminal || context.providerLoginId !== void 0) {
18
+ return { status: "failed", error: { category: "invalid_request", code: "authentication_attempt_not_active", message: "The authentication attempt is not active.", recovery: { kind: "none" } } };
19
+ }
20
+ const attempt = active;
21
+ attempt.expired = context.reason === "timeout";
22
+ attempt.controller.abort(new DOMException("Authentication canceled.", "AbortError"));
23
+ await attempt.settled;
24
+ return attempt.dispatched && !attempt.terminal ? { status: "failed", error: { category: "unknown", code: "authentication_outcome_uncertain", message: "Authentication was interrupted after native execution began.", recovery: { kind: "unknown" } } } : { status: "canceled" };
25
+ }
26
+ });
27
+ async function* start(context) {
28
+ throwIfAgentOperationAborted(context.signal);
29
+ throwIfAgentOperationAborted(input.lifetime);
30
+ if (active) throw new AcpProviderConnectionError("acp_authentication_active", "An ACP authentication attempt is already active.");
31
+ if (terminals.size) throw new AcpProviderConnectionError("acp_authentication_cleanup_pending", "Authentication terminal cleanup must complete before another attempt.");
32
+ const method = input.methods.get(context.flow);
33
+ if (!method) throw new TypeError("The ACP authentication flow is unsupported.");
34
+ let settle;
35
+ const attempt = {
36
+ id: context.attemptId,
37
+ controller: new AbortController(),
38
+ settled: new Promise((resolve) => {
39
+ settle = resolve;
40
+ }),
41
+ resolve: () => settle(),
42
+ dispatched: false,
43
+ expired: false,
44
+ terminal: false
45
+ };
46
+ active = attempt;
47
+ let started;
48
+ const admitted = new Promise((resolve) => {
49
+ started = resolve;
50
+ });
51
+ const result = runAttempt(context, method, attempt, started);
52
+ void result.catch(() => void 0);
53
+ try {
54
+ await admitted;
55
+ yield createAgentAuthenticationOutput({ attemptId: context.attemptId, status: "awaiting_user", occurredAt: input.now() });
56
+ yield await result;
57
+ } finally {
58
+ if (active === attempt) attempt.controller.abort(new DOMException("Authentication observer closed.", "AbortError"));
59
+ await attempt.settled;
60
+ }
61
+ }
62
+ async function runAttempt(context, method, attempt, started) {
63
+ const parent = AbortSignal.any([input.lifetime, attempt.controller.signal, ...context.signal ? [context.signal] : []]);
64
+ const remaining = context.deadlineAt ? Date.parse(context.deadlineAt) - Date.now() : input.limits.authenticationTimeoutMs;
65
+ const deadline = hostDeadline(parent, Math.max(0, Math.min(input.limits.authenticationTimeoutMs, remaining)));
66
+ let connection;
67
+ let terminalRelease;
68
+ let releaseAttempted = false;
69
+ let succeeded = false;
70
+ let cleanupFailed = false;
71
+ try {
72
+ connection = await input.acquire(deadline.signal);
73
+ throwIfAgentOperationAborted(deadline.signal);
74
+ const scope = { kind: "authentication", attemptId: context.attemptId, flow: context.flow };
75
+ if (method.type === "agent") {
76
+ const operation = connection.requestWithScope("authenticate", { methodId: method.methodId }, scope, {
77
+ signal: deadline.signal,
78
+ timeoutMs: Math.max(1, Math.min(input.limits.authenticationTimeoutMs, remaining)),
79
+ onRequestDispatch: () => {
80
+ attempt.dispatched = true;
81
+ },
82
+ onResponseReceived: () => {
83
+ attempt.terminal = true;
84
+ }
85
+ });
86
+ started();
87
+ await operation;
88
+ } else {
89
+ await input.retire(connection);
90
+ connection = void 0;
91
+ attempt.dispatched = true;
92
+ started();
93
+ const { lease, release } = await terminals.acquire((signal) => input.host.authenticateInTerminal({
94
+ instanceId: input.instanceId,
95
+ scope,
96
+ signal,
97
+ methodId: method.methodId,
98
+ args: method.args,
99
+ env: method.env
100
+ }), deadline.signal, deadline.signal, (lease2) => {
101
+ if (lease2?.exited) void Promise.resolve(lease2.exited).catch(() => void 0);
102
+ });
103
+ terminalRelease = release;
104
+ if (!lease || typeof lease.release !== "function" || !lease.exited || typeof lease.exited.then !== "function") {
105
+ throw new TypeError("The authentication terminal lease is invalid.");
106
+ }
107
+ const status = hostExitStatus(await awaitHost(lease.exited, deadline.signal));
108
+ if (status.exitCode !== 0 || status.signal !== void 0) throw new AcpProviderConnectionError("acp_authentication_terminal_failed", "The authentication terminal did not exit successfully.");
109
+ releaseAttempted = true;
110
+ await terminalRelease();
111
+ connection = await input.acquire(deadline.signal);
112
+ attempt.terminal = true;
113
+ }
114
+ throwIfAgentOperationAborted(deadline.signal);
115
+ input.retain(connection);
116
+ succeeded = true;
117
+ } catch {
118
+ } finally {
119
+ if (!succeeded) {
120
+ const cleanup = await Promise.allSettled([
121
+ ...terminalRelease && !releaseAttempted ? [terminalRelease()] : [],
122
+ ...connection ? [input.retire(connection)] : []
123
+ ]);
124
+ cleanupFailed = cleanup.some((result) => result.status === "rejected");
125
+ }
126
+ started();
127
+ deadline.close();
128
+ active = void 0;
129
+ attempt.resolve();
130
+ }
131
+ if (succeeded) return createAgentAuthenticationOutput({ attemptId: context.attemptId, status: "completed", occurredAt: input.now() });
132
+ const interrupted = parent.aborted || deadline.signal.aborted;
133
+ const uncertain = attempt.dispatched && !attempt.terminal;
134
+ const failed = uncertain || cleanupFailed || !interrupted;
135
+ const expired = attempt.expired || !parent.aborted && deadline.signal.aborted;
136
+ return createAgentAuthenticationOutput({
137
+ attemptId: context.attemptId,
138
+ status: failed ? "failed" : expired ? "expired" : "canceled",
139
+ occurredAt: input.now(),
140
+ ...failed ? { error: {
141
+ category: "unknown",
142
+ code: uncertain ? "authentication_outcome_uncertain" : "authentication_failed",
143
+ message: uncertain ? "Authentication ended without a confirmed native result." : "The authentication operation failed.",
144
+ recovery: { kind: "unknown" }
145
+ } } : {}
146
+ });
147
+ }
148
+ return {
149
+ port,
150
+ get active() {
151
+ return active !== void 0;
152
+ },
153
+ get cleanupPending() {
154
+ return terminals.size > 0;
155
+ },
156
+ async close() {
157
+ if (active) {
158
+ const attempt = active;
159
+ attempt.controller.abort(input.lifetime.reason);
160
+ await attempt.settled;
161
+ if (terminals.size) throw new AcpProviderConnectionError("acp_authentication_cleanup_pending", "Authentication terminal cleanup failed.");
162
+ return;
163
+ }
164
+ await terminals.close();
165
+ }
166
+ };
167
+ }
168
+ export {
169
+ createAcpAuthentication
170
+ };
@@ -0,0 +1,12 @@
1
+ import type { AgentAuthenticationFlow } from '@agen-ai/agent-protocol';
2
+ import type { AcpNegotiatedAgent } from '../connection/contracts.js';
3
+ import type { AcpAuthenticationMethods, AcpHostFacilities } from '../host/contracts.js';
4
+ export interface AcpAuthenticationMethod {
5
+ readonly methodId: string;
6
+ readonly type: 'agent' | 'terminal';
7
+ readonly args: readonly string[];
8
+ readonly env: Readonly<Record<string, string>>;
9
+ }
10
+ export declare function resolveAuthenticationMapping(value?: AcpAuthenticationMethods): AcpAuthenticationMethods;
11
+ export declare function selectedAuthenticationMethods(negotiated: AcpNegotiatedAgent, selections: AcpAuthenticationMethods, host: AcpHostFacilities): ReadonlyMap<AgentAuthenticationFlow, AcpAuthenticationMethod>;
12
+ //# sourceMappingURL=methods.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { hostObject, hostText } from "../host/validation.js";
2
+ function resolveAuthenticationMapping(value = {}) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((flow) => !["browser", "device_code", "terminal"].includes(flow))) {
4
+ throw new TypeError("ACP authentication requires explicit supported flow mappings.");
5
+ }
6
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([flow, selection]) => {
7
+ if (!selection || typeof selection !== "object" || Object.keys(selection).some((key) => key !== "methodId")) {
8
+ throw new TypeError("ACP authentication selections contain only an advertised method ID.");
9
+ }
10
+ return [flow, Object.freeze({ methodId: hostText(selection.methodId, 256) })];
11
+ })));
12
+ }
13
+ function selectedAuthenticationMethods(negotiated, selections, host) {
14
+ if (negotiated.authMethods.length > 32) throw new TypeError("ACP authentication method inventory exceeds its bound.");
15
+ const methods = /* @__PURE__ */ new Map();
16
+ for (const value of negotiated.authMethods) {
17
+ const method = hostObject(value);
18
+ const id = hostText(method.id, 256);
19
+ if (methods.has(id)) throw new TypeError("ACP authentication method IDs must be unique.");
20
+ methods.set(id, method);
21
+ }
22
+ const selected = /* @__PURE__ */ new Map();
23
+ for (const flow of ["device_code", "browser", "terminal"]) {
24
+ const selection = selections[flow];
25
+ if (!selection) continue;
26
+ const method = methods.get(selection.methodId);
27
+ if (!method) throw new TypeError("A selected ACP authentication method was not advertised.");
28
+ const type = method.type ?? "agent";
29
+ if (type !== "agent" && type !== "terminal") throw new TypeError("The selected ACP authentication method type is unsupported.");
30
+ if (flow === "terminal" && (type !== "terminal" || !host.authenticateInTerminal) || flow !== "terminal" && type !== "agent") throw new TypeError("ACP authentication method and host flow do not match.");
31
+ const args = method.args ?? [];
32
+ if (!Array.isArray(args) || args.length > 128) throw new TypeError("ACP authentication arguments exceed their bound.");
33
+ const env = method.env == null ? {} : hostObject(method.env);
34
+ if (Object.keys(env).length > 128) throw new TypeError("ACP authentication environment exceeds its bound.");
35
+ const variables = Object.entries(env).map(([name, value]) => {
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) throw new TypeError("ACP authentication environment names are invalid.");
37
+ return [name, hostText(value, 16384, true)];
38
+ });
39
+ selected.set(flow, Object.freeze({
40
+ methodId: selection.methodId,
41
+ type,
42
+ args: Object.freeze(args.map((value) => hostText(value, 16384, true))),
43
+ env: Object.freeze(Object.fromEntries(variables))
44
+ }));
45
+ }
46
+ return selected;
47
+ }
48
+ export {
49
+ resolveAuthenticationMapping,
50
+ selectedAuthenticationMethods
51
+ };
@@ -0,0 +1,4 @@
1
+ import { type AgentAuthenticationFlow, type AgentCapabilities, type AgentProviderKey } from '@agen-ai/agent-protocol';
2
+ import type { AcpNegotiatedAgent } from './connection/contracts.js';
3
+ export declare function acpProviderCapabilities(providerKey: AgentProviderKey, negotiated: AcpNegotiatedAgent, configuration?: 'managed' | 'selectable', authenticationFlows?: readonly AgentAuthenticationFlow[]): AgentCapabilities;
4
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { parseAgentCapabilities } from "@agen-ai/agent-protocol";
2
+ function freezeCapabilities(value) {
3
+ if (value && typeof value === "object") {
4
+ for (const child of Object.values(value)) freezeCapabilities(child);
5
+ Object.freeze(value);
6
+ }
7
+ return value;
8
+ }
9
+ function acpProviderCapabilities(providerKey, negotiated, configuration = "managed", authenticationFlows = []) {
10
+ return freezeCapabilities(parseAgentCapabilities({
11
+ protocolVersion: 10,
12
+ providerKey,
13
+ sessions: { create: true, resume: negotiated.agentCapabilities.loadSession === true || negotiated.agentCapabilities.sessionCapabilities?.resume != null, branch: { kind: "unsupported" } },
14
+ turns: { interactionModes: ["default"], interrupt: true, steer: { kind: "unsupported" } },
15
+ requests: {
16
+ approval: { kind: "supported", modes: [{ persistence: "once", scopeKinds: ["exact_action"] }] },
17
+ elicitation: { kind: "supported", fieldKinds: ["single_select", "multi_select", "boolean"], maxFields: 16, sensitiveFields: false }
18
+ },
19
+ context: { usage: { kind: "unsupported" }, compaction: { kind: "unsupported" } },
20
+ input: { text: true, images: negotiated.agentCapabilities.promptCapabilities?.image === true ? {
21
+ kind: "supported",
22
+ sourceKinds: ["base64"],
23
+ mediaTypes: ["image/png", "image/jpeg", "image/webp"],
24
+ maxImages: 16,
25
+ maxBytesPerImage: 375e3,
26
+ maxTotalBytes: 75e4,
27
+ maxWidthPixels: 1e5,
28
+ maxHeightPixels: 1e5,
29
+ maxPixelsPerImage: 1e8,
30
+ supportsImageOnly: true
31
+ } : { kind: "unsupported" }, contentReferences: { kind: "unsupported" } },
32
+ output: { streaming: true, plans: true, fileChanges: "structured", artifactKinds: [] },
33
+ configuration: configuration === "managed" ? { kind: "managed" } : { kind: "selectable", fieldKinds: ["boolean", "single_select"], maxFields: 100 },
34
+ operations: { kind: "unsupported" },
35
+ managedContent: { kind: "unsupported" },
36
+ integrations: { kind: "unsupported" },
37
+ environment: { instance: { kind: "unsupported" }, session: { kind: "read_and_watch", domains: ["commands"] } },
38
+ collaboration: { kind: "unsupported" },
39
+ generatedResources: { kind: "unsupported" },
40
+ authentication: authenticationFlows.length ? { kind: "supported", flows: authenticationFlows } : { kind: "unsupported" },
41
+ versionReporting: negotiated.agentInfo !== void 0
42
+ }));
43
+ }
44
+ export {
45
+ acpProviderCapabilities
46
+ };
@@ -0,0 +1,35 @@
1
+ import type { AcpJsonObject } from '@agen-ai/acp-transport';
2
+ import { type AgentBooleanConfigurationField, type AgentCapabilities, type AgentConfigurationCatalog, type AgentConfigurationSelectionInput, type AgentSingleSelectConfigurationField } from '@agen-ai/agent-protocol';
3
+ type ConfigurationField = Omit<AgentBooleanConfigurationField, 'revision'> | Omit<AgentSingleSelectConfigurationField, 'revision'>;
4
+ interface AcpConfigurationBinding {
5
+ readonly field: ConfigurationField;
6
+ readonly method: 'session/set_config_option' | 'session/set_mode';
7
+ readonly nativeId: string;
8
+ readonly options: ReadonlyMap<string, string>;
9
+ }
10
+ export declare class AcpConfigurationCatalogState {
11
+ private readonly maxSnapshotBytes;
12
+ private readonly capability;
13
+ private snapshot;
14
+ private bindings;
15
+ private fingerprint;
16
+ private failure;
17
+ private source;
18
+ constructor(maxSnapshotBytes: number, capability: Extract<AgentCapabilities['configuration'], {
19
+ readonly kind: 'selectable';
20
+ }>);
21
+ private replace;
22
+ assertValid(): void;
23
+ retire(error: Error): void;
24
+ observeSetup(response: AcpJsonObject): void;
25
+ observeUpdate(update: AcpJsonObject): boolean;
26
+ observeConfigResponse(response: AcpJsonObject): void;
27
+ observeMode(nativeMode: string): void;
28
+ list(): AgentConfigurationCatalog;
29
+ resolve(input: AgentConfigurationSelectionInput): {
30
+ readonly binding: AcpConfigurationBinding;
31
+ readonly selection: AgentConfigurationSelectionInput;
32
+ };
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,222 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ AGENT_CONFIGURATION_FIELDS_MAX_LENGTH,
4
+ AGENT_CONFIGURATION_OPTIONS_MAX_LENGTH,
5
+ parseAgentConfigurationCatalog,
6
+ parseAgentConfigurationSelectionFor
7
+ } from "@agen-ai/agent-protocol";
8
+ import { AcpConfigurationError, invalidConfigurationCatalog } from "./errors.js";
9
+ function object(value) {
10
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return invalidConfigurationCatalog();
11
+ return value;
12
+ }
13
+ function string(value) {
14
+ if (typeof value !== "string" || !value || value.length > 4096) return invalidConfigurationCatalog();
15
+ return value;
16
+ }
17
+ function optionalDescription(value) {
18
+ return value === null || value === void 0 || value === "" ? {} : { description: string(value) };
19
+ }
20
+ function id(kind, ...nativeIds) {
21
+ return `acp:${kind}:${createHash("sha256").update(JSON.stringify(nativeIds)).digest("hex")}`;
22
+ }
23
+ function compareKeys(left, right) {
24
+ return left.key < right.key ? -1 : left.key > right.key ? 1 : 0;
25
+ }
26
+ function selectBinding(input) {
27
+ if (!input.values.length || input.values.length > AGENT_CONFIGURATION_OPTIONS_MAX_LENGTH) return invalidConfigurationCatalog();
28
+ const options = /* @__PURE__ */ new Map();
29
+ const nativeValues = /* @__PURE__ */ new Set();
30
+ const publicOptions = input.values.map((option) => {
31
+ const nativeValue = string(input.method === "session/set_mode" ? option.id : option.value);
32
+ if (nativeValues.has(nativeValue)) return invalidConfigurationCatalog();
33
+ nativeValues.add(nativeValue);
34
+ const optionId = id("value", input.method, input.nativeId, nativeValue);
35
+ options.set(optionId, nativeValue);
36
+ return { optionId, label: string(option.name), ...optionalDescription(option.description) };
37
+ }).sort((left, right) => compareKeys({ key: left.optionId }, { key: right.optionId }));
38
+ if (!nativeValues.has(input.currentValue)) return invalidConfigurationCatalog();
39
+ return {
40
+ field: {
41
+ key: id("field", input.method, input.nativeId),
42
+ fieldKind: "single_select",
43
+ label: input.name,
44
+ ...input.description === void 0 ? {} : { description: input.description },
45
+ scope: "session",
46
+ applicationTiming: "immediate",
47
+ mutable: true,
48
+ currentValue: id("value", input.method, input.nativeId, input.currentValue),
49
+ options: publicOptions
50
+ },
51
+ method: input.method,
52
+ nativeId: input.nativeId,
53
+ options
54
+ };
55
+ }
56
+ function selectValues(value) {
57
+ if (!Array.isArray(value) || value.length > AGENT_CONFIGURATION_OPTIONS_MAX_LENGTH) return invalidConfigurationCatalog();
58
+ const values = [];
59
+ const groupIds = /* @__PURE__ */ new Set();
60
+ const grouped = value.length > 0 && Object.hasOwn(object(value[0]), "group");
61
+ for (const item of value) {
62
+ const option = object(item);
63
+ if (Object.hasOwn(option, "group") !== grouped) return invalidConfigurationCatalog();
64
+ if (grouped) {
65
+ const groupId = string(option.group);
66
+ if (groupIds.has(groupId)) return invalidConfigurationCatalog();
67
+ groupIds.add(groupId);
68
+ const groupName = string(option.name);
69
+ if (!Array.isArray(option.options)) return invalidConfigurationCatalog();
70
+ for (const member of option.options) {
71
+ if (values.length >= AGENT_CONFIGURATION_OPTIONS_MAX_LENGTH) return invalidConfigurationCatalog();
72
+ const groupedOption = object(member);
73
+ values.push({ ...groupedOption, name: `${groupName}: ${string(groupedOption.name)}` });
74
+ }
75
+ } else values.push(option);
76
+ }
77
+ return values;
78
+ }
79
+ function mapAcpConfigOptions(value) {
80
+ if (!Array.isArray(value) || value.length > AGENT_CONFIGURATION_FIELDS_MAX_LENGTH) return invalidConfigurationCatalog();
81
+ const nativeIds = /* @__PURE__ */ new Set();
82
+ return value.map((item) => {
83
+ const option = object(item);
84
+ const nativeId = string(option.id);
85
+ if (nativeIds.has(nativeId)) return invalidConfigurationCatalog();
86
+ nativeIds.add(nativeId);
87
+ const common = {
88
+ key: id("field", "session/set_config_option", nativeId),
89
+ label: string(option.name),
90
+ ...optionalDescription(option.description),
91
+ scope: "session",
92
+ applicationTiming: "immediate",
93
+ mutable: true
94
+ };
95
+ if (option.type === "boolean" && typeof option.currentValue === "boolean") {
96
+ return {
97
+ field: { ...common, fieldKind: "boolean", currentValue: option.currentValue },
98
+ method: "session/set_config_option",
99
+ nativeId,
100
+ options: /* @__PURE__ */ new Map()
101
+ };
102
+ }
103
+ if (option.type !== "select") return invalidConfigurationCatalog();
104
+ return selectBinding({
105
+ nativeId,
106
+ name: common.label,
107
+ ...optionalDescription(option.description),
108
+ currentValue: string(option.currentValue),
109
+ values: selectValues(option.options),
110
+ method: "session/set_config_option"
111
+ });
112
+ }).sort((left, right) => compareKeys(left.field, right.field));
113
+ }
114
+ function mapAcpModes(value) {
115
+ const modes = object(value);
116
+ if (!Array.isArray(modes.availableModes)) return invalidConfigurationCatalog();
117
+ return [selectBinding({
118
+ nativeId: "mode",
119
+ name: "Session mode",
120
+ currentValue: string(modes.currentModeId),
121
+ values: modes.availableModes.map(object),
122
+ method: "session/set_mode"
123
+ })];
124
+ }
125
+ class AcpConfigurationCatalogState {
126
+ constructor(maxSnapshotBytes, capability) {
127
+ this.maxSnapshotBytes = maxSnapshotBytes;
128
+ this.capability = capability;
129
+ }
130
+ maxSnapshotBytes;
131
+ capability;
132
+ snapshot;
133
+ bindings = /* @__PURE__ */ new Map();
134
+ fingerprint;
135
+ failure;
136
+ source;
137
+ replace(source, native) {
138
+ this.assertValid();
139
+ try {
140
+ if (native === void 0 || Buffer.byteLength(JSON.stringify(native), "utf8") > this.maxSnapshotBytes) return invalidConfigurationCatalog();
141
+ const bindings = source === "config" ? mapAcpConfigOptions(native) : mapAcpModes(native);
142
+ if (bindings.length > this.capability.maxFields || bindings.some((binding) => !this.capability.fieldKinds.includes(binding.field.fieldKind))) return invalidConfigurationCatalog();
143
+ const fingerprint = JSON.stringify(bindings.map((binding) => binding.field));
144
+ if (this.source === source && this.fingerprint === fingerprint) return;
145
+ const revision = (this.snapshot?.revision ?? 0) + 1;
146
+ if (!Number.isSafeInteger(revision)) return invalidConfigurationCatalog();
147
+ const fields = bindings.map((binding) => {
148
+ const previous = this.bindings.get(binding.field.key);
149
+ const previousField = this.snapshot?.fields.find((field) => field.key === binding.field.key);
150
+ const fieldRevision = previous && previousField && JSON.stringify(previous.field) === JSON.stringify(binding.field) ? previousField.revision : revision;
151
+ return { ...binding.field, revision: fieldRevision };
152
+ });
153
+ const snapshot = parseAgentConfigurationCatalog({ revision, fields });
154
+ this.bindings = new Map(bindings.map((binding) => [binding.field.key, binding]));
155
+ this.snapshot = snapshot;
156
+ this.source = source;
157
+ this.fingerprint = fingerprint;
158
+ } catch (cause) {
159
+ const error = cause instanceof AcpConfigurationError ? cause : new AcpConfigurationError(
160
+ "acp_configuration_catalog_invalid",
161
+ "ACP configuration cannot be represented faithfully within the declared bounds.",
162
+ { cause }
163
+ );
164
+ this.failure = error;
165
+ throw error;
166
+ }
167
+ }
168
+ assertValid() {
169
+ if (this.failure) throw this.failure;
170
+ }
171
+ retire(error) {
172
+ this.failure ??= error;
173
+ }
174
+ observeSetup(response) {
175
+ if (response.configOptions !== null && response.configOptions !== void 0) this.replace("config", response.configOptions);
176
+ else if (this.source !== "config" && response.modes !== null && response.modes !== void 0) this.replace("modes", response.modes);
177
+ }
178
+ observeUpdate(update) {
179
+ if (update.sessionUpdate === "config_option_update") {
180
+ this.replace("config", update.configOptions);
181
+ return true;
182
+ }
183
+ if (update.sessionUpdate === "current_mode_update") {
184
+ if (this.source === "modes") this.observeMode(string(update.currentModeId));
185
+ return true;
186
+ }
187
+ return false;
188
+ }
189
+ observeConfigResponse(response) {
190
+ this.replace("config", response.configOptions);
191
+ }
192
+ observeMode(nativeMode) {
193
+ this.assertValid();
194
+ const binding = [...this.bindings.values()].find((candidate) => candidate.method === "session/set_mode");
195
+ if (this.source !== "modes" || !binding || binding.field.fieldKind !== "single_select") return invalidConfigurationCatalog();
196
+ const availableModes = binding.field.options.map((option) => ({
197
+ id: binding.options.get(option.optionId),
198
+ name: option.label,
199
+ ...option.description === void 0 ? {} : { description: option.description }
200
+ }));
201
+ this.replace("modes", { currentModeId: nativeMode, availableModes });
202
+ }
203
+ list() {
204
+ this.assertValid();
205
+ if (!this.snapshot) throw new AcpConfigurationError("acp_configuration_catalog_unknown", "The ACP peer has not supplied a configuration catalog.");
206
+ return this.snapshot;
207
+ }
208
+ resolve(input) {
209
+ try {
210
+ const selection = parseAgentConfigurationSelectionFor(this.list(), input);
211
+ const binding = this.bindings.get(selection.key);
212
+ if (!binding) return invalidConfigurationCatalog();
213
+ return { binding, selection };
214
+ } catch (cause) {
215
+ if (cause instanceof AcpConfigurationError) throw cause;
216
+ throw new AcpConfigurationError("acp_configuration_selection_invalid", "The configuration selection is stale or outside the offered catalog.", { cause });
217
+ }
218
+ }
219
+ }
220
+ export {
221
+ AcpConfigurationCatalogState
222
+ };
@@ -0,0 +1,19 @@
1
+ import type { AcpClient, AcpJsonObject } from '@agen-ai/acp-transport';
2
+ import { type AgentCapabilities, type AgentSessionConfiguration } from '@agen-ai/agent-protocol';
3
+ import { type AgentSessionConfigurationControl } from '@agen-ai/agent-runtime';
4
+ export interface AcpSessionConfiguration {
5
+ readonly control: AgentSessionConfigurationControl;
6
+ readonly observeSetup: (response: AcpJsonObject) => void;
7
+ readonly observeUpdate: (update: AcpJsonObject) => boolean;
8
+ readonly applyOpeningConfiguration: (configuration: AgentSessionConfiguration, signal?: AbortSignal) => Promise<void>;
9
+ }
10
+ export declare function createAcpSessionConfiguration(input: {
11
+ readonly capability: AgentCapabilities['configuration'];
12
+ readonly client: AcpClient;
13
+ readonly getSessionId: () => string;
14
+ readonly assertUsable: () => void;
15
+ readonly onFailure: (error: Error) => void;
16
+ readonly timeoutMs: number;
17
+ readonly maxSnapshotBytes?: number;
18
+ }): AcpSessionConfiguration;
19
+ //# sourceMappingURL=control.d.ts.map