@anureo/adapter-hermes 0.0.0-stage → 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/src/events.js ADDED
@@ -0,0 +1,274 @@
1
+ import { HarnessSdkError } from '@anureo/sdk';
2
+ import { identity, integer, json, object, protocolError, stableId, text } from "./native.js";
3
+ /**
4
+ * Publish cumulative session usage for HermesSession.apply and terminal turn projection.
5
+ * Omit nonfinite or negative counters and await sink publication.
6
+ */
7
+ export async function sessionUsage(sink, payload) {
8
+ const usage = { provider: 'hermes', source: 'native', scope: 'session' };
9
+ const fields = { input: 'input', output: 'output', total: 'total', context_max: 'contextWindow', context_used: 'contextUsed' };
10
+ for (const [native, target] of Object.entries(fields)) {
11
+ const value = payload[native];
12
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
13
+ usage[target] = value;
14
+ }
15
+ await sink.emit({ type: 'usage/updated', payload: usage });
16
+ }
17
+ /**
18
+ * Internal execution projection created by HermesSession.turn. Publish ordered SDK items through
19
+ * its sink, retaining bounded provisional state until native terminal evidence arrives. Native
20
+ * snapshots complete items; deltas only describe provisional progress. Sink failures reject to the
21
+ * session's receipt or event path; the session owns recovery and channel disposal.
22
+ */
23
+ export class HermesTurn {
24
+ /** SDK identity shared by submission bindings, events and interrupt targeting. */
25
+ id;
26
+ /** Durable native admission used to reconcile this execution after reconnect. */
27
+ admissionId;
28
+ /** Native authority paired with generation; HermesSession checks both when reusing this execution. */
29
+ epoch;
30
+ /** Native execution generation within this authority, used to fence interrupt requests. */
31
+ generation;
32
+ /** Set before publishing turn/started to suppress duplicate starts during association and event delivery. */
33
+ started = false;
34
+ /** Set before turn/completed publication. HermesSession uses it to stop treating this turn as active. */
35
+ terminal = false;
36
+ /** Set with terminal evidence before turn/completed publication, and retained after item content is released. */
37
+ status;
38
+ /** Accept validated native execution identity and derive a local turn id stable across replay. */
39
+ constructor({ admissionId, epoch, generation, sessionId, sink, options }) {
40
+ this.admissionId = admissionId;
41
+ this.epoch = epoch;
42
+ this.generation = generation;
43
+ this.sink = sink;
44
+ this.options = options;
45
+ this.id = `hermes-${stableId(sessionId, admissionId)}`;
46
+ }
47
+ /** Publish turn/started at most once, before any binding or item publication. */
48
+ async start() {
49
+ if (this.started)
50
+ return;
51
+ this.started = true;
52
+ await this.sink.emit({ type: 'turn/started', payload: { turnId: this.id, nativeTurnId: this.admissionId } });
53
+ }
54
+ /** HermesSession.associate binds native-claimed input once; late binding of a terminal turn adds no user item. */
55
+ async bind(submissionId, input) {
56
+ await this.start();
57
+ if (this.submissions.has(submissionId))
58
+ return;
59
+ this.submissions.add(submissionId);
60
+ await this.sink.bindSubmission(submissionId, this.id);
61
+ if (this.terminal)
62
+ return;
63
+ // Native admission/claim is the evidence that this input belongs to this execution.
64
+ await this.complete({ id: `${this.id}:user:${stableId(submissionId)}`, type: 'userMessage', content: [{ type: 'text', text: input }], status: 'completed' });
65
+ }
66
+ /** Report incomplete progress once and stop appending to the current partial segment; native snapshots can still replace its text. */
67
+ async gap() {
68
+ if (this.terminal || this.gapReported)
69
+ return;
70
+ this.gapReported = true;
71
+ await this.start();
72
+ if (this.current)
73
+ this.current.incomplete = true;
74
+ await this.complete({ id: `${this.id}:gap`, type: 'notice', severity: 'warning', code: 'HERMES_PROGRESS_GAP',
75
+ text: 'Some intermediate Hermes progress was unavailable; the final answer requires native terminal evidence.', status: 'completed' });
76
+ }
77
+ /** Project the event supplied by HermesSession.applyTurnEvent. Ignore terminal replays and reject malformed payloads or sink failures. */
78
+ async apply(event) {
79
+ if (this.terminal)
80
+ return;
81
+ await this.start();
82
+ switch (event.type) {
83
+ case 'message.delta':
84
+ await this.messageDelta(event.payload);
85
+ return;
86
+ case 'message.interim':
87
+ await this.messageInterim(event.payload);
88
+ return;
89
+ case 'reasoning.delta':
90
+ await this.reasoningDelta(event.payload);
91
+ return;
92
+ case 'tool.start':
93
+ case 'tool.complete':
94
+ await this.toolEvent(event.type, event.payload);
95
+ return;
96
+ case 'message.complete': await this.messageComplete(event.payload);
97
+ }
98
+ }
99
+ /**
100
+ * HermesSession.associate supplies receipt-only terminal evidence after reconnect or submit.
101
+ * Mark unavailable intermediate progress, then publish the final response; unsupported outcomes or
102
+ * non-string final_response reject as protocol errors. Missing final_response means empty text.
103
+ */
104
+ async fromReceipt(result, outcome) {
105
+ if (this.terminal)
106
+ return;
107
+ const status = outcome === 'completed' ? 'completed' : outcome === 'interrupted' || outcome === 'cancelled' ? 'interrupted' : outcome === 'failed' ? 'failed' : undefined;
108
+ if (!status || result.final_response !== undefined && typeof result.final_response !== 'string')
109
+ throw protocolError();
110
+ await this.gap();
111
+ await this.finish(status, typeof result.final_response === 'string' ? result.final_response : '', result);
112
+ }
113
+ items = new Map();
114
+ completed = new Set();
115
+ submissions = new Set();
116
+ segment = 0;
117
+ current;
118
+ lastText = '';
119
+ reasoning = '';
120
+ bytes = 0;
121
+ gapReported = false;
122
+ /** Replace streamed commentary with the native snapshot and seal the segment unless it is stale. */
123
+ async messageInterim(payload) {
124
+ const body = text(payload.text);
125
+ if (payload.segment !== undefined && integer(payload.segment) < this.segment)
126
+ return;
127
+ if (!this.current)
128
+ this.current = { item: { id: `${this.id}:message:${this.segment}`, type: 'agentMessage', text: body, phase: 'commentary' }, incomplete: false };
129
+ else
130
+ this.current.item = { ...this.current.item, text: body };
131
+ await this.commentary();
132
+ }
133
+ /** Retain the accumulated reasoning summary while publishing only the new chunk as a delta. */
134
+ async reasoningDelta(payload) {
135
+ const chunk = text(payload.text);
136
+ this.reasoning += chunk;
137
+ const item = { id: `${this.id}:reasoning`, type: 'reasoning', summary: this.reasoning };
138
+ if (!this.items.has(item.id))
139
+ await this.put({ ...item, summary: '' });
140
+ await this.put(item);
141
+ await this.sink.emit({ type: 'item/delta', payload: { itemId: item.id, kind: 'reasoningSummary', text: chunk } }, { turnId: this.id });
142
+ }
143
+ /**
144
+ * Project both tool phases under one native identity. Ignore duplicate starts, retain prior input
145
+ * when completion omits it, and convert native duration seconds to SDK milliseconds.
146
+ */
147
+ async toolEvent(type, payload) {
148
+ const id = `${this.id}:tool:${identity(payload.tool_id)}`;
149
+ if (type === 'tool.start' && this.items.has(id))
150
+ return;
151
+ const name = identity(payload.name);
152
+ const prior = this.items.get(id);
153
+ const item = { ...(prior?.type === 'toolCall' ? prior : {}), id, type: 'toolCall', kind: 'other',
154
+ title: name, toolName: name, nativeType: name,
155
+ ...(payload.args !== undefined ? { input: json(payload.args) } : {}) };
156
+ if (type === 'tool.start') {
157
+ await this.put(item);
158
+ return;
159
+ }
160
+ await this.complete({ ...item, status: 'completed',
161
+ ...(payload.result !== undefined ? { output: json(payload.result) } : typeof payload.result_text === 'string' ? { output: payload.result_text } : {}),
162
+ ...(typeof payload.duration_s === 'number' && payload.duration_s >= 0 ? { durationMs: payload.duration_s * 1000 } : {}),
163
+ });
164
+ }
165
+ /** Require a recognized terminal status. Use retained text only when both native text fields are absent or null. */
166
+ async messageComplete(payload) {
167
+ const status = payload.status === 'complete' ? 'completed' : payload.status === 'interrupted' ? 'interrupted' : payload.status === 'error' ? 'failed' : undefined;
168
+ if (!status)
169
+ throw protocolError();
170
+ const value = payload.text ?? payload.content;
171
+ if (value != null && typeof value !== 'string')
172
+ throw protocolError();
173
+ await this.finish(status, value == null ? this.current?.item.text ?? this.lastText : value, payload);
174
+ }
175
+ /** Retain a detached item within count/byte budgets and publish item/started only on first appearance. */
176
+ async put(item) {
177
+ const previous = this.items.get(item.id);
178
+ const bytes = Buffer.byteLength(JSON.stringify(item));
179
+ const total = this.bytes - (previous ? Buffer.byteLength(JSON.stringify(previous)) : 0) + bytes;
180
+ if (total > (this.options.maxBufferedBytes ?? 16 * 1024 * 1024) || !previous && this.items.size >= 2048)
181
+ throw new HarnessSdkError('BACKPRESSURE_EXCEEDED');
182
+ this.items.set(item.id, structuredClone(item));
183
+ this.bytes = total;
184
+ if (!previous)
185
+ await this.sink.emit({ type: 'item/started', payload: { item: { ...item, status: 'running' } } }, { turnId: this.id });
186
+ }
187
+ /** Publish item completion at most once per id, starting an unseen item first. */
188
+ async complete(item) {
189
+ if (this.completed.has(item.id))
190
+ return;
191
+ await this.put(item);
192
+ this.completed.add(item.id);
193
+ await this.sink.emit({ type: 'item/completed', payload: { item } }, { turnId: this.id });
194
+ }
195
+ /** Seal the current commentary segment and retain its text as the fallback when a terminal frame omits text. */
196
+ async commentary() {
197
+ if (!this.current)
198
+ return;
199
+ this.lastText = this.current.item.text;
200
+ await this.complete({ ...this.current.item, phase: 'commentary', status: 'completed' });
201
+ this.current = undefined;
202
+ this.segment++;
203
+ }
204
+ /** Reconcile UTF-16 text offsets within a commentary segment; conflicting overlap rejects and a missing prefix marks a gap. */
205
+ async messageDelta(payload) {
206
+ if (payload.text === null) {
207
+ await this.commentary();
208
+ return;
209
+ }
210
+ let chunk = text(payload.text);
211
+ if (payload.segment !== undefined) {
212
+ const next = integer(payload.segment);
213
+ if (next < this.segment)
214
+ return;
215
+ if (next > this.segment) {
216
+ await this.commentary();
217
+ this.segment = next;
218
+ }
219
+ }
220
+ if (!this.current) {
221
+ this.current = { item: { id: `${this.id}:message:${this.segment}`, type: 'agentMessage', text: '', phase: 'commentary' }, incomplete: false };
222
+ await this.put(this.current.item);
223
+ }
224
+ const current = this.current;
225
+ if (current.incomplete)
226
+ return;
227
+ const offset = payload.offset === undefined ? current.item.text.length : integer(payload.offset);
228
+ if (offset > current.item.text.length) {
229
+ await this.gap();
230
+ return;
231
+ }
232
+ if (offset < current.item.text.length) {
233
+ const overlap = Math.min(chunk.length, current.item.text.length - offset);
234
+ if (current.item.text.slice(offset, offset + overlap) !== chunk.slice(0, overlap))
235
+ throw protocolError();
236
+ chunk = chunk.slice(overlap);
237
+ }
238
+ if (!chunk)
239
+ return;
240
+ current.item = { ...current.item, text: current.item.text + chunk };
241
+ await this.put(current.item);
242
+ await this.sink.emit({ type: 'item/delta', payload: { itemId: current.item.id, kind: 'text', text: chunk } }, { turnId: this.id });
243
+ }
244
+ /** Complete final/reasoning items before terminal publication, flag unfinished native items and release retained content after success. */
245
+ async finish(status, body, payload) {
246
+ await this.start();
247
+ const id = this.current?.item.id ?? `${this.id}:final`;
248
+ await this.complete({ id, type: 'agentMessage', text: body, phase: 'final', status: status === 'completed' ? 'completed' : status });
249
+ if (typeof payload.reasoning === 'string')
250
+ this.reasoning = payload.reasoning;
251
+ if (this.reasoning)
252
+ await this.complete({ id: `${this.id}:reasoning`, type: 'reasoning', summary: this.reasoning, status: 'completed' });
253
+ for (const item of this.items.values())
254
+ if (!this.completed.has(item.id)) {
255
+ await this.gap();
256
+ await this.complete({ ...item, extensions: { ...item.extensions, nativeResultUnavailable: true } });
257
+ }
258
+ if (payload.usage != null)
259
+ await sessionUsage(this.sink, object(payload.usage));
260
+ this.terminal = true;
261
+ this.status = status;
262
+ await this.sink.emit({ type: 'turn/completed', payload: { turnId: this.id, status,
263
+ ...(status === 'failed' ? { error: { code: 'HERMES_TURN_FAILED', message: 'Hermes turn failed', retryable: false, outcomeUnknown: false } } : {}) } });
264
+ // Core retains completed results; the mapper retains only identity and control evidence.
265
+ this.items.clear();
266
+ this.completed.clear();
267
+ this.current = undefined;
268
+ this.reasoning = '';
269
+ this.lastText = '';
270
+ this.bytes = 0;
271
+ }
272
+ sink;
273
+ options;
274
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { type HarnessAdapter, type SessionEventSink } from '@anureo/sdk';
2
+ import { type InitializeParams, type InitializeResult, type SessionOpenParams } from '@anureo/protocol';
3
+ import { HermesSession } from "./session.js";
4
+ import { type HermesAdapterOptions } from "./options.js";
5
+ export type { HermesAdapterOptions } from "./options.js";
6
+ /**
7
+ * Connect SDK clients to a shared Hermes gateway owner. Pass this adapter to createClient, which
8
+ * initializes and disposes it through the SDK's output wrapper. Direct port consumers must await
9
+ * initialize before opening sessions and dispose the adapter when finished; disposal ends local
10
+ * observation without stopping the shared owner or its executions.
11
+ */
12
+ export declare class HermesAdapter implements HarnessAdapter {
13
+ /** Configuration for subsequent native work. Argument, toolset and environment containers are copied at construction. */
14
+ readonly options: HermesAdapterOptions;
15
+ /** Validate and copy configuration without native I/O; invalid options throw INVALID_PARAMS. */
16
+ constructor(options?: HermesAdapterOptions);
17
+ /** Return 'http' to OutputSession.open for a new editor-capable session, or undefined when the bridge is unavailable. */
18
+ outputBridgeTransport(params: SessionOpenParams): 'http' | undefined;
19
+ /**
20
+ * Negotiate the SDK protocol and describe the ticketed Hermes owner without creating a session.
21
+ * Cache the first native probe promise, including failure, and release its channel after description.
22
+ * Request validation/version errors throw synchronously; disposed adapters return a rejected promise,
23
+ * and attachment, description or concurrent disposal failures reject the probe.
24
+ */
25
+ initialize(params: InitializeParams): Promise<InitializeResult>;
26
+ /**
27
+ * Open a new or resumed native observation after initialization. OutputSession.open supplies the
28
+ * forwarding sink and owns the returned backend; callbacks can publish before this resolves.
29
+ * Reject invalid requests, unsupported options, binding mismatches or setup failure, disposing
30
+ * partial sessions first. Direct port consumers own cleanup of the returned observation.
31
+ */
32
+ openSession(params: SessionOpenParams, sink: SessionEventSink): Promise<HermesSession>;
33
+ /**
34
+ * Abort attachment and close local channels without stopping native work. The first call awaits
35
+ * tracked session disposal. To observe completion of an in-flight initialization, await its own
36
+ * promise, which can remain pending while the attach child is disposed. Later disposal calls return immediately.
37
+ */
38
+ dispose(): Promise<void>;
39
+ private initialized?;
40
+ private profileId?;
41
+ private editorMcp;
42
+ private disposed;
43
+ private readonly abort;
44
+ private probe?;
45
+ private readonly sessions;
46
+ /**
47
+ * Own the temporary channel for initialize. Ignore probe events and reject server requests once
48
+ * the channel is available. Publish the profile and editor support only after a validated
49
+ * description, then close the probe on either outcome. Description failures have a known outcome
50
+ * because the probe never submits work.
51
+ */
52
+ private probeRuntime;
53
+ /** Reject unsupported features and foreign resume bindings before openSession acquires resources. */
54
+ private assertSupportedOpen;
55
+ }
56
+ /**
57
+ * Create a Hermes adapter for createClient. Configuration is validated synchronously and invalid
58
+ * options throw INVALID_PARAMS. Native attachment starts on initialization. Dispose the returned
59
+ * client when finished. createClient releases the adapter if client initialization fails.
60
+ *
61
+ * @example
62
+ * With the Hermes CLI on PATH and a configured default profile:
63
+ *
64
+ * ```ts
65
+ * import { createHermesAdapter } from '@anureo/adapter-hermes'
66
+ * import { createClient } from '@anureo/sdk'
67
+ *
68
+ * const client = await createClient({
69
+ * adapter: createHermesAdapter(),
70
+ * identity: { adapterId: 'hermes', runtimeConfigId: 'default' },
71
+ * })
72
+ * try {
73
+ * const session = await client.sessions.create({ cwd: process.cwd() })
74
+ * const turn = await session.submit({ input: 'Summarize this workspace.' })
75
+ * console.log(await turn.result())
76
+ * } finally {
77
+ * await client.dispose()
78
+ * }
79
+ * ```
80
+ */
81
+ export declare function createHermesAdapter(options?: HermesAdapterOptions): HermesAdapter;
package/src/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import { HarnessSdkError } from '@anureo/sdk';
2
+ import { AHP_PROTOCOL_CONTRACT, assertRequest, negotiateVersion } from '@anureo/protocol';
3
+ import { attachGateway } from "./attach.js";
4
+ import { describe } from "./native.js";
5
+ import { HermesRpc } from "./rpc.js";
6
+ import { HermesSession, hermesCapabilities } from "./session.js";
7
+ import { validateOptions } from "./options.js";
8
+ /**
9
+ * Connect SDK clients to a shared Hermes gateway owner. Pass this adapter to createClient, which
10
+ * initializes and disposes it through the SDK's output wrapper. Direct port consumers must await
11
+ * initialize before opening sessions and dispose the adapter when finished; disposal ends local
12
+ * observation without stopping the shared owner or its executions.
13
+ */
14
+ export class HermesAdapter {
15
+ /** Configuration for subsequent native work. Argument, toolset and environment containers are copied at construction. */
16
+ options;
17
+ /** Validate and copy configuration without native I/O; invalid options throw INVALID_PARAMS. */
18
+ constructor(options = {}) {
19
+ try {
20
+ validateOptions(options);
21
+ }
22
+ catch {
23
+ throw new HarnessSdkError('INVALID_PARAMS', 'Invalid Hermes adapter options');
24
+ }
25
+ this.options = { ...options, ...(options.toolsets ? { toolsets: [...options.toolsets] } : {}), ...(options.args ? { args: [...options.args] } : {}), ...(options.env ? { env: { ...options.env } } : {}) };
26
+ }
27
+ /** Return 'http' to OutputSession.open for a new editor-capable session, or undefined when the bridge is unavailable. */
28
+ outputBridgeTransport(params) {
29
+ return this.editorMcp && params.mode === 'new' ? 'http' : undefined;
30
+ }
31
+ /**
32
+ * Negotiate the SDK protocol and describe the ticketed Hermes owner without creating a session.
33
+ * Cache the first native probe promise, including failure, and release its channel after description.
34
+ * Request validation/version errors throw synchronously; disposed adapters return a rejected promise,
35
+ * and attachment, description or concurrent disposal failures reject the probe.
36
+ */
37
+ initialize(params) {
38
+ if (this.disposed)
39
+ return Promise.reject(new HarnessSdkError('RUNTIME_DISPOSED'));
40
+ assertRequest('initialize', params);
41
+ negotiateVersion(params.protocolVersions);
42
+ return this.initialized ??= this.probeRuntime();
43
+ }
44
+ /**
45
+ * Open a new or resumed native observation after initialization. OutputSession.open supplies the
46
+ * forwarding sink and owns the returned backend; callbacks can publish before this resolves.
47
+ * Reject invalid requests, unsupported options, binding mismatches or setup failure, disposing
48
+ * partial sessions first. Direct port consumers own cleanup of the returned observation.
49
+ */
50
+ async openSession(params, sink) {
51
+ if (this.disposed)
52
+ throw new HarnessSdkError('RUNTIME_DISPOSED');
53
+ if (!this.profileId)
54
+ throw new HarnessSdkError('NOT_INITIALIZED');
55
+ assertRequest('session/open', params);
56
+ this.assertSupportedOpen(params);
57
+ const session = new HermesSession({ params, sink, options: this.options, profileId: this.profileId, released: () => this.sessions.delete(session) });
58
+ this.sessions.add(session);
59
+ try {
60
+ await session.open();
61
+ if (this.disposed)
62
+ throw new HarnessSdkError('RUNTIME_DISPOSED');
63
+ return session;
64
+ }
65
+ catch (cause) {
66
+ await session.dispose();
67
+ throw cause;
68
+ }
69
+ }
70
+ /**
71
+ * Abort attachment and close local channels without stopping native work. The first call awaits
72
+ * tracked session disposal. To observe completion of an in-flight initialization, await its own
73
+ * promise, which can remain pending while the attach child is disposed. Later disposal calls return immediately.
74
+ */
75
+ async dispose() {
76
+ if (this.disposed)
77
+ return;
78
+ this.disposed = true;
79
+ this.abort.abort();
80
+ this.probe?.dispose();
81
+ await Promise.all([...this.sessions].map(value => value.dispose()));
82
+ this.sessions.clear();
83
+ }
84
+ initialized;
85
+ profileId;
86
+ editorMcp = false;
87
+ disposed = false;
88
+ abort = new AbortController();
89
+ probe;
90
+ sessions = new Set();
91
+ /**
92
+ * Own the temporary channel for initialize. Ignore probe events and reject server requests once
93
+ * the channel is available. Publish the profile and editor support only after a validated
94
+ * description, then close the probe on either outcome. Description failures have a known outcome
95
+ * because the probe never submits work.
96
+ */
97
+ async probeRuntime() {
98
+ const grant = await attachGateway(this.options, this.options.startup ?? 'start', this.abort.signal);
99
+ let channel;
100
+ channel = await HermesRpc.connect(grant, {
101
+ event: async () => { },
102
+ request: async (request) => { await channel?.reject(request.id); },
103
+ failure: () => { },
104
+ }, this.options, this.abort.signal);
105
+ if (this.disposed) {
106
+ channel.dispose();
107
+ throw new HarnessSdkError('RUNTIME_DISPOSED');
108
+ }
109
+ this.probe = channel;
110
+ try {
111
+ const description = describe(await channel.request('runtime.describe'), grant);
112
+ if (this.disposed)
113
+ throw new HarnessSdkError('RUNTIME_DISPOSED');
114
+ this.profileId = description.profileId;
115
+ this.editorMcp = description.editorMcp;
116
+ return {
117
+ protocolVersion: 1,
118
+ protocolContract: AHP_PROTOCOL_CONTRACT,
119
+ runtime: { name: 'hermes', version: grant.version, adapter: 'hermes-gateway/0.2.0' },
120
+ capabilities: hermesCapabilities(),
121
+ };
122
+ }
123
+ catch (cause) {
124
+ if (cause instanceof HarnessSdkError)
125
+ throw new HarnessSdkError(cause.code, cause.message, cause.retryable, false);
126
+ throw new HarnessSdkError('PROTOCOL_ERROR', 'Invalid Hermes gateway description');
127
+ }
128
+ finally {
129
+ channel.dispose();
130
+ if (this.probe === channel)
131
+ this.probe = undefined;
132
+ }
133
+ }
134
+ /** Reject unsupported features and foreign resume bindings before openSession acquires resources. */
135
+ assertSupportedOpen(params) {
136
+ if (params.mode === 'fork' || params.instructions !== undefined || params.permissionMode !== undefined
137
+ || (params.mcpServers !== undefined && (!this.editorMcp || params.mode !== 'new'))
138
+ || Object.keys(params.config ?? {}).length)
139
+ throw new HarnessSdkError('CAPABILITY_UNSUPPORTED');
140
+ if (params.mode === 'resume' && params.binding.nativeRef?.provider !== 'hermes')
141
+ throw new HarnessSdkError('SESSION_BINDING_MISMATCH');
142
+ }
143
+ }
144
+ /**
145
+ * Create a Hermes adapter for createClient. Configuration is validated synchronously and invalid
146
+ * options throw INVALID_PARAMS. Native attachment starts on initialization. Dispose the returned
147
+ * client when finished. createClient releases the adapter if client initialization fails.
148
+ *
149
+ * @example
150
+ * With the Hermes CLI on PATH and a configured default profile:
151
+ *
152
+ * ```ts
153
+ * import { createHermesAdapter } from '@anureo/adapter-hermes'
154
+ * import { createClient } from '@anureo/sdk'
155
+ *
156
+ * const client = await createClient({
157
+ * adapter: createHermesAdapter(),
158
+ * identity: { adapterId: 'hermes', runtimeConfigId: 'default' },
159
+ * })
160
+ * try {
161
+ * const session = await client.sessions.create({ cwd: process.cwd() })
162
+ * const turn = await session.submit({ input: 'Summarize this workspace.' })
163
+ * console.log(await turn.result())
164
+ * } finally {
165
+ * await client.dispose()
166
+ * }
167
+ * ```
168
+ */
169
+ export function createHermesAdapter(options = {}) { return new HermesAdapter(options); }
@@ -0,0 +1,83 @@
1
+ import { type SessionEventSink } from '@anureo/sdk';
2
+ import { type NativeObject } from "./native.js";
3
+ import type { HermesAdapterOptions } from "./options.js";
4
+ import type { HermesRpc, ServerRequest } from "./rpc.js";
5
+ /**
6
+ * Internal interaction registry owned by HermesSession. Map replayed prompts and gateway requests
7
+ * to stable SDK interaction ids without blocking the serial reader on user answers. The two native
8
+ * mechanisms share this registry but retain their different acknowledgement semantics. The SDK
9
+ * Session owns handler cancellation and answer privacy. HermesSession owns channel cleanup and recovery.
10
+ */
11
+ export declare class HermesInteractions {
12
+ /** Capture callbacks supplied by HermesSession; resolve identity/channel lazily so pending answers use the recovered connection. */
13
+ constructor({ sessionId, rpc, sink, turnId, epoch, options }: {
14
+ sessionId: () => string;
15
+ rpc: () => HermesRpc;
16
+ sink: SessionEventSink;
17
+ /** Resolve a generation only in the session's current authority epoch. */
18
+ turnId: (generation: number) => string | undefined;
19
+ epoch: () => number;
20
+ options: HermesAdapterOptions;
21
+ });
22
+ /**
23
+ * Register an approval/clarification from HermesSession.apply or snapshot, ignoring replayed ids.
24
+ * Invalid prompts and a full registry reject registration. The sink can invoke the responder with
25
+ * an answer or null, which sends a denial or empty clarification. Delivery checks the current
26
+ * authority before sending once and requires a native ACK for the same prompt id. The sink
27
+ * observes delivery failures through the responder.
28
+ */
29
+ prompt(value: NativeObject): Promise<void>;
30
+ /**
31
+ * Register session-scoped srq requests from HermesSession's live or held traffic, or from snapshot.
32
+ * Reply with a native error for unsupported requests and reject malformed supported payloads.
33
+ * The sink owns the wait for answers. The responder resolves after local send, so snapshot checks
34
+ * whether a finished delivery still appears open remotely.
35
+ */
36
+ serverRequest(request: ServerRequest): Promise<void>;
37
+ /** Let deliveries already in flight settle before HermesSession.applyTurnEvent projects a terminal frame. The sink handles their outcomes. */
38
+ waitResponders(): Promise<void>;
39
+ /**
40
+ * Handle peer settlement from HermesSession.apply or withdrawal from snapshot. Match prompt
41
+ * generations to fence reused ids and leave in-flight delivery to its own settlement path.
42
+ * Publish an unknown outcome for a bare native resolved signal because it carries no answer.
43
+ * Sink publication failures reject to the caller.
44
+ */
45
+ settled(target: InteractionTarget, reason: string): Promise<void>;
46
+ /**
47
+ * HermesSession.snapshot reconciles open native questions before releasing held traffic. Withdraw
48
+ * missing questions and register new ones; reject RESULT_UNKNOWN if a locally finished delivery is
49
+ * still open remotely, preventing silent suppression or replay of a possibly delivered secret.
50
+ */
51
+ snapshot(prompts: unknown[], requests: unknown[]): Promise<void>;
52
+ private readonly pending;
53
+ /** Parse a nonempty clarify batch with distinct native question ids so one answer can be sent per id. */
54
+ private batchQuestions;
55
+ /** Normalize optional native choices; null, omission and an empty array all leave the question open-ended. */
56
+ private question;
57
+ /** Encode SDK answers for Hermes text fields; unanswered/null values become empty text and string selections are comma-separated. */
58
+ private answer;
59
+ /**
60
+ * Admit within registry budgets and register the responder with the SDK sink. Session invokes it
61
+ * for unsupported or closed requests, and InteractionRegistry invokes it for answers and
62
+ * cancellations. Mark delivery in flight before writing so native settlement can defer to it.
63
+ * Observe fulfillment and rejection as local completion without blocking the reader on the user.
64
+ */
65
+ private register;
66
+ private readonly sessionId;
67
+ private readonly rpc;
68
+ private readonly sink;
69
+ private readonly turnId;
70
+ private readonly epoch;
71
+ private readonly options;
72
+ }
73
+ /** Preserve the native identity needed to match settlement, including the generation for prompts. */
74
+ type InteractionTarget = {
75
+ wire: 'prompt';
76
+ nativeId: string;
77
+ generation: number;
78
+ } | {
79
+ wire: 'request';
80
+ nativeId: string;
81
+ generation?: never;
82
+ };
83
+ export {};