@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 +21 -0
- package/README.md +103 -2
- package/config.d.ts +8 -0
- package/config.mjs +25 -0
- package/elicitation.mjs +125 -0
- package/image-input.d.ts +2 -0
- package/image-input.mjs +34 -0
- package/index.d.ts +4 -0
- package/index.mjs +4 -0
- package/package.json +61 -4
- package/session.d.ts +83 -0
- package/session.mjs +721 -0
- package/transport.d.ts +15 -0
- package/transport.mjs +191 -0
- package/worker.d.ts +26 -0
- package/worker.mjs +253 -0
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
|
-
#
|
|
1
|
+
# @aibolabs/acp-adapter
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/elicitation.mjs
ADDED
|
@@ -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
|
+
}
|
package/image-input.d.ts
ADDED
package/image-input.mjs
ADDED
|
@@ -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
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibolabs/acp-adapter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|