@anureo/adapter-ahp 0.2.0 → 0.3.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.
- package/README.md +35 -29
- package/examples/reference.d.ts +12 -0
- package/examples/reference.js +242 -0
- package/package.json +15 -16
- package/src/index.d.ts +118 -0
- package/src/index.js +360 -0
- package/src/websocket.d.ts +23 -0
- package/src/websocket.js +126 -0
- package/src/wire.d.ts +119 -0
- package/src/wire.js +288 -0
- package/dist/index.d.ts +0 -31
- package/dist/index.js +0 -406
- package/dist/websocket.d.ts +0 -7
- package/dist/wire.d.ts +0 -35
- package/examples/reference.mjs +0 -171
package/README.md
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
# @anureo/adapter-ahp
|
|
2
2
|
|
|
3
|
-
Adapter connecting the Anureo SDK to **external harnesses that
|
|
3
|
+
Adapter connecting the Anureo SDK to **external harnesses that implement AHP** over stdio or WebSocket. These harnesses can themselves be built with the Anureo SDK.
|
|
4
4
|
|
|
5
|
-
> Part of the [Anureo SDK workspace](
|
|
5
|
+
> Part of the [Anureo SDK workspace](../../../README.md). Install with `npm install @anureo/adapter-ahp`.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
+
The examples below use the revised workspace API, which differs from the published
|
|
10
|
+
0.2.0 API. The protocol major remains 1, but external peers must upgrade together
|
|
11
|
+
and reject mismatched lifecycle capabilities during initialization.
|
|
12
|
+
|
|
9
13
|
```typescript
|
|
10
|
-
import {
|
|
11
|
-
import { createAhpAdapter
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Or inject any transport you already own:
|
|
26
|
-
const customRuntime = createHarnessRuntime(createAhpAdapter({
|
|
27
|
-
transport: () => Promise.resolve(myTransport),
|
|
28
|
-
}))
|
|
14
|
+
import { createClient } from '@anureo/sdk'
|
|
15
|
+
import { createAhpAdapter } from '@anureo/adapter-ahp'
|
|
16
|
+
|
|
17
|
+
const client = await createClient({
|
|
18
|
+
adapter: createAhpAdapter({ command: 'my-harness', args: ['serve'] }),
|
|
19
|
+
identity: { adapterId: 'ahp', runtimeConfigId: 'local' },
|
|
20
|
+
})
|
|
21
|
+
try {
|
|
22
|
+
const session = await client.sessions.create({ cwd: process.cwd() })
|
|
23
|
+
const turn = await session.submit({ input: 'Inspect the workspace.' })
|
|
24
|
+
console.log(await turn.result())
|
|
25
|
+
await session.close()
|
|
26
|
+
} finally {
|
|
27
|
+
await client.dispose()
|
|
28
|
+
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
A
|
|
31
|
+
A standalone revised-v1 reference harness lives in [examples/reference.ts](examples/reference.ts); its stdio and WebSocket paths are exercised by the adapter tests.
|
|
32
32
|
|
|
33
33
|
## Behavior the adapter enforces
|
|
34
34
|
|
|
35
|
-
- **Version handshake.** `initialize` negotiates
|
|
35
|
+
- **Version handshake.** `initialize` negotiates major 1 and requires `protocolContract: "native-lifecycle"` in both directions; every request and result is validated against the protocol schema before use.
|
|
36
36
|
- **Sequence integrity.** Events carry `(sessionId, streamId, seq)`. A gap in the sequence is repaired only when the harness negotiated `x-anureo/event-replay@1` (via `x-anureo/events/read`); otherwise the stream fails with `SOURCE_SEQUENCE_GAP` instead of presenting a hole as complete history.
|
|
37
37
|
- **Session metadata.** Delivered through the negotiated `x-anureo/session-metadata@1` extension; the adapter always requests it and ignores duplicated metadata from other channels.
|
|
38
|
-
- **Interactions.** `interaction/requested` is answered through
|
|
38
|
+
- **Interactions.** `interaction/requested` is answered through `interaction/respond` / `interaction/cancel`; the adapter forwards the answer over the wire and surfaces remote resolutions (`interaction/resolved`) without double-answering.
|
|
39
39
|
- **Unknown notifications.** Native notifications outside the AHP event set are converted to safe `notice` events with the raw (redacted) payload attached — a future-proofing harness cannot crash the host.
|
|
40
40
|
- **Connection loss.** A closed transport fails every open session sink and rejects pending responses with `outcomeUnknown`-carrying errors; no session continues silently on a dead wire.
|
|
41
41
|
|
|
@@ -51,14 +51,20 @@ A minimal, dependency-free reference harness lives in [examples/reference.ts](ex
|
|
|
51
51
|
## Development
|
|
52
52
|
|
|
53
53
|
```sh
|
|
54
|
-
cd packages/ahp
|
|
54
|
+
cd packages/adapter/ahp
|
|
55
55
|
bun run typecheck
|
|
56
56
|
bun test tests
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The workspace's `bun run verify:pack` runs real stdio conversations against the reference harness, covering text turns, rejected approvals, and interrupts.
|
|
60
|
-
|
|
61
59
|
## Documentation
|
|
62
60
|
|
|
63
|
-
- [AHP v1 protocol](
|
|
64
|
-
- [Architecture](
|
|
61
|
+
- [AHP v1 protocol](../../../spec/ahp-v1.md) — the wire contract
|
|
62
|
+
- [Architecture](../../../docs/architecture.md)
|
|
63
|
+
|
|
64
|
+
## AHP v1 lifecycle revision
|
|
65
|
+
|
|
66
|
+
The lifecycle design updates AHP v1 and package root entries in place.
|
|
67
|
+
Root migration and local validation are complete: the root implementation and tests
|
|
68
|
+
use the revised lifecycle, and parallel versioned entries have been removed.
|
|
69
|
+
Local conformance and clean-install checks have passed.
|
|
70
|
+
See the [design and implementation plan](../../../docs/native-harness-lifecycle.md).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a scripted AHP peer for stdio or an embedding transport. The supplied send callback
|
|
3
|
+
* receives JSON-RPC frames without line delimiters. receive permits concurrent requests and awaits
|
|
4
|
+
* its reply write. Execution events can continue afterward. Stop admitting requests before close
|
|
5
|
+
* and await outstanding opens if their clients also need to be included in disposal.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createReferenceHarness(send: (message: string) => void | Promise<void>): {
|
|
8
|
+
/** Accept one frame and await its reply write. Independent callers can receive concurrently. */
|
|
9
|
+
receive: (frame: string) => Promise<void>;
|
|
10
|
+
/** Attempt all owned client disposals before rejecting the first release failure. */
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** A standalone AHP v1 peer backed by a scripted native execution. No model calls. */
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { createClient, HarnessSdkError } from '@anureo/sdk';
|
|
5
|
+
import { AHP_PROTOCOL_CONTRACT, assertRequest, isAhpMethod, negotiateVersion, supportsExtension } from '@anureo/protocol';
|
|
6
|
+
/**
|
|
7
|
+
* Create a scripted AHP peer for stdio or an embedding transport. The supplied send callback
|
|
8
|
+
* receives JSON-RPC frames without line delimiters. receive permits concurrent requests and awaits
|
|
9
|
+
* its reply write. Execution events can continue afterward. Stop admitting requests before close
|
|
10
|
+
* and await outstanding opens if their clients also need to be included in disposal.
|
|
11
|
+
*/
|
|
12
|
+
export function createReferenceHarness(send) {
|
|
13
|
+
const adapter = new ReferenceAdapter();
|
|
14
|
+
// The harness retains and disposes every client when it closes.
|
|
15
|
+
const ownedClients = new Set();
|
|
16
|
+
const openSessions = new Map();
|
|
17
|
+
// Replay logs outlive session closure and remain for the harness lifetime.
|
|
18
|
+
const replayLogs = new Map();
|
|
19
|
+
let initialized = false;
|
|
20
|
+
/** openSession and publishSessionEvents await the send. Record the source sequence first so it is available to concurrent replay. */
|
|
21
|
+
const publish = async (event) => {
|
|
22
|
+
const source = openSessions.get(event.sessionId);
|
|
23
|
+
if (!source)
|
|
24
|
+
return;
|
|
25
|
+
event = { ...event, streamId: source.streamId, seq: ++source.lastPublishedSeq };
|
|
26
|
+
if (event.type === 'session/metadata')
|
|
27
|
+
event = { ...event, type: 'extension', payload: { name: 'x-anureo/session-metadata', version: '1', data: event.payload } };
|
|
28
|
+
const log = replayLogs.get(event.sessionId) ?? [];
|
|
29
|
+
log.push(event);
|
|
30
|
+
if (log.length > 2048)
|
|
31
|
+
log.shift();
|
|
32
|
+
replayLogs.set(event.sessionId, log);
|
|
33
|
+
await send(JSON.stringify({ jsonrpc: '2.0', method: event.type, params: event }));
|
|
34
|
+
};
|
|
35
|
+
/** Drain events in order; a read/send rejection stops forwarding and is suppressed by openSession. */
|
|
36
|
+
const publishSessionEvents = async (reader) => {
|
|
37
|
+
for (;;) {
|
|
38
|
+
const next = await reader.next();
|
|
39
|
+
if (next.done)
|
|
40
|
+
return;
|
|
41
|
+
await publish(next.value);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/** Own the client before opening; publish initial state before starting the ongoing event drain. */
|
|
45
|
+
const openSession = async (params) => {
|
|
46
|
+
if (openSessions.has(params.binding.sessionId))
|
|
47
|
+
throw new HarnessSdkError('SESSION_ALREADY_OPEN');
|
|
48
|
+
const client = await createClient({ adapter, identity: params.binding, interactions: { approval: undefined } });
|
|
49
|
+
ownedClients.add(client);
|
|
50
|
+
const session = await client.openSession(params);
|
|
51
|
+
const streamId = randomUUID();
|
|
52
|
+
const state = session.snapshot;
|
|
53
|
+
openSessions.set(session.id, { session, streamId, lastPublishedSeq: 0 });
|
|
54
|
+
const reader = session.events()[Symbol.asyncIterator]();
|
|
55
|
+
await publish({ sessionId: session.id, streamId, seq: 1, timestamp: new Date().toISOString(), type: 'session/state', payload: state });
|
|
56
|
+
void publishSessionEvents(reader).catch(() => { });
|
|
57
|
+
return { binding: session.binding, streamId, state, capabilities: session.capabilities };
|
|
58
|
+
};
|
|
59
|
+
/** invoke returns a complete replay range or reports SOURCE_STREAM_UNAVAILABLE, including when old entries were evicted. */
|
|
60
|
+
const readEvents = ({ sessionId, streamId, afterSeq, throughSeq }) => {
|
|
61
|
+
const events = (replayLogs.get(sessionId) ?? []).filter(event => event.streamId === streamId && event.seq > afterSeq && event.seq <= throughSeq);
|
|
62
|
+
if (events.length !== throughSeq - afterSeq)
|
|
63
|
+
throw new HarnessSdkError('SOURCE_STREAM_UNAVAILABLE');
|
|
64
|
+
return { events };
|
|
65
|
+
};
|
|
66
|
+
/** Enforce handshake/session admission and return SDK results; receive converts failures to RPC errors. */
|
|
67
|
+
const invoke = async (method, params) => {
|
|
68
|
+
if (method === 'initialize') {
|
|
69
|
+
assertRequest(method, params);
|
|
70
|
+
const result = await adapter.initialize(params);
|
|
71
|
+
initialized = true;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (!initialized)
|
|
75
|
+
throw new HarnessSdkError('NOT_INITIALIZED');
|
|
76
|
+
if (method === 'session/open') {
|
|
77
|
+
assertRequest(method, params);
|
|
78
|
+
return openSession(params);
|
|
79
|
+
}
|
|
80
|
+
if (!params || typeof params !== 'object' || !('sessionId' in params) || typeof params.sessionId !== 'string')
|
|
81
|
+
throw new HarnessSdkError('INVALID_PARAMS');
|
|
82
|
+
if (method === 'x-anureo/events/read') {
|
|
83
|
+
return readEvents(params);
|
|
84
|
+
}
|
|
85
|
+
const source = openSessions.get(params.sessionId);
|
|
86
|
+
if (!source)
|
|
87
|
+
throw new HarnessSdkError('SESSION_NOT_FOUND');
|
|
88
|
+
if (!isAhpMethod(method) || method === 'initialize' || method === 'session/open')
|
|
89
|
+
throw new HarnessSdkError('CAPABILITY_UNSUPPORTED');
|
|
90
|
+
assertRequest(method, params);
|
|
91
|
+
const result = await source.session.request(method, params);
|
|
92
|
+
if (method === 'session/close')
|
|
93
|
+
openSessions.delete(params.sessionId);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
/** Accept one frame and await its reply write. Independent callers can receive concurrently. */
|
|
98
|
+
receive: createReceiver(send, invoke),
|
|
99
|
+
/** Attempt all owned client disposals before rejecting the first release failure. */
|
|
100
|
+
async close() {
|
|
101
|
+
const outcomes = await Promise.allSettled([...ownedClients].map(client => client.dispose()));
|
|
102
|
+
const failed = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
103
|
+
if (failed)
|
|
104
|
+
throw failed.reason;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decode one JSON-RPC frame per call and invoke the harness dispatcher. Ignore initialized and
|
|
110
|
+
* send parse/envelope errors directly. Dispatch or result-write failures attempt an RPC error reply.
|
|
111
|
+
* An unusable envelope or failed error write can reject the returned receive promise.
|
|
112
|
+
*/
|
|
113
|
+
function createReceiver(send, invoke) {
|
|
114
|
+
return async function receive(frame) {
|
|
115
|
+
let message;
|
|
116
|
+
try {
|
|
117
|
+
message = JSON.parse(frame);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
await sendRpcError(send, null, { code: -32700, message: 'Parse error' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (message.method === 'initialized' && message.id === undefined)
|
|
124
|
+
return;
|
|
125
|
+
if (message.jsonrpc !== '2.0' || typeof message.method !== 'string' || message.id === undefined) {
|
|
126
|
+
await sendRpcError(send, message.id ?? null, { code: -32600, message: 'Invalid request' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const id = message.id;
|
|
131
|
+
const result = await invoke(message.method, message.params);
|
|
132
|
+
await send(JSON.stringify({ jsonrpc: '2.0', id, result }));
|
|
133
|
+
}
|
|
134
|
+
catch (cause) {
|
|
135
|
+
const code = cause && typeof cause === 'object' && 'code' in cause
|
|
136
|
+
? String(cause.code)
|
|
137
|
+
: 'HARNESS_ERROR';
|
|
138
|
+
await sendRpcError(send, message.id, {
|
|
139
|
+
code: -32070,
|
|
140
|
+
message: code,
|
|
141
|
+
data: { code, retryable: false, outcomeUnknown: false },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Write the correlated error frame for receive and let serialization or transport failure escape. */
|
|
147
|
+
function sendRpcError(send, id, error) {
|
|
148
|
+
return send(JSON.stringify({ jsonrpc: '2.0', id, error }));
|
|
149
|
+
}
|
|
150
|
+
const capabilities = {
|
|
151
|
+
input: { delivery: ['native'], interrupt: { targets: ['turn'], reportsPendingInputs: false } },
|
|
152
|
+
turn: { inputModalities: ['text'] }, interactions: ['approval'],
|
|
153
|
+
extensions: [{ name: 'x-anureo/event-replay', version: '1' }, { name: 'x-anureo/session-metadata', version: '1' }],
|
|
154
|
+
};
|
|
155
|
+
/** Scripted native backend used by the reference peer; each open owns its active execution slot. */
|
|
156
|
+
class ReferenceAdapter {
|
|
157
|
+
/**
|
|
158
|
+
* The harness dispatcher and SDK wrapper both negotiate this scripted backend. Reject invalid
|
|
159
|
+
* parameters or versions and retain title support for later opens once a caller advertises it.
|
|
160
|
+
*/
|
|
161
|
+
async initialize(params) {
|
|
162
|
+
assertRequest('initialize', params);
|
|
163
|
+
negotiateVersion(params.protocolVersions);
|
|
164
|
+
this.metadataEnabled ||= supportsExtension(params.clientCapabilities ?? {}, 'x-anureo/session-metadata', '1');
|
|
165
|
+
return { protocolVersion: 1, protocolContract: AHP_PROTOCOL_CONTRACT, runtime: { name: 'reference', version: '1', adapter: 'reference/1' }, capabilities };
|
|
166
|
+
}
|
|
167
|
+
/** OutputSession.open adopts this backend, which owns one scripted execution and publishes through the supplied sink. */
|
|
168
|
+
async openSession(params, sink) {
|
|
169
|
+
let active;
|
|
170
|
+
let closed = false;
|
|
171
|
+
const binding = { ...params.binding, nativeRef: { provider: 'reference', value: params.binding.sessionId } };
|
|
172
|
+
const metadataEnabled = this.metadataEnabled;
|
|
173
|
+
/** Submit starts this in the background. Recheck active ownership after approval so interrupted work skips output. */
|
|
174
|
+
const runExecution = async (turnId, text) => {
|
|
175
|
+
if (text === '/approval')
|
|
176
|
+
await sink.requestInteraction({ kind: 'approval', interactionId: `approval:${turnId}`, turnId, options: [{ id: 'allow', label: 'Allow once', semantic: 'allowOnce' }, { id: 'deny', label: 'Deny', semantic: 'deny' }] }, async () => { });
|
|
177
|
+
if (closed || active !== turnId)
|
|
178
|
+
return;
|
|
179
|
+
await sink.emit({ type: 'item/started', payload: { item: { id: 'message', type: 'agentMessage', text: '' } } }, { turnId });
|
|
180
|
+
await sink.emit({ type: 'item/completed', payload: { item: { id: 'message', type: 'agentMessage', text, phase: 'final' } } }, { turnId });
|
|
181
|
+
active = undefined;
|
|
182
|
+
await sink.emit({ type: 'turn/completed', payload: { turnId, status: 'completed' } });
|
|
183
|
+
};
|
|
184
|
+
/** Convert background failure into a terminal only while this execution still owns the slot. */
|
|
185
|
+
const handleExecutionFailure = async (turnId) => {
|
|
186
|
+
if (!closed && active === turnId) {
|
|
187
|
+
active = undefined;
|
|
188
|
+
await sink.emit({ type: 'turn/completed', payload: { turnId, status: 'failed', error: { code: 'SCRIPTED_EXECUTION_FAILED', message: 'Scripted execution failed', retryable: false, outcomeUnknown: false } } });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
binding, capabilities,
|
|
193
|
+
/**
|
|
194
|
+
* OutputSession dispatches one scripted operation here. Submit returns a dispatch receipt,
|
|
195
|
+
* interrupt waits for the active turn's terminal, and close confirms observation.
|
|
196
|
+
* Busy, mismatched and unsupported requests reject.
|
|
197
|
+
*/
|
|
198
|
+
async execute(method, value) {
|
|
199
|
+
if (method === 'session/close') {
|
|
200
|
+
return { closed: true, observationClosed: true, executionStopped: 'unconfirmed' };
|
|
201
|
+
}
|
|
202
|
+
if (method === 'turn/interrupt') {
|
|
203
|
+
const { turnId } = value;
|
|
204
|
+
if (active !== turnId)
|
|
205
|
+
throw new HarnessSdkError('TURN_MISMATCH');
|
|
206
|
+
active = undefined;
|
|
207
|
+
await sink.emit({ type: 'turn/completed', payload: { turnId, status: 'interrupted' } });
|
|
208
|
+
return { accepted: true, turnId };
|
|
209
|
+
}
|
|
210
|
+
if (method !== 'input/submit')
|
|
211
|
+
throw new HarnessSdkError('CAPABILITY_UNSUPPORTED');
|
|
212
|
+
if (active)
|
|
213
|
+
throw new HarnessSdkError('SESSION_BUSY', 'The scripted backend admits one execution at a time');
|
|
214
|
+
const input = value;
|
|
215
|
+
const turnId = randomUUID();
|
|
216
|
+
active = turnId;
|
|
217
|
+
await sink.emit({ type: 'turn/started', payload: { turnId } });
|
|
218
|
+
await sink.bindSubmission(input.submissionId, turnId);
|
|
219
|
+
const text = input.input.filter(block => block.type === 'text').map(block => block.text).join('\n');
|
|
220
|
+
if (metadataEnabled && text.startsWith('/title ')) {
|
|
221
|
+
await sink.emit({ type: 'session/metadata', payload: { title: text.slice(7) || null } });
|
|
222
|
+
}
|
|
223
|
+
if (text !== '/wait')
|
|
224
|
+
void runExecution(turnId, text).catch(() => handleExecutionFailure(turnId));
|
|
225
|
+
return { submissionId: input.submissionId, disposition: 'dispatched' };
|
|
226
|
+
},
|
|
227
|
+
/** Mark closed so executions waiting for approval skip output; the SDK owns observation teardown. */
|
|
228
|
+
async dispose() { closed = true; },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Session handles own all scripted state, so client disposal has no shared native resource to release. */
|
|
232
|
+
async dispose() { }
|
|
233
|
+
/** Enable metadata cumulatively during negotiation. Each open snapshots the current flag. */
|
|
234
|
+
metadataEnabled = false;
|
|
235
|
+
}
|
|
236
|
+
if (import.meta.main) {
|
|
237
|
+
const harness = createReferenceHarness(message => { process.stdout.write(message + '\n'); });
|
|
238
|
+
const input = createInterface({ input: process.stdin });
|
|
239
|
+
for await (const line of input)
|
|
240
|
+
void harness.receive(line);
|
|
241
|
+
await harness.close();
|
|
242
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anureo/adapter-ahp",
|
|
3
|
-
"version": "0.2.0",
|
|
4
3
|
"description": "Anureo SDK adapter for external AHP harnesses over stdio or WebSocket",
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"anureo",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "git+https://github.com/hi-youichi/anureo-sdk.git",
|
|
18
|
-
"directory": "packages/ahp"
|
|
18
|
+
"directory": "packages/adapter/ahp"
|
|
19
19
|
},
|
|
20
20
|
"bugs": {
|
|
21
21
|
"url": "https://github.com/hi-youichi/anureo-sdk/issues"
|
|
@@ -26,24 +26,23 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"types": "./
|
|
30
|
-
"import": "./
|
|
31
|
-
"default": "./
|
|
29
|
+
"types": "./src/index.d.ts",
|
|
30
|
+
"import": "./src/index.js",
|
|
31
|
+
"default": "./src/index.js"
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
"files": [
|
|
35
|
-
"dist",
|
|
36
|
-
"schema",
|
|
37
|
-
"protocol",
|
|
38
|
-
"examples",
|
|
39
|
-
"README.md",
|
|
40
|
-
"LICENSE"
|
|
41
|
-
],
|
|
42
34
|
"dependencies": {
|
|
43
|
-
"@anureo/protocol": "0.
|
|
44
|
-
"@anureo/sdk": "0.
|
|
35
|
+
"@anureo/protocol": "0.3.0",
|
|
36
|
+
"@anureo/sdk": "0.3.0"
|
|
45
37
|
},
|
|
46
38
|
"engines": {
|
|
47
39
|
"node": ">=22"
|
|
48
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"src",
|
|
43
|
+
"bin",
|
|
44
|
+
"schema",
|
|
45
|
+
"protocol",
|
|
46
|
+
"examples"
|
|
47
|
+
]
|
|
49
48
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Negotiates a remote AHP peer and routes its validated streams into SDK session sinks. */
|
|
2
|
+
import { type BackendSession, type HarnessAdapter, type SessionEventSink } from "@anureo/sdk";
|
|
3
|
+
import { type InitializeParams, type InitializeResult, type SessionOpenParams } from "@anureo/protocol";
|
|
4
|
+
import { createStdioTransport, type AhpTransport, type WireOptions } from "./wire.js";
|
|
5
|
+
export { AhpWire, createStdioTransport, type AhpTransport } from "./wire.js";
|
|
6
|
+
export { createWebSocketTransport } from "./websocket.js";
|
|
7
|
+
/**
|
|
8
|
+
* Configure one adapter-owned connection. A supplied transport takes precedence over a URL, which
|
|
9
|
+
* takes precedence over a command. Wire limits and timeouts apply to the selected connection.
|
|
10
|
+
* The adapter owns closing the selected transport.
|
|
11
|
+
*/
|
|
12
|
+
export interface AhpAdapterOptions extends WireOptions {
|
|
13
|
+
/** Supply a connection or a factory invoked once on the first valid initialize call. */
|
|
14
|
+
transport?: AhpTransport | (() => Promise<AhpTransport>);
|
|
15
|
+
/** Executable used when neither transport nor URL is supplied. */
|
|
16
|
+
command?: string;
|
|
17
|
+
/** Arguments passed to the stdio executable. Omission supplies no arguments. */
|
|
18
|
+
args?: string[];
|
|
19
|
+
/** Environment passed to the stdio process factory. The default factory inherits when omitted. */
|
|
20
|
+
env?: Record<string, string | undefined>;
|
|
21
|
+
/** Working directory for the stdio process. Omission uses the host working directory. */
|
|
22
|
+
cwd?: string;
|
|
23
|
+
/** A URL requires an explicit connect factory. */
|
|
24
|
+
url?: string;
|
|
25
|
+
/** Open the URL connection. The adapter owns closing the returned transport. */
|
|
26
|
+
connect?: (url: string) => Promise<AhpTransport>;
|
|
27
|
+
/** Override stdio process creation. Transport cleanup calls the returned process's dispose method. */
|
|
28
|
+
processFactory?: Parameters<typeof createStdioTransport>[0]["processFactory"];
|
|
29
|
+
}
|
|
30
|
+
/** Create an adapter without I/O. Pass it to createClient and dispose the resulting client. */
|
|
31
|
+
export declare function createAhpAdapter(options: AhpAdapterOptions): HarnessAdapter;
|
|
32
|
+
/**
|
|
33
|
+
* Connect SDK sessions to one AHP peer. Direct users await initialize before opening sessions,
|
|
34
|
+
* release their backend handles, and then await adapter disposal.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AhpAdapter implements HarnessAdapter {
|
|
37
|
+
private readonly options;
|
|
38
|
+
/** Configure the connection without opening it. */
|
|
39
|
+
constructor(options: AhpAdapterOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Negotiate once and send initialized before resolving. Valid calls share the first attempt,
|
|
42
|
+
* including rejection. Invalid params or unsupported offered versions throw synchronously.
|
|
43
|
+
* Disposal, transport and handshake failures reject. The SDK's output wrapper awaits this result
|
|
44
|
+
* and uses the negotiated output capability before Client stores the capabilities.
|
|
45
|
+
*/
|
|
46
|
+
initialize(params: InitializeParams): Promise<InitializeResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Open a remote session. OutputSession.open supplies the sink, which may receive notifications
|
|
49
|
+
* before the open reply. Resolve with the validated backend handle after any fallback metadata
|
|
50
|
+
* publication. Duplicate IDs, RPC/schema failures, invalid bindings, retired streams and sink
|
|
51
|
+
* failures reject. Failed setup removes local routing. The SDK wrapper owns backend disposal.
|
|
52
|
+
*/
|
|
53
|
+
openSession(params: SessionOpenParams, sink: SessionEventSink): Promise<BackendSession>;
|
|
54
|
+
/**
|
|
55
|
+
* Close the owned connection and clear routing after successful close. The SDK wrapper attempts
|
|
56
|
+
* session cleanup first. The first disposal awaits wire cleanup and can reject. Later calls return
|
|
57
|
+
* immediately. If acquisition is pending, initialize closes the arriving transport before rejecting.
|
|
58
|
+
*/
|
|
59
|
+
dispose(): Promise<void>;
|
|
60
|
+
private wire?;
|
|
61
|
+
private initialized?;
|
|
62
|
+
private result?;
|
|
63
|
+
private readonly sessions;
|
|
64
|
+
private readonly retiredStreams;
|
|
65
|
+
private readonly inFlightInteractionResponses;
|
|
66
|
+
private disposed;
|
|
67
|
+
/**
|
|
68
|
+
* initialize shares this attempt. Attach wire callbacks before sending the handshake, then store
|
|
69
|
+
* capabilities for notification handling before notifying the peer of readiness. Acquisition,
|
|
70
|
+
* validation and write failures reject the shared attempt. Adapter disposal owns wire cleanup.
|
|
71
|
+
*/
|
|
72
|
+
private initializeConnection;
|
|
73
|
+
/**
|
|
74
|
+
* Select one transport for initializeConnection. Keep a supplied instance synchronous so the
|
|
75
|
+
* wire starts immediately. Factory promises and selection errors flow to the initialization attempt.
|
|
76
|
+
*/
|
|
77
|
+
private selectTransport;
|
|
78
|
+
/** Commit the open reply's stream after checking its identity. A throw leaves openSession to remove routing. */
|
|
79
|
+
private bindOpenedStream;
|
|
80
|
+
/** Build the backend that openSession transfers to OutputSession after setup and metadata publication. */
|
|
81
|
+
private createBackendSession;
|
|
82
|
+
/** AhpWire awaits this on close. Fail sinks in registration order before rejecting response waiters. */
|
|
83
|
+
private failConnection;
|
|
84
|
+
/**
|
|
85
|
+
* AhpWire serializes this callback while continuing to read RPC replies. Ignore duplicate
|
|
86
|
+
* sequences and retired streams, repair gaps before delivery, and forward unknown methods as
|
|
87
|
+
* notices. Rejection starts wire cleanup, which invokes failConnection.
|
|
88
|
+
*/
|
|
89
|
+
private notification;
|
|
90
|
+
/**
|
|
91
|
+
* Fill the prefix immediately before the live event. notification awaits each repaired event before
|
|
92
|
+
* delivering the live one. Replayed events can already have reached the sink when a later entry
|
|
93
|
+
* proves the range invalid or incomplete and rejects the callback.
|
|
94
|
+
*/
|
|
95
|
+
private replayMissingEvents;
|
|
96
|
+
/**
|
|
97
|
+
* Admit one source event from notification or replayMissingEvents. Advance its sequence before
|
|
98
|
+
* awaiting publication, so replay checks the admitted sequence. Await sink operations except the
|
|
99
|
+
* interaction answer, whose confirmation can itself arrive on this notification queue.
|
|
100
|
+
*/
|
|
101
|
+
private deliver;
|
|
102
|
+
/** Decode either metadata channel for deliver. Reject unnegotiated versions or invalid payloads before publication. */
|
|
103
|
+
private readMetadata;
|
|
104
|
+
/**
|
|
105
|
+
* Register the sink's responder when deliver receives a native interaction. The sink owns local
|
|
106
|
+
* publication and settlement. Observe rejection without blocking the notification queue.
|
|
107
|
+
*/
|
|
108
|
+
private registerInteraction;
|
|
109
|
+
/**
|
|
110
|
+
* The SDK invokes the responder on answer or cancellation, including rejection at admission.
|
|
111
|
+
* Await the first RPC reply or matching resolution, validate it, and reject unknown outcomes.
|
|
112
|
+
* The SDK publishes local settlement. Release both registrations on exit and leave any outstanding
|
|
113
|
+
* RPC under the wire's own deadline and close handling.
|
|
114
|
+
*/
|
|
115
|
+
private respondToInteraction;
|
|
116
|
+
/** Backend disposal removes its own registration and remembers up to 128 streams to ignore late events. */
|
|
117
|
+
private retire;
|
|
118
|
+
}
|