afm-bridge 0.1.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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/client.d.ts +74 -0
- package/dist/client.js +216 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.js +152 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.d.ts +28 -0
- package/dist/node/index.js +177 -0
- package/dist/node/index.js.map +1 -0
- package/dist/rpc.d.ts +37 -0
- package/dist/rpc.js +172 -0
- package/dist/rpc.js.map +1 -0
- package/dist/schema.d.ts +53 -0
- package/dist/schema.js +149 -0
- package/dist/schema.js.map +1 -0
- package/dist/session.d.ts +63 -0
- package/dist/session.js +202 -0
- package/dist/session.js.map +1 -0
- package/dist/tauri/index.d.ts +29 -0
- package/dist/tauri/index.js +80 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/tool.d.ts +39 -0
- package/dist/tool.js +48 -0
- package/dist/tool.js.map +1 -0
- package/dist/transport.d.ts +37 -0
- package/dist/transport.js +24 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/zod/index.d.ts +21 -0
- package/dist/zod/index.js +44 -0
- package/dist/zod/index.js.map +1 -0
- package/package.json +79 -0
- package/src/client.ts +312 -0
- package/src/errors.ts +170 -0
- package/src/index.ts +22 -0
- package/src/node/index.ts +203 -0
- package/src/rpc.ts +204 -0
- package/src/schema.ts +206 -0
- package/src/session.ts +290 -0
- package/src/tauri/index.ts +100 -0
- package/src/tool.ts +80 -0
- package/src/transport.ts +60 -0
- package/src/types.ts +142 -0
- package/src/zod/index.ts +53 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AfmError,
|
|
3
|
+
ClientClosedError,
|
|
4
|
+
ModelUnavailableError,
|
|
5
|
+
ProtocolError,
|
|
6
|
+
RequestTimeoutError,
|
|
7
|
+
SessionLostError,
|
|
8
|
+
SidecarCrashedError,
|
|
9
|
+
} from './errors.js'
|
|
10
|
+
import { type RequestOptions, RpcPeer } from './rpc.js'
|
|
11
|
+
import { type DeltaParams, Session, type SessionHost } from './session.js'
|
|
12
|
+
import { runTool, type Tool, toolDefinition } from './tool.js'
|
|
13
|
+
import type { CloseInfo, Connection, Transport } from './transport.js'
|
|
14
|
+
import type { CallOptions, ModelId, ModelStatus, ServerInfo, SessionOptions } from './types.js'
|
|
15
|
+
|
|
16
|
+
/** Protocol version this client speaks. See PROTOCOL.md section 3. */
|
|
17
|
+
export const PROTOCOL_VERSION = 1
|
|
18
|
+
export const CLIENT_VERSION = '0.1.0'
|
|
19
|
+
|
|
20
|
+
export interface AppleFoundationModelsOptions {
|
|
21
|
+
/** How to reach the sidecar, e.g. `new NodeStdioTransport()` from `afm-bridge/node`. */
|
|
22
|
+
transport: Transport
|
|
23
|
+
/** Reported to the sidecar in `initialize` (shows up in its debug log). */
|
|
24
|
+
clientInfo?: { name: string; version: string }
|
|
25
|
+
/**
|
|
26
|
+
* Automatic restart after the sidecar crashes. The process is restarted on the next
|
|
27
|
+
* call; after `maxRestarts` crashes within `windowMs` calls fail with `SidecarCrashedError`
|
|
28
|
+
* until the window passes. Default: 3 restarts per 60 s.
|
|
29
|
+
*/
|
|
30
|
+
restart?: { maxRestarts?: number; windowMs?: number }
|
|
31
|
+
/** Timeout for starting the sidecar and the `initialize` handshake. Default 10 s. */
|
|
32
|
+
initializeTimeoutMs?: number
|
|
33
|
+
/** Default client-side timeout for every call. Default: none (use `AbortSignal`). */
|
|
34
|
+
requestTimeoutMs?: number
|
|
35
|
+
/** Receives every stderr line of the sidecar. */
|
|
36
|
+
onLog?: (line: string) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Live {
|
|
40
|
+
epoch: number
|
|
41
|
+
peer: RpcPeer
|
|
42
|
+
connection: Connection
|
|
43
|
+
info: ServerInfo
|
|
44
|
+
slot: Slot
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Per-process state that exists before the handshake finishes. */
|
|
48
|
+
interface Slot {
|
|
49
|
+
peer?: RpcPeer
|
|
50
|
+
intentional: boolean
|
|
51
|
+
exited?: CloseInfo
|
|
52
|
+
listeners: Map<number, (params: DeltaParams) => void>
|
|
53
|
+
/** Tools by session id, then by name. */
|
|
54
|
+
tools: Map<string, Map<string, Tool<unknown>>>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const LOG_TAIL = 20
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Entry point of afm-bridge. The sidecar starts lazily on the first call.
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* const afm = new AppleFoundationModels({ transport: new NodeStdioTransport() })
|
|
64
|
+
* const session = await afm.createSession({ instructions: 'Answer briefly.' })
|
|
65
|
+
* console.log(await session.respond('Hello'))
|
|
66
|
+
* await afm.close()
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export class AppleFoundationModels {
|
|
70
|
+
private readonly transport: Transport
|
|
71
|
+
private readonly options: AppleFoundationModelsOptions
|
|
72
|
+
private live: Live | undefined
|
|
73
|
+
private starting: Promise<Live> | undefined
|
|
74
|
+
private epoch = 0
|
|
75
|
+
private crashes: number[] = []
|
|
76
|
+
private closed = false
|
|
77
|
+
private readonly logTail: string[] = []
|
|
78
|
+
private readonly host: SessionHost
|
|
79
|
+
|
|
80
|
+
constructor(options: AppleFoundationModelsOptions) {
|
|
81
|
+
this.transport = options.transport
|
|
82
|
+
this.options = options
|
|
83
|
+
this.host = {
|
|
84
|
+
request: (epoch, method, params, opts) => this.requestInEpoch(epoch, method, params, opts),
|
|
85
|
+
listen: (epoch, requestId, listener) => {
|
|
86
|
+
const slot = this.live?.epoch === epoch ? this.live.slot : undefined
|
|
87
|
+
slot?.listeners.set(requestId, listener)
|
|
88
|
+
return () => slot?.listeners.delete(requestId)
|
|
89
|
+
},
|
|
90
|
+
disposed: (epoch, sessionId) => {
|
|
91
|
+
if (this.live?.epoch === epoch) this.live.slot.tools.delete(sessionId)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Handshake result: server version, macOS version and status of every model. Starts the sidecar. */
|
|
97
|
+
async serverInfo(): Promise<ServerInfo> {
|
|
98
|
+
return (await this.connect()).info
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fresh availability of a model. Never throws because the model is unavailable:
|
|
103
|
+
* check `available` and `reason` instead.
|
|
104
|
+
*/
|
|
105
|
+
async availability(model: ModelId = 'on-device', options: CallOptions = {}): Promise<ModelStatus> {
|
|
106
|
+
try {
|
|
107
|
+
return await this.request<ModelStatus>('model/status', { model }, options)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof ModelUnavailableError) {
|
|
110
|
+
return { available: false, reason: error.reason, message: error.message }
|
|
111
|
+
}
|
|
112
|
+
throw error
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Number of tokens `text` occupies in the model's context (macOS 26.4+). */
|
|
117
|
+
async tokenCount(text: string, options: CallOptions & { model?: ModelId } = {}): Promise<number> {
|
|
118
|
+
const { model, ...call } = options
|
|
119
|
+
const result = await this.request<{ tokenCount: number }>(
|
|
120
|
+
'model/tokenCount',
|
|
121
|
+
{ text, ...(model ? { model } : {}) },
|
|
122
|
+
call,
|
|
123
|
+
)
|
|
124
|
+
return result.tokenCount
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Creates a conversation. Throws `ModelUnavailableError` (with `reason`) when the model cannot be used. */
|
|
128
|
+
async createSession(options: SessionOptions = {}): Promise<Session> {
|
|
129
|
+
const live = await this.connect()
|
|
130
|
+
const { signal, tools, ...params } = options
|
|
131
|
+
const result = await this.requestInEpoch<{ sessionId: string; contextSize: number }>(
|
|
132
|
+
live.epoch,
|
|
133
|
+
'session/create',
|
|
134
|
+
tools?.length ? { ...params, tools: tools.map(toolDefinition) } : params,
|
|
135
|
+
{ signal },
|
|
136
|
+
)
|
|
137
|
+
if (tools?.length) live.slot.tools.set(result.sessionId, new Map(tools.map((t) => [t.name, t])))
|
|
138
|
+
return new Session(
|
|
139
|
+
this.host,
|
|
140
|
+
live.epoch,
|
|
141
|
+
result.sessionId,
|
|
142
|
+
result.contextSize,
|
|
143
|
+
options.model ?? 'on-device',
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Stops the sidecar. Pending calls reject with `ClientClosedError`. The client cannot be reused. */
|
|
148
|
+
async close(): Promise<void> {
|
|
149
|
+
if (this.closed) return
|
|
150
|
+
this.closed = true
|
|
151
|
+
const live = this.live ?? (await this.starting?.catch(() => undefined))
|
|
152
|
+
this.live = undefined
|
|
153
|
+
if (!live) return
|
|
154
|
+
live.slot.intentional = true
|
|
155
|
+
try {
|
|
156
|
+
await live.peer.request('shutdown', {}, { timeoutMs: 2000, cancelGraceMs: 0 })
|
|
157
|
+
} catch {
|
|
158
|
+
// The process is killed below anyway.
|
|
159
|
+
}
|
|
160
|
+
live.peer.close(new ClientClosedError('The afm-bridge client was closed'))
|
|
161
|
+
await live.connection.close()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MARK: - Internals
|
|
165
|
+
|
|
166
|
+
private async request<T>(method: string, params: unknown, options: CallOptions = {}): Promise<T> {
|
|
167
|
+
const live = await this.connect()
|
|
168
|
+
return this.requestInEpoch<T>(live.epoch, method, params, options)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private requestInEpoch<T>(
|
|
172
|
+
epoch: number,
|
|
173
|
+
method: string,
|
|
174
|
+
params: unknown,
|
|
175
|
+
options: RequestOptions = {},
|
|
176
|
+
): Promise<T> {
|
|
177
|
+
if (this.closed) return Promise.reject(new ClientClosedError('The afm-bridge client was closed'))
|
|
178
|
+
const live = this.live
|
|
179
|
+
if (!live || live.epoch !== epoch) {
|
|
180
|
+
return Promise.reject(
|
|
181
|
+
new SessionLostError(
|
|
182
|
+
'This session belonged to an afm-bridge-server process that is no longer running (it crashed or was restarted). Create a new session.',
|
|
183
|
+
{ type: 'sessionLost' },
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
return live.peer.request<T>(method, params, {
|
|
188
|
+
...options,
|
|
189
|
+
timeoutMs: options.timeoutMs ?? this.options.requestTimeoutMs,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private connect(): Promise<Live> {
|
|
194
|
+
if (this.closed) return Promise.reject(new ClientClosedError('The afm-bridge client was closed'))
|
|
195
|
+
if (this.live) return Promise.resolve(this.live)
|
|
196
|
+
if (!this.starting) {
|
|
197
|
+
this.starting = this.start().finally(() => {
|
|
198
|
+
this.starting = undefined
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
return this.starting
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async start(): Promise<Live> {
|
|
205
|
+
const { maxRestarts = 3, windowMs = 60_000 } = this.options.restart ?? {}
|
|
206
|
+
const now = Date.now()
|
|
207
|
+
this.crashes = this.crashes.filter((t) => now - t < windowMs)
|
|
208
|
+
if (this.crashes.length > maxRestarts) {
|
|
209
|
+
throw new SidecarCrashedError(
|
|
210
|
+
`afm-bridge-server crashed ${this.crashes.length} times within ${Math.round(windowMs / 1000)} s; not restarting it again yet.${this.formatLogTail()}`,
|
|
211
|
+
{ type: 'sidecarCrashed', data: { crashes: this.crashes.length } },
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const epoch = ++this.epoch
|
|
216
|
+
const slot: Slot = { intentional: false, listeners: new Map(), tools: new Map() }
|
|
217
|
+
const connection = await this.transport.connect({
|
|
218
|
+
onMessage: (line) => slot.peer?.handleLine(line),
|
|
219
|
+
onLog: (line) => {
|
|
220
|
+
this.logTail.push(line)
|
|
221
|
+
if (this.logTail.length > LOG_TAIL) this.logTail.shift()
|
|
222
|
+
this.options.onLog?.(line)
|
|
223
|
+
},
|
|
224
|
+
onClose: (info) => this.handleClose(epoch, slot, info),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const peer = new RpcPeer(connection, {
|
|
228
|
+
onNotification: (method, params) => {
|
|
229
|
+
if (method === 'session/delta') {
|
|
230
|
+
const p = params as DeltaParams
|
|
231
|
+
slot.listeners.get(p.requestId)?.(p)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
onRequest: async (method, params, signal) => {
|
|
235
|
+
if (method !== 'tool/call') {
|
|
236
|
+
throw new AfmError(`Method not found: ${method}`, { type: 'methodNotFound', code: -32601 })
|
|
237
|
+
}
|
|
238
|
+
const {
|
|
239
|
+
sessionId,
|
|
240
|
+
name,
|
|
241
|
+
arguments: args,
|
|
242
|
+
} = params as { sessionId: string; name: string; arguments: unknown }
|
|
243
|
+
const found = slot.tools.get(sessionId)?.get(name)
|
|
244
|
+
if (!found) throw new AfmError(`Unknown tool '${name}'`, { type: 'toolNotFound', code: -32000 })
|
|
245
|
+
return runTool(found, args, { signal, sessionId })
|
|
246
|
+
},
|
|
247
|
+
onPendingChange: (count) => connection.setActive?.(count > 0),
|
|
248
|
+
onProtocolError: (error) => this.options.onLog?.(`[afm-bridge client] ${error.message}`),
|
|
249
|
+
})
|
|
250
|
+
slot.peer = peer
|
|
251
|
+
if (slot.exited) peer.close(this.crashError(slot.exited))
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const info = await peer.request<ServerInfo>(
|
|
255
|
+
'initialize',
|
|
256
|
+
{
|
|
257
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
258
|
+
client: this.options.clientInfo ?? { name: 'afm-bridge', version: CLIENT_VERSION },
|
|
259
|
+
},
|
|
260
|
+
{ timeoutMs: this.options.initializeTimeoutMs ?? 10_000, cancelGraceMs: 0 },
|
|
261
|
+
)
|
|
262
|
+
if (info.protocolVersion !== PROTOCOL_VERSION) {
|
|
263
|
+
throw new ProtocolError(
|
|
264
|
+
`afm-bridge-server ${info.server.version} speaks protocol version ${info.protocolVersion}, this client needs ${PROTOCOL_VERSION}. Install matching versions of afm-bridge and afm-bridge-server.`,
|
|
265
|
+
{ type: 'protocolVersionMismatch' },
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
if (this.closed) throw new ClientClosedError('The afm-bridge client was closed')
|
|
269
|
+
const live: Live = { epoch, peer, connection, info, slot }
|
|
270
|
+
this.live = live
|
|
271
|
+
return live
|
|
272
|
+
} catch (error) {
|
|
273
|
+
slot.intentional = true
|
|
274
|
+
peer.close(error instanceof Error ? error : new Error(String(error)))
|
|
275
|
+
await connection.close().catch(() => {})
|
|
276
|
+
if (error instanceof RequestTimeoutError) {
|
|
277
|
+
throw new SidecarCrashedError(
|
|
278
|
+
`afm-bridge-server did not answer the initialize handshake within ${this.options.initializeTimeoutMs ?? 10_000} ms.${this.formatLogTail()}`,
|
|
279
|
+
{ type: 'sidecarCrashed', cause: error },
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
throw error
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private handleClose(epoch: number, slot: Slot, info: CloseInfo) {
|
|
287
|
+
slot.exited = info
|
|
288
|
+
if (this.live?.epoch === epoch) this.live = undefined
|
|
289
|
+
if (slot.intentional || this.closed) {
|
|
290
|
+
slot.peer?.close(new ClientClosedError('The afm-bridge client was closed'))
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
this.crashes.push(Date.now())
|
|
294
|
+
slot.peer?.close(this.crashError(info))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private crashError(info: CloseInfo): SidecarCrashedError {
|
|
298
|
+
const how = info.signal ? `signal ${info.signal}` : `exit code ${info.code}`
|
|
299
|
+
return new SidecarCrashedError(
|
|
300
|
+
`afm-bridge-server stopped unexpectedly (${how}).${this.formatLogTail()}`,
|
|
301
|
+
{
|
|
302
|
+
type: 'sidecarCrashed',
|
|
303
|
+
data: { exitCode: info.code, signal: info.signal },
|
|
304
|
+
cause: info.error,
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private formatLogTail(): string {
|
|
310
|
+
return this.logTail.length > 0 ? `\nLast log lines:\n${this.logTail.join('\n')}` : ''
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { UnavailableReason } from './types.js'
|
|
2
|
+
|
|
3
|
+
export interface RpcErrorPayload {
|
|
4
|
+
code: number
|
|
5
|
+
message: string
|
|
6
|
+
data?: Record<string, unknown> & { type?: string }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Base class of every error thrown by afm-bridge. `type` matches PROTOCOL.md section 7. */
|
|
10
|
+
export class AfmError extends Error {
|
|
11
|
+
override name = 'AfmError'
|
|
12
|
+
readonly type: string
|
|
13
|
+
readonly code: number | undefined
|
|
14
|
+
readonly data: Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
message: string,
|
|
18
|
+
init: { type?: string; code?: number; data?: Record<string, unknown>; cause?: unknown } = {},
|
|
19
|
+
) {
|
|
20
|
+
super(message, init.cause === undefined ? undefined : { cause: init.cause })
|
|
21
|
+
this.type = init.type ?? 'generationFailed'
|
|
22
|
+
this.code = init.code
|
|
23
|
+
this.data = init.data ?? {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Framing, version or JSON-RPC level problem between client and sidecar. */
|
|
28
|
+
export class ProtocolError extends AfmError {
|
|
29
|
+
override name = 'ProtocolError'
|
|
30
|
+
}
|
|
31
|
+
export class InvalidParamsError extends AfmError {
|
|
32
|
+
override name = 'InvalidParamsError'
|
|
33
|
+
}
|
|
34
|
+
/** The request was cancelled through an `AbortSignal`. Has `name === 'AbortError'` like DOM aborts. */
|
|
35
|
+
export class AbortError extends AfmError {
|
|
36
|
+
override name = 'AbortError'
|
|
37
|
+
constructor(message = 'The operation was aborted', init: ConstructorParameters<typeof AfmError>[1] = {}) {
|
|
38
|
+
super(message, { type: 'cancelled', code: -32800, ...init })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ModelUnavailableError extends AfmError {
|
|
42
|
+
override name = 'ModelUnavailableError'
|
|
43
|
+
get reason(): UnavailableReason {
|
|
44
|
+
return (this.data.reason as UnavailableReason | undefined) ?? 'unknown'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class UnsupportedOSError extends AfmError {
|
|
48
|
+
override name = 'UnsupportedOSError'
|
|
49
|
+
}
|
|
50
|
+
export class SessionNotFoundError extends AfmError {
|
|
51
|
+
override name = 'SessionNotFoundError'
|
|
52
|
+
}
|
|
53
|
+
/** Another `respond`/`stream` is already running in this session. */
|
|
54
|
+
export class SessionBusyError extends AfmError {
|
|
55
|
+
override name = 'SessionBusyError'
|
|
56
|
+
}
|
|
57
|
+
export class ContextWindowExceededError extends AfmError {
|
|
58
|
+
override name = 'ContextWindowExceededError'
|
|
59
|
+
get contextSize(): number | undefined {
|
|
60
|
+
return this.data.contextSize as number | undefined
|
|
61
|
+
}
|
|
62
|
+
get tokenCount(): number | undefined {
|
|
63
|
+
return this.data.tokenCount as number | undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class GuardrailViolationError extends AfmError {
|
|
67
|
+
override name = 'GuardrailViolationError'
|
|
68
|
+
}
|
|
69
|
+
export class RefusalError extends AfmError {
|
|
70
|
+
override name = 'RefusalError'
|
|
71
|
+
}
|
|
72
|
+
export class UnsupportedLanguageError extends AfmError {
|
|
73
|
+
override name = 'UnsupportedLanguageError'
|
|
74
|
+
get languageCode(): string | undefined {
|
|
75
|
+
return this.data.languageCode as string | undefined
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class UnsupportedCapabilityError extends AfmError {
|
|
79
|
+
override name = 'UnsupportedCapabilityError'
|
|
80
|
+
}
|
|
81
|
+
export class UnsupportedSchemaError extends AfmError {
|
|
82
|
+
override name = 'UnsupportedSchemaError'
|
|
83
|
+
}
|
|
84
|
+
export class DecodingError extends AfmError {
|
|
85
|
+
override name = 'DecodingError'
|
|
86
|
+
}
|
|
87
|
+
export class RateLimitedError extends AfmError {
|
|
88
|
+
override name = 'RateLimitedError'
|
|
89
|
+
}
|
|
90
|
+
export class GenerationTimeoutError extends AfmError {
|
|
91
|
+
override name = 'GenerationTimeoutError'
|
|
92
|
+
}
|
|
93
|
+
export class ToolExecutionError extends AfmError {
|
|
94
|
+
override name = 'ToolExecutionError'
|
|
95
|
+
}
|
|
96
|
+
export class QuotaExceededError extends AfmError {
|
|
97
|
+
override name = 'QuotaExceededError'
|
|
98
|
+
}
|
|
99
|
+
export class NetworkError extends AfmError {
|
|
100
|
+
override name = 'NetworkError'
|
|
101
|
+
}
|
|
102
|
+
export class ServiceUnavailableError extends AfmError {
|
|
103
|
+
override name = 'ServiceUnavailableError'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Client-only errors (never sent over the wire).
|
|
107
|
+
|
|
108
|
+
/** The sidecar process died and could not be (re)started. */
|
|
109
|
+
export class SidecarCrashedError extends AfmError {
|
|
110
|
+
override name = 'SidecarCrashedError'
|
|
111
|
+
}
|
|
112
|
+
/** The session lived in a sidecar process that has since crashed or been closed. Create a new one. */
|
|
113
|
+
export class SessionLostError extends AfmError {
|
|
114
|
+
override name = 'SessionLostError'
|
|
115
|
+
}
|
|
116
|
+
/** Spawning the sidecar failed (binary missing, not executable, ...). */
|
|
117
|
+
export class TransportError extends AfmError {
|
|
118
|
+
override name = 'TransportError'
|
|
119
|
+
}
|
|
120
|
+
/** The client-side `timeoutMs` elapsed. */
|
|
121
|
+
export class RequestTimeoutError extends AfmError {
|
|
122
|
+
override name = 'RequestTimeoutError'
|
|
123
|
+
}
|
|
124
|
+
/** `close()` was called on the client. */
|
|
125
|
+
export class ClientClosedError extends AfmError {
|
|
126
|
+
override name = 'ClientClosedError'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type ErrorClass = new (message: string, init: ConstructorParameters<typeof AfmError>[1]) => AfmError
|
|
130
|
+
|
|
131
|
+
const byType: Record<string, ErrorClass> = {
|
|
132
|
+
parseError: ProtocolError,
|
|
133
|
+
invalidRequest: ProtocolError,
|
|
134
|
+
methodNotFound: ProtocolError,
|
|
135
|
+
protocolVersionMismatch: ProtocolError,
|
|
136
|
+
notInitialized: ProtocolError,
|
|
137
|
+
invalidParams: InvalidParamsError,
|
|
138
|
+
cancelled: AbortError,
|
|
139
|
+
modelUnavailable: ModelUnavailableError,
|
|
140
|
+
unsupportedOS: UnsupportedOSError,
|
|
141
|
+
sessionNotFound: SessionNotFoundError,
|
|
142
|
+
sessionBusy: SessionBusyError,
|
|
143
|
+
contextWindowExceeded: ContextWindowExceededError,
|
|
144
|
+
guardrailViolation: GuardrailViolationError,
|
|
145
|
+
refusal: RefusalError,
|
|
146
|
+
unsupportedLanguage: UnsupportedLanguageError,
|
|
147
|
+
unsupportedCapability: UnsupportedCapabilityError,
|
|
148
|
+
unsupportedSchema: UnsupportedSchemaError,
|
|
149
|
+
decodingFailure: DecodingError,
|
|
150
|
+
rateLimited: RateLimitedError,
|
|
151
|
+
timeout: GenerationTimeoutError,
|
|
152
|
+
toolExecution: ToolExecutionError,
|
|
153
|
+
quotaExceeded: QuotaExceededError,
|
|
154
|
+
networkFailure: NetworkError,
|
|
155
|
+
serviceUnavailable: ServiceUnavailableError,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Builds the typed error for a JSON-RPC error object. Unknown types become a plain `AfmError`. */
|
|
159
|
+
export function errorFromRpc(payload: RpcErrorPayload): AfmError {
|
|
160
|
+
const { type = 'unknown', ...data } = payload.data ?? {}
|
|
161
|
+
if (type === 'assetsUnavailable') {
|
|
162
|
+
return new ModelUnavailableError(payload.message, {
|
|
163
|
+
type,
|
|
164
|
+
code: payload.code,
|
|
165
|
+
data: { ...data, reason: 'modelNotReady' },
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const Class = byType[type] ?? AfmError
|
|
169
|
+
return new Class(payload.message, { type, code: payload.code, data })
|
|
170
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Core entry point: no Node APIs, safe to import in a browser or webview.
|
|
2
|
+
export {
|
|
3
|
+
AppleFoundationModels,
|
|
4
|
+
type AppleFoundationModelsOptions,
|
|
5
|
+
CLIENT_VERSION,
|
|
6
|
+
PROTOCOL_VERSION,
|
|
7
|
+
} from './client.js'
|
|
8
|
+
export * from './errors.js'
|
|
9
|
+
export {
|
|
10
|
+
type DeepPartial,
|
|
11
|
+
isStructuredSchema,
|
|
12
|
+
type JSONSchema,
|
|
13
|
+
jsonSchema,
|
|
14
|
+
type SchemaInput,
|
|
15
|
+
type StructuredSchema,
|
|
16
|
+
validate,
|
|
17
|
+
} from './schema.js'
|
|
18
|
+
export { Session } from './session.js'
|
|
19
|
+
export { type Tool, type ToolContext, tool } from './tool.js'
|
|
20
|
+
export type { CloseInfo, Connection, Transport, TransportHandlers } from './transport.js'
|
|
21
|
+
export { createLineSplitter } from './transport.js'
|
|
22
|
+
export type * from './types.js'
|