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/dist/rpc.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { AbortError, AfmError, errorFromRpc, ProtocolError, RequestTimeoutError, } from './errors.js';
|
|
2
|
+
const DEFAULT_CANCEL_GRACE_MS = 5000;
|
|
3
|
+
/** JSON-RPC 2.0 peer over one connection. Both sides can send requests. */
|
|
4
|
+
export class RpcPeer {
|
|
5
|
+
connection;
|
|
6
|
+
handlers;
|
|
7
|
+
nextId = 1;
|
|
8
|
+
pending = new Map();
|
|
9
|
+
incoming = new Map();
|
|
10
|
+
closedError;
|
|
11
|
+
constructor(connection, handlers) {
|
|
12
|
+
this.connection = connection;
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
}
|
|
15
|
+
get pendingCount() {
|
|
16
|
+
return this.pending.size;
|
|
17
|
+
}
|
|
18
|
+
request(method, params, options = {}) {
|
|
19
|
+
if (this.closedError)
|
|
20
|
+
return Promise.reject(this.closedError);
|
|
21
|
+
const { signal, timeoutMs } = options;
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return Promise.reject(new AbortError());
|
|
24
|
+
const id = this.nextId++;
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let timer;
|
|
27
|
+
let graceTimer;
|
|
28
|
+
let cancelError;
|
|
29
|
+
const cancel = (error) => {
|
|
30
|
+
if (cancelError || !this.pending.has(id))
|
|
31
|
+
return;
|
|
32
|
+
cancelError = error;
|
|
33
|
+
this.notify('cancel', { id });
|
|
34
|
+
// Wait for the sidecar's answer so the session is free again when we reject,
|
|
35
|
+
// but never hang if the sidecar does not answer.
|
|
36
|
+
const grace = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
|
|
37
|
+
if (grace <= 0)
|
|
38
|
+
this.settle(id, { error });
|
|
39
|
+
else
|
|
40
|
+
graceTimer = setTimeout(() => this.settle(id, { error }), grace);
|
|
41
|
+
};
|
|
42
|
+
const onAbort = () => cancel(new AbortError());
|
|
43
|
+
this.pending.set(id, {
|
|
44
|
+
resolve: (value) => resolve(value),
|
|
45
|
+
// A cancelled request is answered with `cancelled`; report the reason we cancelled for.
|
|
46
|
+
reject: (error) => reject(cancelError && error instanceof AfmError && error.type === 'cancelled' ? cancelError : error),
|
|
47
|
+
cleanup: () => {
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (graceTimer)
|
|
51
|
+
clearTimeout(graceTimer);
|
|
52
|
+
signal?.removeEventListener('abort', onAbort);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
this.handlers.onPendingChange?.(this.pending.size);
|
|
56
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
57
|
+
if (timeoutMs !== undefined) {
|
|
58
|
+
timer = setTimeout(() => cancel(new RequestTimeoutError(`Request '${method}' timed out after ${timeoutMs} ms`)), timeoutMs);
|
|
59
|
+
}
|
|
60
|
+
options.onStart?.(id);
|
|
61
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
notify(method, params) {
|
|
65
|
+
if (this.closedError)
|
|
66
|
+
return;
|
|
67
|
+
this.write({ jsonrpc: '2.0', method, params });
|
|
68
|
+
}
|
|
69
|
+
/** Feeds one line received from the connection. */
|
|
70
|
+
handleLine(line) {
|
|
71
|
+
let message;
|
|
72
|
+
try {
|
|
73
|
+
message = JSON.parse(line);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
this.handlers.onProtocolError?.(new ProtocolError(`Sidecar sent invalid JSON: ${line.slice(0, 200)}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (typeof message !== 'object' || message === null || message.jsonrpc !== '2.0') {
|
|
80
|
+
this.handlers.onProtocolError?.(new ProtocolError(`Sidecar sent a non JSON-RPC message: ${line.slice(0, 200)}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const { id, method } = message;
|
|
84
|
+
if (typeof method === 'string') {
|
|
85
|
+
if (id === undefined)
|
|
86
|
+
this.handleNotification(method, message.params);
|
|
87
|
+
else
|
|
88
|
+
this.handleIncomingRequest(id, method, message.params);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (typeof id === 'number') {
|
|
92
|
+
if ('error' in message)
|
|
93
|
+
this.settle(id, { error: errorFromRpc(message.error) });
|
|
94
|
+
else
|
|
95
|
+
this.settle(id, { result: message.result });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Error without id: the sidecar could not parse something we sent.
|
|
99
|
+
if ('error' in message)
|
|
100
|
+
this.handlers.onProtocolError?.(errorFromRpc(message.error));
|
|
101
|
+
}
|
|
102
|
+
/** Rejects everything in flight; later calls reject with the same error. */
|
|
103
|
+
close(error) {
|
|
104
|
+
if (this.closedError)
|
|
105
|
+
return;
|
|
106
|
+
this.closedError = error;
|
|
107
|
+
for (const id of [...this.pending.keys()])
|
|
108
|
+
this.settle(id, { error });
|
|
109
|
+
for (const controller of this.incoming.values())
|
|
110
|
+
controller.abort();
|
|
111
|
+
this.incoming.clear();
|
|
112
|
+
}
|
|
113
|
+
handleNotification(method, params) {
|
|
114
|
+
if (method === 'cancel') {
|
|
115
|
+
const id = params?.id;
|
|
116
|
+
if (id !== undefined)
|
|
117
|
+
this.incoming.get(id)?.abort();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.handlers.onNotification(method, params);
|
|
121
|
+
}
|
|
122
|
+
handleIncomingRequest(id, method, params) {
|
|
123
|
+
const handler = this.handlers.onRequest;
|
|
124
|
+
if (!handler) {
|
|
125
|
+
this.write({
|
|
126
|
+
jsonrpc: '2.0',
|
|
127
|
+
id,
|
|
128
|
+
error: { code: -32601, message: `Method not found: ${method}`, data: { type: 'methodNotFound' } },
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
this.incoming.set(id, controller);
|
|
134
|
+
handler(method, params, controller.signal).then((result) => {
|
|
135
|
+
this.incoming.delete(id);
|
|
136
|
+
this.write({ jsonrpc: '2.0', id, result });
|
|
137
|
+
}, (error) => {
|
|
138
|
+
this.incoming.delete(id);
|
|
139
|
+
const payload = error instanceof AfmError && error.code !== undefined
|
|
140
|
+
? { code: error.code, message: error.message, data: { ...error.data, type: error.type } }
|
|
141
|
+
: {
|
|
142
|
+
code: -32000,
|
|
143
|
+
message: error instanceof Error ? error.message : String(error),
|
|
144
|
+
data: { type: 'handlerError' },
|
|
145
|
+
};
|
|
146
|
+
this.write({ jsonrpc: '2.0', id, error: payload });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
settle(id, outcome) {
|
|
150
|
+
const entry = this.pending.get(id);
|
|
151
|
+
if (!entry)
|
|
152
|
+
return;
|
|
153
|
+
this.pending.delete(id);
|
|
154
|
+
entry.cleanup();
|
|
155
|
+
this.handlers.onPendingChange?.(this.pending.size);
|
|
156
|
+
if ('error' in outcome && outcome.error !== undefined)
|
|
157
|
+
entry.reject(outcome.error);
|
|
158
|
+
else
|
|
159
|
+
entry.resolve(outcome.result);
|
|
160
|
+
}
|
|
161
|
+
write(message) {
|
|
162
|
+
if (this.closedError)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
this.connection.send(JSON.stringify(message));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
this.handlers.onProtocolError?.(error instanceof Error ? error : new Error(String(error)));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=rpc.js.map
|
package/dist/rpc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The JSON Schema subset afm-bridge supports (PROTOCOL.md section 8). */
|
|
2
|
+
export interface JSONSchema {
|
|
3
|
+
type?: 'object' | 'array' | 'string' | 'integer' | 'number' | 'boolean' | 'null' | Array<string>;
|
|
4
|
+
description?: string;
|
|
5
|
+
properties?: Record<string, JSONSchema>;
|
|
6
|
+
required?: string[];
|
|
7
|
+
additionalProperties?: false;
|
|
8
|
+
items?: JSONSchema;
|
|
9
|
+
minItems?: number;
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
minimum?: number;
|
|
12
|
+
maximum?: number;
|
|
13
|
+
enum?: Array<string | null>;
|
|
14
|
+
const?: string;
|
|
15
|
+
pattern?: string;
|
|
16
|
+
anyOf?: JSONSchema[];
|
|
17
|
+
oneOf?: JSONSchema[];
|
|
18
|
+
$ref?: string;
|
|
19
|
+
$defs?: Record<string, JSONSchema>;
|
|
20
|
+
definitions?: Record<string, JSONSchema>;
|
|
21
|
+
[annotation: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare const structured: unique symbol;
|
|
24
|
+
/**
|
|
25
|
+
* A schema plus a parser that turns the generated JSON into `T`. Create one with
|
|
26
|
+
* `jsonSchema()` or `zodSchema()` from `afm-bridge/zod`.
|
|
27
|
+
*/
|
|
28
|
+
export interface StructuredSchema<T> {
|
|
29
|
+
readonly [structured]: true;
|
|
30
|
+
readonly jsonSchema: JSONSchema;
|
|
31
|
+
/** Validates and converts a generated value. Throws `DecodingError` when it does not match. */
|
|
32
|
+
parse(value: unknown): T;
|
|
33
|
+
}
|
|
34
|
+
/** A plain JSON Schema object or a `StructuredSchema`. */
|
|
35
|
+
export type SchemaInput<T = unknown> = JSONSchema | StructuredSchema<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Wraps a JSON Schema. Without `parse` the result is checked by the built-in validator
|
|
38
|
+
* for the supported subset and typed as `T`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function jsonSchema<T = unknown>(schema: JSONSchema, parse?: (value: unknown) => T): StructuredSchema<T>;
|
|
41
|
+
export declare function isStructuredSchema(value: unknown): value is StructuredSchema<unknown>;
|
|
42
|
+
export declare function toStructuredSchema<T>(input: SchemaInput<T>): StructuredSchema<T>;
|
|
43
|
+
/** Recursively optional, for partial values while a structured response streams in. */
|
|
44
|
+
export type DeepPartial<T> = T extends Array<infer U> ? Array<DeepPartial<U>> : T extends object ? {
|
|
45
|
+
[K in keyof T]?: DeepPartial<T[K]>;
|
|
46
|
+
} : T;
|
|
47
|
+
interface Issue {
|
|
48
|
+
path: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
/** Returns the first mismatch between `value` and `schema`, or undefined. `format` is not checked. */
|
|
52
|
+
export declare function validate(schema: JSONSchema, value: unknown): Issue | undefined;
|
|
53
|
+
export {};
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { DecodingError } from './errors.js';
|
|
2
|
+
const structured = Symbol.for('afm-bridge.schema');
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a JSON Schema. Without `parse` the result is checked by the built-in validator
|
|
5
|
+
* for the supported subset and typed as `T`.
|
|
6
|
+
*/
|
|
7
|
+
export function jsonSchema(schema, parse) {
|
|
8
|
+
return {
|
|
9
|
+
[structured]: true,
|
|
10
|
+
jsonSchema: schema,
|
|
11
|
+
parse: parse ??
|
|
12
|
+
((value) => {
|
|
13
|
+
const issue = validate(schema, value);
|
|
14
|
+
if (issue) {
|
|
15
|
+
throw new DecodingError(`Generated value does not match the schema at '${issue.path}': ${issue.message}`, {
|
|
16
|
+
type: 'decodingFailure',
|
|
17
|
+
data: { path: issue.path, rawContent: JSON.stringify(value) },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function isStructuredSchema(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && structured in value;
|
|
26
|
+
}
|
|
27
|
+
export function toStructuredSchema(input) {
|
|
28
|
+
return isStructuredSchema(input) ? input : jsonSchema(input);
|
|
29
|
+
}
|
|
30
|
+
/** Returns the first mismatch between `value` and `schema`, or undefined. `format` is not checked. */
|
|
31
|
+
export function validate(schema, value) {
|
|
32
|
+
return check(schema, value, '#', schema, 0);
|
|
33
|
+
}
|
|
34
|
+
function check(schema, value, path, root, depth) {
|
|
35
|
+
if (depth > 64)
|
|
36
|
+
return undefined;
|
|
37
|
+
const fail = (message) => ({ path, message });
|
|
38
|
+
if (schema.$ref !== undefined) {
|
|
39
|
+
const target = resolveRef(schema.$ref, root);
|
|
40
|
+
if (!target)
|
|
41
|
+
return fail(`cannot resolve ${schema.$ref}`);
|
|
42
|
+
return check(target, value, path, root, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
const branches = schema.anyOf ?? schema.oneOf;
|
|
45
|
+
if (branches) {
|
|
46
|
+
const ok = branches.some((b) => !check(b, value, path, root, depth + 1));
|
|
47
|
+
return ok ? undefined : fail('does not match any of the allowed schemas');
|
|
48
|
+
}
|
|
49
|
+
if (schema.enum) {
|
|
50
|
+
return schema.enum.includes(value)
|
|
51
|
+
? undefined
|
|
52
|
+
: fail(`must be one of ${JSON.stringify(schema.enum)}`);
|
|
53
|
+
}
|
|
54
|
+
if (schema.const !== undefined) {
|
|
55
|
+
return value === schema.const ? undefined : fail(`must be ${JSON.stringify(schema.const)}`);
|
|
56
|
+
}
|
|
57
|
+
const types = schema.type === undefined ? [] : Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
58
|
+
if (value === null) {
|
|
59
|
+
return types.includes('null') ? undefined : fail('must not be null');
|
|
60
|
+
}
|
|
61
|
+
const type = types.find((t) => t !== 'null');
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'string': {
|
|
64
|
+
if (typeof value !== 'string')
|
|
65
|
+
return fail('must be a string');
|
|
66
|
+
// A pattern that is valid for Swift but not for JavaScript is skipped, not failed.
|
|
67
|
+
const pattern = schema.pattern === undefined ? undefined : safeRegExp(schema.pattern);
|
|
68
|
+
if (pattern && !pattern.test(value))
|
|
69
|
+
return fail(`must match /${schema.pattern}/`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
case 'integer':
|
|
73
|
+
case 'number':
|
|
74
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
75
|
+
return fail(`must be a ${type}`);
|
|
76
|
+
if (type === 'integer' && !Number.isInteger(value))
|
|
77
|
+
return fail('must be an integer');
|
|
78
|
+
if (schema.minimum !== undefined && value < schema.minimum)
|
|
79
|
+
return fail(`must be >= ${schema.minimum}`);
|
|
80
|
+
if (schema.maximum !== undefined && value > schema.maximum)
|
|
81
|
+
return fail(`must be <= ${schema.maximum}`);
|
|
82
|
+
return undefined;
|
|
83
|
+
case 'boolean':
|
|
84
|
+
return typeof value === 'boolean' ? undefined : fail('must be a boolean');
|
|
85
|
+
case 'array': {
|
|
86
|
+
if (!Array.isArray(value))
|
|
87
|
+
return fail('must be an array');
|
|
88
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
89
|
+
return fail(`must have at least ${schema.minItems} items`);
|
|
90
|
+
}
|
|
91
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
92
|
+
return fail(`must have at most ${schema.maxItems} items`);
|
|
93
|
+
}
|
|
94
|
+
if (!schema.items)
|
|
95
|
+
return undefined;
|
|
96
|
+
for (const [i, item] of value.entries()) {
|
|
97
|
+
const issue = check(schema.items, item, `${path}/${i}`, root, depth + 1);
|
|
98
|
+
if (issue)
|
|
99
|
+
return issue;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
case 'object': {
|
|
104
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
105
|
+
return fail('must be an object');
|
|
106
|
+
const record = value;
|
|
107
|
+
const properties = schema.properties ?? {};
|
|
108
|
+
for (const key of schema.required ?? []) {
|
|
109
|
+
if (!(key in record))
|
|
110
|
+
return fail(`is missing required property '${key}'`);
|
|
111
|
+
}
|
|
112
|
+
for (const [key, v] of Object.entries(record)) {
|
|
113
|
+
const sub = properties[key];
|
|
114
|
+
if (!sub) {
|
|
115
|
+
if (schema.additionalProperties === false)
|
|
116
|
+
return fail(`has unexpected property '${key}'`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const issue = check(sub, v, `${path}/${escapePointer(key)}`, root, depth + 1);
|
|
120
|
+
if (issue)
|
|
121
|
+
return issue;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function resolveRef(ref, root) {
|
|
130
|
+
if (ref === '#')
|
|
131
|
+
return root;
|
|
132
|
+
const match = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
|
|
133
|
+
if (!match?.[1] || !match[2])
|
|
134
|
+
return undefined;
|
|
135
|
+
const defs = (match[1] === '$defs' ? root.$defs : root.definitions) ?? {};
|
|
136
|
+
return defs[match[2].replace(/~1/g, '/').replace(/~0/g, '~')];
|
|
137
|
+
}
|
|
138
|
+
function escapePointer(key) {
|
|
139
|
+
return key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
140
|
+
}
|
|
141
|
+
function safeRegExp(pattern) {
|
|
142
|
+
try {
|
|
143
|
+
return new RegExp(pattern, 'u');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAyB3C,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;AAgBlD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,MAAkB,EAClB,KAA6B;IAE7B,OAAO;QACL,CAAC,UAAU,CAAC,EAAE,IAAI;QAClB,UAAU,EAAE,MAAM;QAClB,KAAK,EACH,KAAK;YACL,CAAC,CAAC,KAAc,EAAE,EAAE;gBAClB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;gBACrC,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,IAAI,aAAa,CACrB,iDAAiD,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,OAAO,EAAE,EAChF;wBACE,IAAI,EAAE,iBAAiB;wBACvB,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;qBAC9D,CACF,CAAA;gBACH,CAAC;gBACD,OAAO,KAAU,CAAA;YACnB,CAAC,CAAC;KACL,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,IAAI,KAAK,CAAA;AAC3E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAI,KAAqB;IACzD,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAA6B,CAAC,CAAC,CAAC,UAAU,CAAI,KAAmB,CAAC,CAAA;AACxG,CAAC;AAiBD,sGAAsG;AACtG,MAAM,UAAU,QAAQ,CAAC,MAAkB,EAAE,KAAc;IACzD,OAAO,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;AAC7C,CAAC;AAED,SAAS,KAAK,CACZ,MAAkB,EAClB,KAAc,EACd,IAAY,EACZ,IAAgB,EAChB,KAAa;IAEb,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,SAAS,CAAA;IAChC,MAAM,IAAI,GAAG,CAAC,OAAe,EAAS,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IAE5D,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QAC5C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,kBAAkB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACzD,OAAO,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;IACpD,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAA;IAC7C,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;QACxE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAA;IAC3E,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAsB,CAAC;YACjD,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,IAAI,CAAC,kBAAkB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC3D,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAC7F,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACvG,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAA;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,EAAE,CAAC;YACd,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC,kBAAkB,CAAC,CAAA;YAC9D,mFAAmF;YACnF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YACrF,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,eAAe,MAAM,CAAC,OAAO,GAAG,CAAC,CAAA;YAClF,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,KAAK,SAAS,CAAC;QACf,KAAK,QAAQ;YACX,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;YAC1F,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,oBAAoB,CAAC,CAAA;YACrF,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,MAAM,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC,cAAc,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;YACvG,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,MAAM,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC,cAAc,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;YACvG,OAAO,SAAS,CAAA;QAClB,KAAK,SAAS;YACZ,OAAO,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QAC3E,KAAK,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,kBAAkB,CAAC,CAAA;YAC1D,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACpE,OAAO,IAAI,CAAC,sBAAsB,MAAM,CAAC,QAAQ,QAAQ,CAAC,CAAA;YAC5D,CAAC;YACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACpE,OAAO,IAAI,CAAC,qBAAqB,MAAM,CAAC,QAAQ,QAAQ,CAAC,CAAA;YAC3D,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAA;YACnC,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBACxC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;gBACxE,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAA;YACzB,CAAC;YACD,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,KAAK,QAAQ,EAAE,CAAC;YACd,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,mBAAmB,CAAC,CAAA;YACvF,MAAM,MAAM,GAAG,KAAgC,CAAA;YAC/C,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAA;YAC1C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACxC,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC;oBAAE,OAAO,IAAI,CAAC,iCAAiC,GAAG,GAAG,CAAC,CAAA;YAC5E,CAAC;YACD,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9C,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;gBAC3B,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,IAAI,MAAM,CAAC,oBAAoB,KAAK,KAAK;wBAAE,OAAO,IAAI,CAAC,4BAA4B,GAAG,GAAG,CAAC,CAAA;oBAC1F,SAAQ;gBACV,CAAC;gBACD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;gBAC7E,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAA;YACzB,CAAC;YACD,OAAO,SAAS,CAAA;QAClB,CAAC;QACD;YACE,OAAO,SAAS,CAAA;IACpB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAW,EAAE,IAAgB;IAC/C,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAC5B,MAAM,KAAK,GAAG,iCAAiC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9C,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IACzE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;AAC/D,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;AACrD,CAAC;AAED,SAAS,UAAU,CAAC,OAAe;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { RequestOptions } from './rpc.js';
|
|
2
|
+
import type { GenerateResult, ModelId, Prompt, RespondOptions, SessionInfo, StreamChunk, StructuredOptions, StructuredResult, StructuredStreamChunk } from './types.js';
|
|
3
|
+
/** What a session needs from the client. Internal. */
|
|
4
|
+
export interface SessionHost {
|
|
5
|
+
request<T>(epoch: number, method: string, params: unknown, options?: RequestOptions): Promise<T>;
|
|
6
|
+
listen(epoch: number, requestId: number, listener: (params: DeltaParams) => void): () => void;
|
|
7
|
+
disposed(epoch: number, sessionId: string): void;
|
|
8
|
+
}
|
|
9
|
+
export interface DeltaParams {
|
|
10
|
+
requestId: number;
|
|
11
|
+
delta?: string;
|
|
12
|
+
reset?: boolean;
|
|
13
|
+
text?: string;
|
|
14
|
+
partial?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A conversation with the model. The sidecar keeps its history, so every call sees
|
|
18
|
+
* the previous prompts and responses. Only one `respond`/`stream` may run at a time.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Session {
|
|
21
|
+
private readonly host;
|
|
22
|
+
private readonly epoch;
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Context window in tokens. */
|
|
25
|
+
readonly contextSize: number;
|
|
26
|
+
readonly model: ModelId;
|
|
27
|
+
private disposed;
|
|
28
|
+
/** @internal Use `AppleFoundationModels.createSession()`. */
|
|
29
|
+
constructor(host: SessionHost, epoch: number, id: string,
|
|
30
|
+
/** Context window in tokens. */
|
|
31
|
+
contextSize: number, model: ModelId);
|
|
32
|
+
/**
|
|
33
|
+
* Generates a complete response. Returns the text, or with `schema` the parsed and
|
|
34
|
+
* validated value.
|
|
35
|
+
*/
|
|
36
|
+
respond(prompt: Prompt, options?: RespondOptions & {
|
|
37
|
+
schema?: undefined;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
respond<T>(prompt: Prompt, options: StructuredOptions<T>): Promise<T>;
|
|
40
|
+
/** Like `respond`, but also returns token usage (macOS 27+). */
|
|
41
|
+
generate(prompt: Prompt, options?: RespondOptions & {
|
|
42
|
+
schema?: undefined;
|
|
43
|
+
}): Promise<GenerateResult>;
|
|
44
|
+
generate<T>(prompt: Prompt, options: StructuredOptions<T>): Promise<StructuredResult<T>>;
|
|
45
|
+
/**
|
|
46
|
+
* Streams the response. Text: each chunk carries the new `delta` and the full `text`
|
|
47
|
+
* so far. With `schema`: each chunk carries the `partial` value so far. The last chunk
|
|
48
|
+
* has `done: true` with the complete result. Breaking out of the loop cancels the generation.
|
|
49
|
+
*/
|
|
50
|
+
stream(prompt: Prompt, options?: RespondOptions & {
|
|
51
|
+
schema?: undefined;
|
|
52
|
+
}): AsyncGenerator<StreamChunk, void, undefined>;
|
|
53
|
+
stream<T>(prompt: Prompt, options: StructuredOptions<T>): AsyncGenerator<StructuredStreamChunk<T>, void, undefined>;
|
|
54
|
+
/** Loads model resources ahead of time to reduce the latency of the first response. */
|
|
55
|
+
prewarm(promptPrefix?: string): Promise<void>;
|
|
56
|
+
/** Context usage of the session. */
|
|
57
|
+
info(): Promise<SessionInfo>;
|
|
58
|
+
/** Frees the session in the sidecar and cancels a running generation. Safe to call twice. */
|
|
59
|
+
dispose(): Promise<void>;
|
|
60
|
+
private call;
|
|
61
|
+
private assertUsable;
|
|
62
|
+
private respondParams;
|
|
63
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { AbortError, AfmError, ClientClosedError, SessionLostError, SessionNotFoundError } from './errors.js';
|
|
2
|
+
import { toStructuredSchema } from './schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* A conversation with the model. The sidecar keeps its history, so every call sees
|
|
5
|
+
* the previous prompts and responses. Only one `respond`/`stream` may run at a time.
|
|
6
|
+
*/
|
|
7
|
+
export class Session {
|
|
8
|
+
host;
|
|
9
|
+
epoch;
|
|
10
|
+
id;
|
|
11
|
+
contextSize;
|
|
12
|
+
model;
|
|
13
|
+
disposed = false;
|
|
14
|
+
/** @internal Use `AppleFoundationModels.createSession()`. */
|
|
15
|
+
constructor(host, epoch, id,
|
|
16
|
+
/** Context window in tokens. */
|
|
17
|
+
contextSize, model) {
|
|
18
|
+
this.host = host;
|
|
19
|
+
this.epoch = epoch;
|
|
20
|
+
this.id = id;
|
|
21
|
+
this.contextSize = contextSize;
|
|
22
|
+
this.model = model;
|
|
23
|
+
}
|
|
24
|
+
async respond(prompt, options = {}) {
|
|
25
|
+
const result = await this.generate(prompt, options);
|
|
26
|
+
return options.schema ? result.content : result.text;
|
|
27
|
+
}
|
|
28
|
+
async generate(prompt, options = {}) {
|
|
29
|
+
const { signal, timeoutMs } = options;
|
|
30
|
+
const schema = options.schema ? toStructuredSchema(options.schema) : undefined;
|
|
31
|
+
const result = await this.call('session/respond', this.respondParams(prompt, options, schema), {
|
|
32
|
+
signal,
|
|
33
|
+
timeoutMs,
|
|
34
|
+
});
|
|
35
|
+
if (!schema)
|
|
36
|
+
return withUsage({ text: result.text }, result.usage);
|
|
37
|
+
return withUsage({ content: parseContent(schema, result), text: result.text }, result.usage);
|
|
38
|
+
}
|
|
39
|
+
async *stream(prompt, options = {}) {
|
|
40
|
+
const { signal, timeoutMs } = options;
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
throw new AbortError();
|
|
43
|
+
this.assertUsable();
|
|
44
|
+
const schema = options.schema ? toStructuredSchema(options.schema) : undefined;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const onAbort = () => controller.abort();
|
|
47
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
const queue = new ChunkQueue();
|
|
49
|
+
let text = '';
|
|
50
|
+
let unsubscribe = () => { };
|
|
51
|
+
const request = this.host.request(this.epoch, 'session/respond', { ...this.respondParams(prompt, options, schema), stream: true }, {
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
timeoutMs,
|
|
54
|
+
onStart: (id) => {
|
|
55
|
+
unsubscribe = this.host.listen(this.epoch, id, (p) => {
|
|
56
|
+
if (schema) {
|
|
57
|
+
if (p.partial !== undefined)
|
|
58
|
+
queue.push({ done: false, partial: p.partial });
|
|
59
|
+
}
|
|
60
|
+
else if (p.reset) {
|
|
61
|
+
text = p.text ?? '';
|
|
62
|
+
queue.push({ done: false, delta: '', text, reset: true });
|
|
63
|
+
}
|
|
64
|
+
else if (p.delta) {
|
|
65
|
+
text += p.delta;
|
|
66
|
+
queue.push({ done: false, delta: p.delta, text });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
request.then((result) => queue.end(result), (error) => queue.fail(error));
|
|
72
|
+
let finished = false;
|
|
73
|
+
try {
|
|
74
|
+
while (true) {
|
|
75
|
+
const item = await queue.next();
|
|
76
|
+
if (item.kind === 'chunk') {
|
|
77
|
+
yield item.chunk;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
finished = true;
|
|
81
|
+
const { result } = item;
|
|
82
|
+
yield schema
|
|
83
|
+
? withUsage({ done: true, content: parseContent(schema, result), text: result.text }, result.usage)
|
|
84
|
+
: withUsage({ done: true, delta: '', text: result.text }, result.usage);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
unsubscribe();
|
|
90
|
+
signal?.removeEventListener('abort', onAbort);
|
|
91
|
+
if (!finished) {
|
|
92
|
+
// The consumer stopped early or an error was thrown: make sure the
|
|
93
|
+
// generation is cancelled and the session is free before we return.
|
|
94
|
+
controller.abort();
|
|
95
|
+
await request.catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Loads model resources ahead of time to reduce the latency of the first response. */
|
|
100
|
+
async prewarm(promptPrefix) {
|
|
101
|
+
await this.call('session/prewarm', { sessionId: this.id, ...(promptPrefix ? { promptPrefix } : {}) });
|
|
102
|
+
}
|
|
103
|
+
/** Context usage of the session. */
|
|
104
|
+
async info() {
|
|
105
|
+
return this.call('session/info', { sessionId: this.id });
|
|
106
|
+
}
|
|
107
|
+
/** Frees the session in the sidecar and cancels a running generation. Safe to call twice. */
|
|
108
|
+
async dispose() {
|
|
109
|
+
if (this.disposed)
|
|
110
|
+
return;
|
|
111
|
+
this.disposed = true;
|
|
112
|
+
this.host.disposed(this.epoch, this.id);
|
|
113
|
+
try {
|
|
114
|
+
await this.host.request(this.epoch, 'session/dispose', { sessionId: this.id });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Nothing to free when the process is already gone.
|
|
118
|
+
if (error instanceof SessionLostError || error instanceof ClientClosedError)
|
|
119
|
+
return;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
call(method, params, options) {
|
|
124
|
+
try {
|
|
125
|
+
this.assertUsable();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return Promise.reject(error);
|
|
129
|
+
}
|
|
130
|
+
return this.host.request(this.epoch, method, params, options);
|
|
131
|
+
}
|
|
132
|
+
assertUsable() {
|
|
133
|
+
if (this.disposed) {
|
|
134
|
+
throw new SessionNotFoundError(`Session ${this.id} was disposed`, {
|
|
135
|
+
type: 'sessionNotFound',
|
|
136
|
+
data: { sessionId: this.id },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
respondParams(prompt, options, schema) {
|
|
141
|
+
const { temperature, maximumResponseTokens, sampling, toolCalling, reasoningLevel } = options;
|
|
142
|
+
const generation = {
|
|
143
|
+
temperature,
|
|
144
|
+
maximumResponseTokens,
|
|
145
|
+
sampling,
|
|
146
|
+
toolCalling,
|
|
147
|
+
reasoningLevel,
|
|
148
|
+
};
|
|
149
|
+
const defined = Object.fromEntries(Object.entries(generation).filter(([, v]) => v !== undefined));
|
|
150
|
+
return {
|
|
151
|
+
sessionId: this.id,
|
|
152
|
+
prompt,
|
|
153
|
+
...(Object.keys(defined).length > 0 ? { options: defined } : {}),
|
|
154
|
+
...(schema ? { schema: schema.jsonSchema } : {}),
|
|
155
|
+
...(schema && options.includeSchemaInPrompt !== undefined
|
|
156
|
+
? { includeSchemaInPrompt: options.includeSchemaInPrompt }
|
|
157
|
+
: {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseContent(schema, result) {
|
|
162
|
+
return schema.parse(result.content !== undefined ? result.content : JSON.parse(result.text));
|
|
163
|
+
}
|
|
164
|
+
function withUsage(value, usage) {
|
|
165
|
+
return usage ? { ...value, usage } : value;
|
|
166
|
+
}
|
|
167
|
+
/** Single-consumer queue that delivers buffered chunks before an error. */
|
|
168
|
+
class ChunkQueue {
|
|
169
|
+
items = [];
|
|
170
|
+
error;
|
|
171
|
+
waiter;
|
|
172
|
+
push(chunk) {
|
|
173
|
+
this.items.push({ kind: 'chunk', chunk });
|
|
174
|
+
this.wake();
|
|
175
|
+
}
|
|
176
|
+
end(result) {
|
|
177
|
+
this.items.push({ kind: 'end', result });
|
|
178
|
+
this.wake();
|
|
179
|
+
}
|
|
180
|
+
fail(error) {
|
|
181
|
+
this.error = { value: error };
|
|
182
|
+
this.wake();
|
|
183
|
+
}
|
|
184
|
+
async next() {
|
|
185
|
+
while (true) {
|
|
186
|
+
const item = this.items.shift();
|
|
187
|
+
if (item)
|
|
188
|
+
return item;
|
|
189
|
+
if (this.error)
|
|
190
|
+
throw this.error.value instanceof Error ? this.error.value : new AfmError(String(this.error.value));
|
|
191
|
+
await new Promise((resolve) => {
|
|
192
|
+
this.waiter = resolve;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
wake() {
|
|
197
|
+
const waiter = this.waiter;
|
|
198
|
+
this.waiter = undefined;
|
|
199
|
+
waiter?.();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=session.js.map
|