@aibolabs/acp-adapter 0.0.0-stage → 0.1.8

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chldu2000
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @aibolabs/acp-adapter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generic [Agent Client Protocol](https://agentclientprotocol.com) client for Aibo session providers.
4
+ Part of the host SDK from 0.1.2 (`worker` from 0.1.3, option approvals from 0.1.4, form elicitation
5
+ from 0.1.5, package-owned launch from 0.1.6, `parameterScope` from 0.1.7, usage mapping from 0.1.8): plugins declaring `hostSdk` import it at runtime and
6
+ keep it as a development dependency only (see [host SDK](https://github.com/chldu2000/aibo/blob/main/docs/host-sdk.md)).
7
+
8
+ | Entry | Contents |
9
+ | --- | --- |
10
+ | `@aibolabs/acp-adapter/transport` | `AcpTransport`: NDJSON JSON-RPC to the agent process, bounded frames (8 MiB, 32 MiB prompts), write backpressure, stderr tail, timeouts; every failure settles pending requests |
11
+ | `@aibolabs/acp-adapter/session` | `AcpSession`: initialize/authenticate, new/load, mode and model selection confirmed by the agent, prompts, messages, reasoning, tools, permission requests, cancellation and recovery; `usage_update` and per-turn prompt usage map to the Aibo usage snapshot (`contextTokens`/`contextWindow`, accumulated input/output/total; host SDK 0.1.8) |
12
+ | `@aibolabs/acp-adapter/config` | Parsing of ACP session config options into models, reasoning levels and context windows |
13
+ | `@aibolabs/acp-adapter/image-input` | Validation of host image descriptors into ACP image content blocks |
14
+ | `@aibolabs/acp-adapter/worker` | `serveAcpAgent`: a Runtime 2.1 Worker driven by `plugin.json` plus `acp.json`, or by a code extension; host-tool MCP bridge included |
15
+
16
+ ## Configuration-only plugins
17
+
18
+ `acp.json` (schema `aibo.acp-agent/v1`) holds `label`, `command` (an `executable` declared in
19
+ `plugin.json` `executableDependencies`), optional `args`, and `modes` mapping Aibo's `ask`, `plan` and `edit` to
20
+ native mode IDs; `edit` is the write mode, and `auto` (SDK 0.1.4) is a second write mode for controls whose
21
+ profile sets `approvalReviewer: "auto-review"`. Optional: `authMethodId`, `clientMeta`, `persistsEmptySessions`
22
+ (default `true`) and `requestPrefix`. The host manifest schema does not allow plugin fields, so this file is
23
+ read only by the Worker; an invalid one stops it before the Runtime handshake. Recovery data uses
24
+ `<pluginId>.recovery`, optional features use the manifest's `<pluginId>.<feature>` operations, and profiles follow
25
+ `agent-managed` execution. Declaring `hostTools` and `aibo.session.tool.respond` connects Aibo's host tools through
26
+ the SDK MCP bridge. See `aibo-plugins/plugins/acp-template`.
27
+
28
+ `approvalOptions` (SDK 0.1.4) lists native permission options as `{ optionId, toolKind?, label?, sessionControl?, contextReset? }`.
29
+ `label` replaces the agent's text on the approval card; `toolKind` limits the entry to requests for that ACP tool kind.
30
+ `sessionControl` names a `plugin.json` session control that some control lists in `transitions`: the option is offered
31
+ even outside a write mode, and choosing it lets the host commit that control before the agent is answered.
32
+ The Worker rejects entries whose control is undeclared, unmapped in `modes`, or not a transition target, and requires
33
+ the `{ requestId, optionId }` form of `approval.respond`.
34
+
35
+ `elicitation: true` (SDK 0.1.5; `extension.elicitation` in code) declares ACP form elicitation and claims
36
+ `user-input.respond`, which the manifest must declare. Each form field becomes one host question: selects and
37
+ booleans as options, strings and numbers as validated free text, a multi-select as a single pick, and a
38
+ `_askUserQuestionCustomAnswer` companion as the question's "other" input. Forms the host cannot express
39
+ (nested objects, more than 8 fields, duplicate labels, `url` mode) are cancelled without asking. `contextReset: true` (only with `sessionControl`) marks an
40
+ option after which the agent continues in a fresh context under the same session; the host records it and notes
41
+ on the first later resume that the restored context may predate the reset. The agent's mode report for a committed switch is adopted
42
+ and the turn continues; any other mode change during a turn fails it, and the host mode is restored before the next prompt.
43
+
44
+ Mode switching uses the agent's mode config option when it returns one, and the standard `session/set_mode`
45
+ request when it only exposes the session modes API. Reasoning and context-window capabilities are claimed only
46
+ when the agent returned those options, unless the extension sets `parameterizedPicker` (Cursor exposes
47
+ parameters per model).
48
+
49
+ ## Writing a provider
50
+
51
+ An `AcpSession` is configured with an extension object. Required fields:
52
+
53
+ - `label`: agent name used in error messages (`Cursor` gives `Cursor session is not ready`).
54
+ - `command` / `args`: the agent executable, spawned without a shell in the trusted workspace.
55
+ - `recoverySchema`, `namespace`: recovery data schema and namespace of `extension.updated` events.
56
+ - `writableMode`: the native mode that may run write-authorized turns.
57
+ - `validateExecutionProfile(profile, permissions)`: maps the host execution profile to a native mode.
58
+
59
+ Optional hooks cover agent differences: `authMethodId`, `clientMeta`, `persistsEmptySessions`, `parameterizedPicker`,
60
+ `commandCategory`, `parameterized`, `subagentFromTool`, `handleRequest` and `handleNotification`
61
+ for vendor methods. Hooks receive a narrow session surface (`event`, `respond`, `await`,
62
+ `updateSubagent`, `subagents`, `turnId`, `sessionId`); the session keeps all other state private.
63
+
64
+ Client file and terminal capabilities are advertised as unsupported, and only `allow_once` /
65
+ `reject_once` permission options are selected: the host approves each request, and persistent
66
+ agent-side grants are never chosen on the user's behalf. The Cursor plugin in `aibo-plugins`
67
+ is the reference extension.
68
+
69
+ With `approvalOptions: true` (the Worker sets it when the manifest's `approval.respond` declares
70
+ `optionId`), `approval.requested` carries `options: [{ id, kind: 'allow' | 'reject' }]` for those once options, and
71
+ `respondApproval(requestId, { optionId })` answers with one of them; any other option ID is rejected.
72
+ Extension approvals receive `{ optionId }` instead of `accept` / `cancel`.
73
+
74
+ `BASE_CAPABILITIES` is a candidate list: `session.resume` is returned only when initialize
75
+ advertises `loadSession: true`. An agent without load support can still create and run sessions;
76
+ restoring a persisted session fails explicitly rather than creating a replacement.
77
+ Vendor questions are not part of the default capability set. An extension implementing question
78
+ requests through `handleRequest` and `await` must add `user-input.respond` to its `capabilities`.
79
+
80
+ ## Package-owned ACP runtime (host SDK 0.1.6)
81
+
82
+ Instead of an external `command`, a configuration-only plugin can declare:
83
+
84
+ ```json
85
+ { "launch": { "kind": "node", "entry": "vendor/node_modules/example-acp/dist/index.js" } }
86
+ ```
87
+
88
+ This is a fragment of `acp.json`; label, schema and modes remain required. `command` and `launch`
89
+ are mutually exclusive. `args` are passed after the package entry. The entry is resolved against
90
+ `plugin.json`, checked to remain inside the package, and started with `process.execPath` (the Node
91
+ resolved for the Worker: a local installation, a manual selection or the Aibo-managed download). The child's working directory remains the user workspace. Missing entries fail startup.
92
+ No shell, npm, npx or global executable is needed. External `command` still requires an executable
93
+ manifest dependency. A package launch only needs the Node runtime dependency, plus any genuine external
94
+ tools it uses. Declare `hostSdk >=0.1.6`; ship the locked production dependency graph and platform assets.
95
+ `extensionFromConfig` accepts a third `manifestUrl` argument for package launches.
96
+
97
+ ## Sequential model parameters (host SDK 0.1.7)
98
+
99
+ Declare `"parameterScope": "current-model"` in `acp.json`, or `parameterScope: 'current-model'`
100
+ in the extension. Use the new `model.select` output contract variant requiring this field.
101
+ The adapter returns it on list and set; options still belong only to the current model.
102
+ The host selects and confirms a model before offering its reasoning options. This adapter does
103
+ not accept `all-models` in acp.json because ACP session configuration is not a complete matrix.
104
+ Omission preserves legacy behavior. Require hostSdk >=0.1.7 for this declaration.
package/config.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type AcpSelectOption = { value: string; name?: string; description?: string; tokens?: number };
2
+ export declare function selectValues(config: unknown): AcpSelectOption[];
3
+ export declare function modelParameters(configs: readonly unknown[], model: string | undefined): {
4
+ levels: { id: string; label: string; values: { id: string; value: string }[] }[];
5
+ current: string | null;
6
+ context: { id: string; currentValue: string } | null;
7
+ contextWindows: { id: string; label: string; description?: string; tokens?: number }[];
8
+ };
package/config.mjs ADDED
@@ -0,0 +1,25 @@
1
+ // ACP values remain opaque. In particular, labels are not token counts.
2
+ export function selectValues(config) {
3
+ return (Array.isArray(config?.options) ? config.options : []).flatMap(option => Array.isArray(option?.options) ? option.options : [option])
4
+ .filter(option => typeof option?.value === 'string' && option.value.length);
5
+ }
6
+ export function modelParameters(configs, model) {
7
+ const valid = configs.filter(config => config?.type === 'select' && typeof config.id === 'string' && typeof config.currentValue === 'string' && selectValues(config).length);
8
+ const thoughts = valid.filter(config => config.category === 'thought_level' || (!config.category && ['effort', 'reasoning', 'reasoning_effort', 'thought_level', 'thinking'].includes(config.id)));
9
+ const contexts = valid.filter(config => config.category === 'model_config' && ['context', 'context_window', 'context_size'].includes(config.id));
10
+ let combinations = thoughts.length ? [{ values: [], labels: [] }] : [];
11
+ for (const config of thoughts) {
12
+ if (combinations.length * selectValues(config).length > 128) { combinations = []; break; }
13
+ combinations = combinations.flatMap(previous => selectValues(config).map(option => ({
14
+ values: [...previous.values, { id: config.id, value: option.value }],
15
+ labels: [...previous.labels, thoughts.length > 1 ? `${config.name || config.id}: ${option.name || option.value}` : option.name || option.value],
16
+ })));
17
+ }
18
+ const levels = combinations.map(({ values, labels }) => ({ id: JSON.stringify([model, values]), label: labels.join(' · '), values }));
19
+ const current = levels.find(level => level.values.every(value => thoughts.find(config => config.id === value.id)?.currentValue === value.value))?.id ?? null;
20
+ const context = contexts.length === 1 ? contexts[0] : null;
21
+ return { levels, current, context, contextWindows: selectValues(context).map(option => ({ id: option.value, label: option.name || option.value,
22
+ ...(option.description ? { description: option.description } : {}),
23
+ ...(Number.isSafeInteger(option.tokens) && option.tokens > 0 ? { tokens: option.tokens } : {}),
24
+ })) };
25
+ }
@@ -0,0 +1,125 @@
1
+ // ACP form elicitation (`elicitation/create`, mode `form`) mapped onto host questions. The host
2
+ // question model is one answer per question: an option label, or free text when `isOther` is set.
3
+ // Schemas that model cannot express faithfully return null and the request is cancelled.
4
+ import { pluginError } from './session.mjs';
5
+
6
+ export const MAX_QUESTIONS = 8;
7
+ const MAX_OPTIONS = 32;
8
+ // Cross-agent marker for a free-text "custom answer" companion to a select question
9
+ // (claude-agent-acp's AskUserQuestion bridge; intentionally not vendor-namespaced).
10
+ const CUSTOM_ANSWER = '_askUserQuestionCustomAnswer';
11
+ const YES = '是', NO = '否';
12
+
13
+ const text = (value, max) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null;
14
+
15
+ function choices(values) {
16
+ if (!Array.isArray(values) || !values.length || values.length > MAX_OPTIONS) return null;
17
+ const options = values.map(value => value && typeof value === 'object' && !Array.isArray(value)
18
+ ? { value: value.const, label: text(value.title, 200) ?? (typeof value.const === 'string' ? value.const : null), description: text(value.description, 500) }
19
+ : { value, label: typeof value === 'string' ? value : null, description: null });
20
+ if (options.some(option => typeof option.value !== 'string' || !option.label)) return null;
21
+ // Answers come back as labels, so labels must identify exactly one value.
22
+ if (new Set(options.map(option => option.label)).size !== options.length) return null;
23
+ return options;
24
+ }
25
+
26
+ function field(schema) {
27
+ const s = schema && typeof schema === 'object' ? schema : {};
28
+ if (s.type === 'string' && (s.oneOf || s.enum)) {
29
+ const options = choices(s.oneOf ?? s.enum);
30
+ return options && { kind: 'choice', options };
31
+ }
32
+ if (s.type === 'array') {
33
+ const items = s.items && typeof s.items === 'object' ? s.items : {};
34
+ const options = choices(items.anyOf ?? items.oneOf ?? items.enum);
35
+ // The host submits one answer per question; a form that needs more than one pick cannot be met.
36
+ if (!options || (s.minItems ?? 0) > 1) return null;
37
+ return { kind: 'multi', options };
38
+ }
39
+ if (s.type === 'boolean') return { kind: 'boolean', options: [{ value: true, label: YES, description: null }, { value: false, label: NO, description: null }] };
40
+ if (s.type === 'string') return { kind: 'text' };
41
+ if (s.type === 'number' || s.type === 'integer') return { kind: s.type };
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Maps an elicitation form to host questions and an `answer` encoder, or returns null when the
47
+ * schema cannot be represented. `answer` receives host answers (`{ [questionId]: [value] }`) and
48
+ * returns the ACP accept response; invalid answers throw `invalid_input` and leave the request pending.
49
+ */
50
+ export function elicitationForm(params) {
51
+ const schema = params?.requestedSchema;
52
+ const message = text(params?.message, 4_000);
53
+ if (!schema || schema.type !== 'object' || !schema.properties || typeof schema.properties !== 'object' || !message) return null;
54
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
55
+ const entries = Object.entries(schema.properties);
56
+ const companions = new Map();
57
+ for (const [key, property] of entries) {
58
+ const target = property?._meta?.[CUSTOM_ANSWER]?.questionId;
59
+ if (typeof target === 'string' && target !== key && schema.properties[target] && property.type === 'string') companions.set(target, key);
60
+ }
61
+ const companionKeys = new Set(companions.values());
62
+ const fields = [];
63
+ for (const [key, property] of entries) {
64
+ if (companionKeys.has(key)) continue;
65
+ const parsed = field(property);
66
+ if (!parsed) return null;
67
+ fields.push({ key, schema: property, required: required.has(key), companion: companions.get(key), ...parsed });
68
+ }
69
+ if (!fields.length || fields.length > MAX_QUESTIONS) return null;
70
+ const single = fields.length === 1;
71
+ // The host question card has no form-level text, so the message leads the first question.
72
+ const questions = fields.map((f, index) => {
73
+ const title = text(f.schema.title, 120), description = text(f.schema.description, 2_000);
74
+ const own = single ? description : description ?? title ?? f.key;
75
+ let question = index === 0 ? (own ? `${message}\n\n${own}` : message) : own;
76
+ if (f.kind === 'multi') question += '\n\n(可多选;aibo 目前每题只能选择一项)';
77
+ return {
78
+ id: f.key, header: title, question,
79
+ options: (f.options ?? []).map(({ label, description }) => ({ label, description })),
80
+ isOther: !f.options || f.companion !== undefined,
81
+ };
82
+ });
83
+
84
+ function answer(answers) {
85
+ if (!answers || typeof answers !== 'object' || Object.keys(answers).some(id => !fields.some(f => f.key === id))) throw pluginError('invalid_input', 'Answer names an unknown question');
86
+ const content = {};
87
+ for (const f of fields) {
88
+ const values = answers[f.key];
89
+ const value = Array.isArray(values) && values.length === 1 && typeof values[0] === 'string' ? values[0].trim() : Array.isArray(values) && values.length === 0 ? '' : null;
90
+ if (value === null) throw pluginError('invalid_input', `Question ${f.key} takes one answer`);
91
+ if (!value) { if (f.required) throw pluginError('invalid_input', `Question ${f.key} is required`); continue; }
92
+ const option = f.options?.find(candidate => candidate.label === value);
93
+ if (option) { content[f.key] = f.kind === 'multi' ? [option.value] : option.value; continue; }
94
+ if (f.companion) {
95
+ // Typed text answers through the companion field; a required select still needs a pick.
96
+ if (f.required) throw pluginError('invalid_input', `Question ${f.key} requires one of its options`);
97
+ content[f.companion] = value; continue;
98
+ }
99
+ if (f.options) throw pluginError('invalid_input', `Question ${f.key} requires one of its options`);
100
+ if (f.kind === 'text') { content[f.key] = checkText(f, value); continue; }
101
+ content[f.key] = checkNumber(f, value);
102
+ }
103
+ return { action: 'accept', content };
104
+ }
105
+ return { title: single ? null : message, questions, answer };
106
+ }
107
+
108
+ function checkText(f, value) {
109
+ const s = f.schema, invalid = () => pluginError('invalid_input', `Question ${f.key} answer does not match its format`);
110
+ if ((s.minLength !== undefined && value.length < s.minLength) || (s.maxLength !== undefined && value.length > s.maxLength)) throw invalid();
111
+ if (s.format === 'email' && !/^[^\s@]+@[^\s@]+$/.test(value)) throw invalid();
112
+ if (s.format === 'uri' && !URL.canParse(value)) throw invalid();
113
+ if (s.format === 'date' && !/^\d{4}-\d{2}-\d{2}$/.test(value)) throw invalid();
114
+ if (s.format === 'date-time' && Number.isNaN(Date.parse(value))) throw invalid();
115
+ return value;
116
+ }
117
+
118
+ function checkNumber(f, value) {
119
+ const s = f.schema, number = Number(value);
120
+ if (!/^-?\d+(\.\d+)?(e[+-]?\d+)?$/i.test(value) || !Number.isFinite(number) || (f.kind === 'integer' && !Number.isInteger(number))
121
+ || (s.minimum !== undefined && number < s.minimum) || (s.maximum !== undefined && number > s.maximum)) {
122
+ throw pluginError('invalid_input', `Question ${f.key} needs ${f.kind === 'integer' ? 'an integer' : 'a number'}${s.minimum !== undefined || s.maximum !== undefined ? ` in range ${s.minimum ?? '-∞'}–${s.maximum ?? '∞'}` : ''}`);
123
+ }
124
+ return number;
125
+ }
@@ -0,0 +1,2 @@
1
+ /** Validates host image descriptors and returns ACP image content blocks. `label` names the agent in errors. */
2
+ export declare function imageInput(attachments?: readonly unknown[], supported?: boolean, label?: string): { type: 'image'; mimeType: string; data: string }[];
@@ -0,0 +1,34 @@
1
+ import { openSync, fstatSync, readSync, closeSync, constants } from 'node:fs';
2
+ import { isAbsolute } from 'node:path';
3
+ const MAX_IMAGE = 10 * 1024 * 1024, MAX_TOTAL = 20 * 1024 * 1024;
4
+ const invalid = message => Object.assign(new Error(message), { kind:'invalid_input' });
5
+ /** Only host-expanded image descriptors contain paths; ordinary references stay in prompt text. */
6
+ /** `label` names the agent in error messages. */
7
+ export function imageInput(attachments = [], supported = false, label = 'ACP') {
8
+ if (!Array.isArray(attachments)) throw invalid(`Invalid ${label} attachments`);
9
+ const images = attachments.filter(item => {
10
+ if (!item || typeof item.attachmentId !== 'string' || (item.type !== undefined && item.type !== 'image')) throw invalid(`Invalid ${label} attachment descriptor`);
11
+ return item.type === 'image';
12
+ });
13
+ if (images.length && !supported) throw Object.assign(new Error(`This ${label} CLI does not advertise image input`), { kind:'unsupported' });
14
+ if (images.length > 8) throw invalid('At most 8 images can be sent together');
15
+ let total = 0;
16
+ return images.map(item => {
17
+ if (typeof item.path !== 'string' || !isAbsolute(item.path) || !['image/png','image/jpeg','image/gif','image/webp'].includes(item.mimeType)) throw invalid(`Invalid ${label} image descriptor`);
18
+ const fd = openSync(item.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
19
+ try {
20
+ const stat = fstatSync(fd);
21
+ if (!stat.isFile() || !stat.size || stat.size > MAX_IMAGE || (total += stat.size) > MAX_TOTAL) throw invalid('Image size limit exceeded (10 MiB per image, 20 MiB total)');
22
+ const bytes = Buffer.alloc(stat.size);
23
+ let offset = 0;
24
+ while (offset < bytes.length) { const count = readSync(fd, bytes, offset, bytes.length-offset, offset); if (!count) throw invalid('Image changed while reading'); offset += count; }
25
+ if (fstatSync(fd).size !== stat.size) throw invalid('Image changed while reading');
26
+ const valid = item.mimeType === 'image/png' ? bytes.subarray(0,8).equals(Buffer.from('89504e470d0a1a0a','hex'))
27
+ : item.mimeType === 'image/jpeg' ? bytes.subarray(0,3).equals(Buffer.from('ffd8ff','hex'))
28
+ : item.mimeType === 'image/gif' ? ['GIF87a','GIF89a'].includes(bytes.subarray(0,6).toString())
29
+ : bytes.subarray(0,4).toString() === 'RIFF' && bytes.subarray(8,12).toString() === 'WEBP';
30
+ if (!valid) throw invalid('Image bytes do not match the declared media type');
31
+ return { type:'image', mimeType:item.mimeType, data:bytes.toString('base64') };
32
+ } finally { closeSync(fd); }
33
+ });
34
+ }
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './session.js';
2
+ export * from './transport.js';
3
+ export * from './config.js';
4
+ export * from './image-input.js';
package/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ export { AcpSession, BASE_CAPABILITIES, pluginError, object, bounded } from './session.mjs';
2
+ export { AcpTransport } from './transport.mjs';
3
+ export { modelParameters, selectValues } from './config.mjs';
4
+ export { imageInput } from './image-input.mjs';
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@aibolabs/acp-adapter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.8",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/chldu2000/aibo.git",
8
+ "directory": "packages/acp-adapter"
9
+ },
10
+ "type": "module",
11
+ "files": [
12
+ "index.mjs",
13
+ "index.d.ts",
14
+ "session.mjs",
15
+ "session.d.ts",
16
+ "transport.mjs",
17
+ "transport.d.ts",
18
+ "config.mjs",
19
+ "config.d.ts",
20
+ "image-input.mjs",
21
+ "image-input.d.ts",
22
+ "elicitation.mjs",
23
+ "worker.mjs",
24
+ "worker.d.ts",
25
+ "README.md"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./index.d.ts",
30
+ "import": "./index.mjs"
31
+ },
32
+ "./session": {
33
+ "types": "./session.d.ts",
34
+ "import": "./session.mjs"
35
+ },
36
+ "./transport": {
37
+ "types": "./transport.d.ts",
38
+ "import": "./transport.mjs"
39
+ },
40
+ "./config": {
41
+ "types": "./config.d.ts",
42
+ "import": "./config.mjs"
43
+ },
44
+ "./image-input": {
45
+ "types": "./image-input.d.ts",
46
+ "import": "./image-input.mjs"
47
+ },
48
+ "./worker": {
49
+ "types": "./worker.d.ts",
50
+ "import": "./worker.mjs"
51
+ }
52
+ },
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "dependencies": {
57
+ "@aibolabs/capability-runtime": "0.1.8"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "registry": "https://registry.npmjs.org/"
62
+ }
63
+ }
package/session.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { AcpTransport } from './transport.js';
2
+
3
+ export declare const BASE_CAPABILITIES: readonly string[];
4
+ export declare function pluginError(kind: string, message: string): Error & { kind: string };
5
+ export declare function object(value: unknown): Record<string, any>;
6
+ export declare function bounded(value: unknown, max?: number): string;
7
+
8
+ /** The narrow session surface extension hooks may use. */
9
+ export type AcpSessionHooks = {
10
+ readonly label: string;
11
+ readonly sessionId: string | null | undefined;
12
+ readonly turnId: string | null | undefined;
13
+ readonly subagents: Map<string, Record<string, any>>;
14
+ event(type: string, payload: unknown, correlation?: unknown): void;
15
+ respond(id: string | number, result: unknown): void;
16
+ /** Registers a pending interaction; answers `cancelled` and returns false once 32 are waiting. */
17
+ await(requestId: string, rpcId: string | number, interaction:
18
+ | { kind: 'question'; answer(answers: unknown): unknown; [key: string]: unknown }
19
+ | { kind: string; approve(decision: 'accept' | 'cancel'): unknown; [key: string]: unknown }): boolean;
20
+ updateSubagent(subagent: Record<string, any>, changes?: Record<string, any>): void;
21
+ };
22
+
23
+ export type AcpExtension = {
24
+ /** Agent name used in error messages; the transport uses `${label} ACP`. */
25
+ label: string;
26
+ command: string;
27
+ args?: string[];
28
+ clientName?: string;
29
+ clientMeta?: Record<string, unknown>;
30
+ /** When set, the agent must advertise it and `authenticate` runs before new/load. */
31
+ authMethodId?: string;
32
+ recoverySchema: string;
33
+ /** Namespace of `extension.updated` events for unrecognized updates. */
34
+ namespace: string;
35
+ requestPrefix?: string;
36
+ /** Candidate capabilities, narrowed by native negotiation. Declare user-input.respond only with a question handler. */
37
+ capabilities?: readonly string[];
38
+ /** Native mode that performs writes; only it may run write-authorized turns. */
39
+ writableMode: string;
40
+ /** All native modes that may run write-authorized turns (for example Manual and Auto); defaults to `[writableMode]`. */
41
+ writableModes?: readonly string[];
42
+ persistsEmptySessions?: boolean;
43
+ /** SDK 0.1.7: parameters are discovered after selecting a model. */
44
+ parameterScope?: 'current-model';
45
+ validateExecutionProfile(profile: unknown, permissions: readonly string[]): { mode: string; profile: Record<string, any> };
46
+ commandCategory?(command: Record<string, any>): string;
47
+ parameterized?(config: Record<string, any>, result: Record<string, any>): boolean;
48
+ /** Approval events carry `options` and replies select one by `optionId` (approval.respond option variant). */
49
+ approvalOptions?: boolean;
50
+ /**
51
+ * Native options shown with a host label, optionally scoped to an ACP tool kind. An option with
52
+ * `sessionControl` is offered even outside writable modes; the host commits that control before the
53
+ * agent is answered, and the matching `current_mode_update` to `mode` is adopted with `profile`.
54
+ * Any other native mode change during a turn fails the turn. `contextReset` marks options after
55
+ * which the agent continues in a fresh context under the same native session.
56
+ */
57
+ /** Declare ACP form elicitation and ask its questions through `user-input.respond`. */
58
+ elicitation?: boolean;
59
+ approvalChoices?: readonly { optionId: string; toolKind?: string; label?: string; sessionControl?: string; contextReset?: boolean; mode?: string; profile?: Record<string, unknown> }[];
60
+ /** The agent exposes parameters per model: claim reasoning and context-window even when the current model has none. */
61
+ parameterizedPicker?: boolean;
62
+ subagentFromTool?(update: Record<string, any>): { name: string; task: string; activity: string; [key: string]: unknown } | null;
63
+ handleRequest?(session: AcpSessionHooks, message: Record<string, any>, params: Record<string, any>, requestId: string): boolean;
64
+ handleNotification?(session: AcpSessionHooks, message: Record<string, any>): boolean;
65
+ };
66
+
67
+ export declare class AcpSession {
68
+ constructor(options: { extension: AcpExtension; transportFactory?: (options: { cwd: string }) => AcpTransport; emit?: (event: unknown) => void; pluginVersion?: string; cancelGraceMs?: number; commandWaitMs?: number });
69
+ readonly sessionId: string | null | undefined;
70
+ hostToolsRegistered?: boolean;
71
+ open(options: { mode: 'create' | 'resume'; workspaceId: string; workspacePath: string; executionProfile: unknown; recovery?: unknown; permissions: readonly string[]; mcpServers?: unknown[]; hostMcpTools?: { providerIdentifier: string; toolName: string }[] }): Promise<{ nativeSessionId: string; recovery: unknown; capabilities: string[] }>;
72
+ prompt(options: { text: string; turnId: string; attachments?: unknown[]; additionalInstructions?: string; writable?: boolean }): Promise<{ status: 'completed' | 'interrupted' | 'failed'; recovery: unknown }>;
73
+ cancel(): Promise<{ accepted: true }>;
74
+ respondApproval(requestId: string, answer: 'accept' | 'cancel' | { optionId: string }): { resolved: true; recovery: unknown; capabilities: string[] };
75
+ respondUserInput(requestId: string, answers: unknown): { resolved: true; recovery: unknown; capabilities: string[] };
76
+ capabilities(): string[];
77
+ commands(): Promise<{ commands: Record<string, any>[] }>;
78
+ configure(kind: 'reasoning' | 'context', input: Record<string, any>): Promise<Record<string, any>>;
79
+ models(input: Record<string, any>): Promise<Record<string, any>>;
80
+ snapshot(): { nativeSessionId: string; recovery: unknown; capabilities: string[] };
81
+ recovery(): unknown;
82
+ close(): Promise<{ accepted: true }>;
83
+ }