ruicode 0.0.0-stage → 0.67.1
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 +150 -2
- package/dist/binary.d.ts +10 -0
- package/dist/binary.js +23 -0
- package/dist/client.d.ts +97 -0
- package/dist/client.js +370 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.js +31 -0
- package/dist/feed.d.ts +78 -0
- package/dist/feed.js +237 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/notifications.d.ts +17 -0
- package/dist/notifications.js +69 -0
- package/dist/sessions.d.ts +160 -0
- package/dist/sessions.js +279 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +87 -0
- package/dist/wire.d.ts +4066 -0
- package/dist/wire.js +5 -0
- package/package.json +34 -5
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { components } from "./wire.ts";
|
|
2
|
+
type Schemas = components["schemas"];
|
|
3
|
+
type Reply = Schemas[Extract<keyof Schemas, `${string}Reply`>];
|
|
4
|
+
type Answer<T> = T extends {
|
|
5
|
+
answer: infer A;
|
|
6
|
+
} ? A : never;
|
|
7
|
+
type Code<T> = T extends {
|
|
8
|
+
code: infer C extends string;
|
|
9
|
+
} ? C : never;
|
|
10
|
+
type Status<T> = T extends {
|
|
11
|
+
status: infer S extends string;
|
|
12
|
+
} ? S : never;
|
|
13
|
+
export type RequestAnswer = Answer<Reply>;
|
|
14
|
+
export type RequestRejectionCode = Code<RequestAnswer>;
|
|
15
|
+
/** HTTP ErrorReply deliberately accepts codes beyond the command rejection enums. */
|
|
16
|
+
export type RequestErrorCode = RequestRejectionCode | Schemas["ErrorReply"]["code"];
|
|
17
|
+
type RequestErrorType = Schemas["ErrorReply"]["type"] | Status<Extract<RequestAnswer, {
|
|
18
|
+
code: string;
|
|
19
|
+
}>>;
|
|
20
|
+
type StartErrorCode = Schemas["StartError"]["rui_start_error"]["code"];
|
|
21
|
+
/** Rui rejected one request. `code` is the Host's rejection code; `message` preserves advisory detail when provided. */
|
|
22
|
+
export declare class RuiRequestError extends Error {
|
|
23
|
+
readonly name = "RuiRequestError";
|
|
24
|
+
readonly code: RequestErrorCode;
|
|
25
|
+
/** The Host's error class, such as `invocation_error` or `busy`. */
|
|
26
|
+
readonly type: RequestErrorType;
|
|
27
|
+
constructor(type: RequestErrorType, code: RequestErrorCode, message?: string | null);
|
|
28
|
+
}
|
|
29
|
+
/** Rui refused to open. `code` is one of the refusal codes at open (D136). */
|
|
30
|
+
export declare class RuiStartError extends Error {
|
|
31
|
+
readonly name = "RuiStartError";
|
|
32
|
+
readonly code: StartErrorCode;
|
|
33
|
+
constructor(code: StartErrorCode, message: string);
|
|
34
|
+
}
|
|
35
|
+
/** The Rui child process is gone, or was never ready. */
|
|
36
|
+
export declare class RuiUnavailableError extends Error {
|
|
37
|
+
readonly name = "RuiUnavailableError";
|
|
38
|
+
constructor(message: string, options?: {
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The three error types a caller sees (D116). Request failures are
|
|
2
|
+
// RuiRequestError, a refusal at open is RuiStartError, and a call that finds
|
|
3
|
+
// no running Host is RuiUnavailableError.
|
|
4
|
+
/** Rui rejected one request. `code` is the Host's rejection code; `message` preserves advisory detail when provided. */
|
|
5
|
+
export class RuiRequestError extends Error {
|
|
6
|
+
name = "RuiRequestError";
|
|
7
|
+
code;
|
|
8
|
+
/** The Host's error class, such as `invocation_error` or `busy`. */
|
|
9
|
+
type;
|
|
10
|
+
constructor(type, code, message) {
|
|
11
|
+
super(message ?? `Rui rejected the request: ${code}`);
|
|
12
|
+
this.type = type;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Rui refused to open. `code` is one of the refusal codes at open (D136). */
|
|
17
|
+
export class RuiStartError extends Error {
|
|
18
|
+
name = "RuiStartError";
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The Rui child process is gone, or was never ready. */
|
|
26
|
+
export class RuiUnavailableError extends Error {
|
|
27
|
+
name = "RuiUnavailableError";
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super(message, options);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/feed.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Link } from "./sessions.ts";
|
|
2
|
+
import type { components } from "./wire.ts";
|
|
3
|
+
type Schemas = components["schemas"];
|
|
4
|
+
type Row = Schemas["ActivityItem"];
|
|
5
|
+
/** Every kind the Host's feed has, in its own order. */
|
|
6
|
+
export declare const FEED_KINDS: readonly ["admission", "user", "assistant", "call", "tool_result", "hosted_call", "turn_ended", "stop", "sent", "send_failed", "received", "approval_requested", "session_changed", "compacted"];
|
|
7
|
+
export type FeedKind = (typeof FEED_KINDS)[number];
|
|
8
|
+
/** Entry text is inline up to this many bytes (D168). */
|
|
9
|
+
export declare const INLINE_TEXT_BYTES: number;
|
|
10
|
+
export interface ListActivityOptions {
|
|
11
|
+
/** The `cursor` of the last entry read. Left out, the feed starts at its beginning. */
|
|
12
|
+
after?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Follows new entries, polling at 250 ms rising to 2 s only while busy.
|
|
15
|
+
* An empty idle/finished/failed/stopped page with nothing queued sleeps without a timer
|
|
16
|
+
* until send/update/decide/interrupt/stop succeeds through any handle for this Session
|
|
17
|
+
* on this Rui instance. Writes from another process to the same Store are not noticed
|
|
18
|
+
* while asleep. `signal` abort ends the iterator even while asleep.
|
|
19
|
+
*/
|
|
20
|
+
follow?: boolean;
|
|
21
|
+
/** Only these kinds, by their wire names. Left out, every kind. */
|
|
22
|
+
kinds?: readonly FeedKind[];
|
|
23
|
+
/** Ends the iterator, even while it waits for new entries: a pending `next()` then resolves as done. */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/** Maps only the row's own keys; nested values retain their wire spelling (D128). */
|
|
27
|
+
type CamelKey<Key extends string> = Key extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<CamelKey<Tail>>}` : Key;
|
|
28
|
+
type CamelRow<Payload> = {
|
|
29
|
+
[Key in keyof Payload as Key extends "session" ? "sessionId" : Key extends string ? CamelKey<Key> : Key]: Payload[Key];
|
|
30
|
+
};
|
|
31
|
+
type EntryText = {
|
|
32
|
+
/** The row's text, when this kind carries it and it fits inline. */
|
|
33
|
+
text?: string;
|
|
34
|
+
/** Text exceeds 16 KiB: use `session.readContent(entry)`. */
|
|
35
|
+
truncated?: boolean;
|
|
36
|
+
/** The text's length in bytes, when this kind carries text. */
|
|
37
|
+
contentBytes?: string;
|
|
38
|
+
};
|
|
39
|
+
type EntryFromRow<R extends Row> = R extends Row ? {
|
|
40
|
+
[Kind in keyof R["value"]]: Omit<R, "value"> & {
|
|
41
|
+
kind: Kind;
|
|
42
|
+
cursor: string;
|
|
43
|
+
} & CamelRow<R["value"][Kind]> & ("content" extends keyof R["value"][Kind] ? EntryText : {});
|
|
44
|
+
}[keyof R["value"]] : never;
|
|
45
|
+
/** A discriminated wire-row union with top-level camelCase fields and an opaque continuation `cursor`.
|
|
46
|
+
* `session_changed` uses `reasoning_effort_refused` and inline `providerText` for a refused effort change.
|
|
47
|
+
*/
|
|
48
|
+
export type FeedEntry = EntryFromRow<Row>;
|
|
49
|
+
/** An opaque cursor: after the row at this position and ordinal. */
|
|
50
|
+
export declare function encodeCursor(position: string, ordinal: string): string;
|
|
51
|
+
export declare function decodeCursor(cursor: string): {
|
|
52
|
+
position: string;
|
|
53
|
+
ordinal: string;
|
|
54
|
+
};
|
|
55
|
+
/** The whole text of an entry that has some, as UTF-8 bytes, however long (D134). */
|
|
56
|
+
export declare function readContentBytes(link: Link, session: string, entry: {
|
|
57
|
+
position: string;
|
|
58
|
+
ordinal: string;
|
|
59
|
+
}): Promise<Buffer>;
|
|
60
|
+
/** The whole text of an entry as a string. A text past the JavaScript string limit (about 512 MiB) throws; use `readContentBytes`. */
|
|
61
|
+
export declare function readContent(link: Link, session: string, entry: {
|
|
62
|
+
position: string;
|
|
63
|
+
ordinal: string;
|
|
64
|
+
}): Promise<string>;
|
|
65
|
+
export declare function listActivity(link: Link, session: string, options?: ListActivityOptions): AsyncGenerator<FeedEntry, void, undefined>;
|
|
66
|
+
export interface SearchActivityOptions {
|
|
67
|
+
/** Resume before this search entry's cursor. */
|
|
68
|
+
before?: string;
|
|
69
|
+
kinds?: readonly FeedKind[];
|
|
70
|
+
limit?: number;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}
|
|
73
|
+
export type ActivitySearchEntry = Schemas["ActivitySearchReply"]["items"][number] & {
|
|
74
|
+
cursor: string;
|
|
75
|
+
};
|
|
76
|
+
/** Search public feed text newest first. Empty budget-limited pages still advance. */
|
|
77
|
+
export declare function searchActivity(link: Link, session: string, query: string, options?: SearchActivityOptions): AsyncGenerator<ActivitySearchEntry, void, undefined>;
|
|
78
|
+
export {};
|
package/dist/feed.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// The Session feed: `listActivity` is an async iterator over /v1/activity-page
|
|
2
|
+
// (D9, D10, D117, A-D117, D134, D168). The Host sends rows with content
|
|
3
|
+
// references only, and no cursor type. The client encodes the cursor from a
|
|
4
|
+
// row's position and ordinal, and fills `text` from /v1/activity-content, so
|
|
5
|
+
// the Host holds no reply buffer for either (D168).
|
|
6
|
+
import { WIRE_VERSION } from "./transport.js";
|
|
7
|
+
/** Every kind the Host's feed has, in its own order. */
|
|
8
|
+
export const FEED_KINDS = [
|
|
9
|
+
"admission",
|
|
10
|
+
"user",
|
|
11
|
+
"assistant",
|
|
12
|
+
"call",
|
|
13
|
+
"tool_result",
|
|
14
|
+
"hosted_call",
|
|
15
|
+
"turn_ended",
|
|
16
|
+
"stop",
|
|
17
|
+
"sent",
|
|
18
|
+
"send_failed",
|
|
19
|
+
"received",
|
|
20
|
+
"approval_requested",
|
|
21
|
+
"session_changed",
|
|
22
|
+
"compacted",
|
|
23
|
+
];
|
|
24
|
+
// A kind the wire gains fails to compile here until the list has it.
|
|
25
|
+
const complete = true;
|
|
26
|
+
void complete;
|
|
27
|
+
/** Entry text is inline up to this many bytes (D168). */
|
|
28
|
+
export const INLINE_TEXT_BYTES = 16 * 1024;
|
|
29
|
+
const PAGE_ROWS = 16;
|
|
30
|
+
const FIRST_POLL_MS = 250;
|
|
31
|
+
const MAX_POLL_MS = 2_000;
|
|
32
|
+
/** The only endings whose `turn_ended` entry carries text (D168). */
|
|
33
|
+
const ENDINGS_WITH_TEXT = new Set(["provider_refused", "failed"]);
|
|
34
|
+
/** An opaque cursor: after the row at this position and ordinal. */
|
|
35
|
+
export function encodeCursor(position, ordinal) {
|
|
36
|
+
return Buffer.from(`1:${position}:${ordinal}`).toString("base64url");
|
|
37
|
+
}
|
|
38
|
+
export function decodeCursor(cursor) {
|
|
39
|
+
const match = /^1:(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/.exec(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
40
|
+
if (!match)
|
|
41
|
+
throw new RangeError("not a feed cursor");
|
|
42
|
+
return { position: match[1], ordinal: match[2] };
|
|
43
|
+
}
|
|
44
|
+
function camel(name) {
|
|
45
|
+
if (name === "session")
|
|
46
|
+
return "sessionId";
|
|
47
|
+
return name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
48
|
+
}
|
|
49
|
+
function camelCase(row) {
|
|
50
|
+
return Object.fromEntries(Object.entries(row).map(([name, value]) => [camel(name), value]));
|
|
51
|
+
}
|
|
52
|
+
function isReference(value) {
|
|
53
|
+
return typeof value === "object" && value !== null && "bytes" in value && "sha256" in value;
|
|
54
|
+
}
|
|
55
|
+
/** Builds an entry from a row, without its text. Returns the content reference when text is to be read. */
|
|
56
|
+
function entryOf(row) {
|
|
57
|
+
const [kind, payload] = Object.entries(row.value)[0];
|
|
58
|
+
const reference = isReference(payload.content) ? payload.content : undefined;
|
|
59
|
+
const wanted = kind !== "turn_ended" || ENDINGS_WITH_TEXT.has(String(payload.ending));
|
|
60
|
+
const entry = {
|
|
61
|
+
kind,
|
|
62
|
+
cursor: encodeCursor(row.position, row.ordinal),
|
|
63
|
+
position: row.position,
|
|
64
|
+
ordinal: row.ordinal,
|
|
65
|
+
at: row.at,
|
|
66
|
+
...camelCase(payload),
|
|
67
|
+
...(reference && wanted ? { contentBytes: reference.bytes } : {}),
|
|
68
|
+
}; // Object.entries erases the generated kind/payload correlation at this boundary.
|
|
69
|
+
return { entry, reference: reference && wanted ? reference : undefined };
|
|
70
|
+
}
|
|
71
|
+
function activityBody(store, session, request) {
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
version: WIRE_VERSION,
|
|
74
|
+
kind: "activity_page",
|
|
75
|
+
store,
|
|
76
|
+
session,
|
|
77
|
+
end: request.end,
|
|
78
|
+
position: request.position,
|
|
79
|
+
ordinal: request.ordinal,
|
|
80
|
+
direction: "forward",
|
|
81
|
+
kinds: [...request.kinds],
|
|
82
|
+
limit: String(PAGE_ROWS),
|
|
83
|
+
...(request.since === null ? {} : { since: request.since }),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function contentBody(store, session, entry) {
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
version: WIRE_VERSION,
|
|
89
|
+
kind: "activity_content",
|
|
90
|
+
store,
|
|
91
|
+
session,
|
|
92
|
+
position: entry.position,
|
|
93
|
+
ordinal: entry.ordinal,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** The Host stores up to a little under 1 GiB of text in one entry. */
|
|
97
|
+
const MAX_CONTENT_BYTES = 1024 * 1024 * 1024;
|
|
98
|
+
/** The whole text of an entry that has some, as UTF-8 bytes, however long (D134). */
|
|
99
|
+
export function readContentBytes(link, session, entry) {
|
|
100
|
+
return link.exchange("/v1/activity-content", (store) => contentBody(store, session, entry), undefined, { max: MAX_CONTENT_BYTES });
|
|
101
|
+
}
|
|
102
|
+
/** The whole text of an entry as a string. A text past the JavaScript string limit (about 512 MiB) throws; use `readContentBytes`. */
|
|
103
|
+
export async function readContent(link, session, entry) {
|
|
104
|
+
return (await readContentBytes(link, session, entry)).toString("utf8");
|
|
105
|
+
}
|
|
106
|
+
/** Waits `ms`, or less when `signal` aborts. */
|
|
107
|
+
function pause(ms, signal) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
if (signal?.aborted)
|
|
110
|
+
return resolve();
|
|
111
|
+
const done = () => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
signal?.removeEventListener("abort", done);
|
|
114
|
+
resolve();
|
|
115
|
+
};
|
|
116
|
+
const timer = setTimeout(done, ms);
|
|
117
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function* listActivity(link, session, options = {}) {
|
|
121
|
+
// The Host wants kinds in its own order, each once.
|
|
122
|
+
const kinds = options.kinds === undefined ? FEED_KINDS : FEED_KINDS.filter((kind) => options.kinds?.includes(kind));
|
|
123
|
+
let position = "0";
|
|
124
|
+
let ordinal = null;
|
|
125
|
+
if (options.after !== undefined) {
|
|
126
|
+
const cursor = decodeCursor(options.after);
|
|
127
|
+
position = cursor.position;
|
|
128
|
+
ordinal = cursor.ordinal;
|
|
129
|
+
}
|
|
130
|
+
let pinnedEnd = null;
|
|
131
|
+
let wait = FIRST_POLL_MS;
|
|
132
|
+
let since = null;
|
|
133
|
+
let idle = false;
|
|
134
|
+
const { signal } = options;
|
|
135
|
+
const watcher = options.follow ? link.activity?.watch(session) : undefined;
|
|
136
|
+
try {
|
|
137
|
+
while (!signal?.aborted) {
|
|
138
|
+
// Capture before I/O so a write racing an idle reply cannot be lost.
|
|
139
|
+
const revision = watcher?.revision ?? 0;
|
|
140
|
+
const page = JSON.parse((await link.exchange("/v1/activity-page", (store) => activityBody(store, session, { end: pinnedEnd, position, ordinal, kinds, since }))).toString("utf8"));
|
|
141
|
+
let receivedItems = false;
|
|
142
|
+
if (page.delta === "not_modified") {
|
|
143
|
+
since = page.since;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
for (const row of page.items) {
|
|
147
|
+
// Cancellation is honoured before each entry, and again after any read that took time.
|
|
148
|
+
if (signal?.aborted)
|
|
149
|
+
return;
|
|
150
|
+
const { entry, reference } = entryOf(row);
|
|
151
|
+
if (reference && "content" in entry) {
|
|
152
|
+
if (Number(reference.bytes) > INLINE_TEXT_BYTES) {
|
|
153
|
+
entry.truncated = true;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const prefix = await link.exchange("/v1/activity-content", (store) => contentBody(store, session, row), undefined, { prefix: INLINE_TEXT_BYTES });
|
|
157
|
+
entry.text = prefix.toString("utf8");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (signal?.aborted)
|
|
161
|
+
return;
|
|
162
|
+
yield entry;
|
|
163
|
+
position = row.position;
|
|
164
|
+
ordinal = row.ordinal;
|
|
165
|
+
}
|
|
166
|
+
if (page.more) {
|
|
167
|
+
// A bounded read keeps the end the first page named; a following one asks again.
|
|
168
|
+
pinnedEnd = options.follow ? null : page.end;
|
|
169
|
+
// A Session token cannot stand in for rows not yet consumed.
|
|
170
|
+
since = null;
|
|
171
|
+
wait = FIRST_POLL_MS;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!options.follow)
|
|
175
|
+
return;
|
|
176
|
+
pinnedEnd = null;
|
|
177
|
+
// Exhausted: everything the filter matches up to the end has been read, so the next poll starts at the end
|
|
178
|
+
// rather than rescanning the rows between the last match and it.
|
|
179
|
+
position = page.end;
|
|
180
|
+
ordinal = null;
|
|
181
|
+
since = page.since;
|
|
182
|
+
idle = page.queued.count === "0" && dormant(page.work.state.kind);
|
|
183
|
+
receivedItems = page.items.length > 0;
|
|
184
|
+
}
|
|
185
|
+
if (receivedItems)
|
|
186
|
+
wait = FIRST_POLL_MS;
|
|
187
|
+
else if (idle) {
|
|
188
|
+
await (watcher ? watcher.wait(revision, signal) : untilAbort(signal));
|
|
189
|
+
wait = FIRST_POLL_MS;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
await pause(wait, signal);
|
|
193
|
+
wait = Math.min(wait * 2, MAX_POLL_MS);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
watcher?.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function dormant(kind) {
|
|
202
|
+
return kind === "idle" || kind === "finished" || kind === "failed" || kind === "stopped";
|
|
203
|
+
}
|
|
204
|
+
/** A bare internal Link has no local writes to observe; cancellation still ends its idle wait. */
|
|
205
|
+
function untilAbort(signal) {
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
if (signal?.aborted)
|
|
208
|
+
resolve();
|
|
209
|
+
else
|
|
210
|
+
signal?.addEventListener("abort", () => resolve(), { once: true });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Search public feed text newest first. Empty budget-limited pages still advance. */
|
|
214
|
+
export async function* searchActivity(link, session, query, options = {}) {
|
|
215
|
+
const bytes = Buffer.byteLength(query, "utf8");
|
|
216
|
+
if (bytes === 0 || bytes > 256 || Buffer.from(query, "utf8").toString("utf8") !== query)
|
|
217
|
+
throw new RangeError("query must be 1–256 UTF-8 bytes");
|
|
218
|
+
const limit = options.limit ?? PAGE_ROWS;
|
|
219
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > PAGE_ROWS)
|
|
220
|
+
throw new RangeError("limit must be 1–16");
|
|
221
|
+
const kinds = options.kinds === undefined ? FEED_KINDS : FEED_KINDS.filter((kind) => options.kinds?.includes(kind));
|
|
222
|
+
let before = options.before === undefined ? null : decodeCursor(options.before);
|
|
223
|
+
while (!options.signal?.aborted) {
|
|
224
|
+
const page = JSON.parse((await link.exchange("/v1/activity-search", (store) => JSON.stringify({
|
|
225
|
+
version: WIRE_VERSION, kind: "activity_search", store, session, query, kinds: [...kinds],
|
|
226
|
+
before_position: before?.position ?? null, before_ordinal: before?.ordinal ?? null, limit: String(limit),
|
|
227
|
+
}))).toString("utf8"));
|
|
228
|
+
for (const item of page.items) {
|
|
229
|
+
if (options.signal?.aborted)
|
|
230
|
+
return;
|
|
231
|
+
yield { ...item, cursor: encodeCursor(item.position, item.ordinal) };
|
|
232
|
+
}
|
|
233
|
+
if (page.next === null)
|
|
234
|
+
return;
|
|
235
|
+
before = page.next;
|
|
236
|
+
}
|
|
237
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Rui, type ConnectOptions, type ExitEvent, type McpServer } from "./client.ts";
|
|
2
|
+
export { RuiRequestError, RuiStartError, RuiUnavailableError } from "./errors.ts";
|
|
3
|
+
export { Session, Sessions, type AgentSpec, type ListSessionsOptions, type SessionArchiveState, type SessionArchiveResult, type SessionCreate, type ModelSummary, type SendOptions, type SessionSpec, type SessionSummary, type SessionStatus, type SessionUpdate, type WriteOptions, } from "./sessions.ts";
|
|
4
|
+
export { FEED_KINDS, INLINE_TEXT_BYTES, type FeedEntry, type FeedKind, type ListActivityOptions, type SearchActivityOptions, type ActivitySearchEntry } from "./feed.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The package's public surface. Transport types stay internal (D19).
|
|
2
|
+
export { Rui } from "./client.js";
|
|
3
|
+
export { RuiRequestError, RuiStartError, RuiUnavailableError } from "./errors.js";
|
|
4
|
+
export { Session, Sessions, } from "./sessions.js";
|
|
5
|
+
export { FEED_KINDS, INLINE_TEXT_BYTES } from "./feed.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class SessionNotifications {
|
|
2
|
+
#private;
|
|
3
|
+
watch(session: string): WriteWatch;
|
|
4
|
+
/** Rejects and releases every active follower, including one still reading its page. */
|
|
5
|
+
fail(error: Error): void;
|
|
6
|
+
notify(session: string): void;
|
|
7
|
+
}
|
|
8
|
+
declare class WriteWatch {
|
|
9
|
+
#private;
|
|
10
|
+
revision: number;
|
|
11
|
+
constructor(release: () => void);
|
|
12
|
+
notify(): void;
|
|
13
|
+
wait(revision: number, signal: AbortSignal | undefined): Promise<void>;
|
|
14
|
+
fail(error: Error): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Owned by one Rui instance. Only active followers occupy the per-Session map.
|
|
2
|
+
export class SessionNotifications {
|
|
3
|
+
#sessions = new Map();
|
|
4
|
+
watch(session) {
|
|
5
|
+
let watches = this.#sessions.get(session);
|
|
6
|
+
if (!watches)
|
|
7
|
+
this.#sessions.set(session, (watches = new Set()));
|
|
8
|
+
const watch = new WriteWatch(() => {
|
|
9
|
+
if (watches.delete(watch) && watches.size === 0)
|
|
10
|
+
this.#sessions.delete(session);
|
|
11
|
+
});
|
|
12
|
+
watches.add(watch);
|
|
13
|
+
return watch;
|
|
14
|
+
}
|
|
15
|
+
/** Rejects and releases every active follower, including one still reading its page. */
|
|
16
|
+
fail(error) {
|
|
17
|
+
for (const watches of this.#sessions.values()) {
|
|
18
|
+
for (const watch of watches)
|
|
19
|
+
watch.fail(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
notify(session) {
|
|
23
|
+
for (const watch of this.#sessions.get(session) ?? [])
|
|
24
|
+
watch.notify();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class WriteWatch {
|
|
28
|
+
revision = 0;
|
|
29
|
+
#release;
|
|
30
|
+
#wake;
|
|
31
|
+
#error;
|
|
32
|
+
constructor(release) {
|
|
33
|
+
this.#release = release;
|
|
34
|
+
}
|
|
35
|
+
notify() {
|
|
36
|
+
this.revision++;
|
|
37
|
+
this.#wake?.();
|
|
38
|
+
}
|
|
39
|
+
wait(revision, signal) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
return resolve();
|
|
43
|
+
if (this.#error)
|
|
44
|
+
return reject(this.#error);
|
|
45
|
+
if (this.revision !== revision)
|
|
46
|
+
return resolve();
|
|
47
|
+
const done = (error) => {
|
|
48
|
+
signal?.removeEventListener("abort", abort);
|
|
49
|
+
this.#wake = undefined;
|
|
50
|
+
if (error)
|
|
51
|
+
reject(error);
|
|
52
|
+
else
|
|
53
|
+
resolve();
|
|
54
|
+
};
|
|
55
|
+
const abort = () => done();
|
|
56
|
+
this.#wake = done;
|
|
57
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
fail(error) {
|
|
61
|
+
this.#error = error;
|
|
62
|
+
this.#wake?.(error);
|
|
63
|
+
this.close();
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
this.#wake?.();
|
|
67
|
+
this.#release();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { SessionNotifications } from "./notifications.ts";
|
|
2
|
+
import { type ActivitySearchEntry, type SearchActivityOptions, type FeedEntry, type ListActivityOptions } from "./feed.ts";
|
|
3
|
+
import { type ReplyLimits } from "./transport.ts";
|
|
4
|
+
import type { components } from "./wire.ts";
|
|
5
|
+
/** What a Session handle needs from the client. */
|
|
6
|
+
export interface Link {
|
|
7
|
+
readonly activity?: SessionNotifications;
|
|
8
|
+
exchange(path: string, build: (store: string) => string, environment?: Buffer, limits?: ReplyLimits): Promise<Buffer>;
|
|
9
|
+
}
|
|
10
|
+
type Schemas = components["schemas"];
|
|
11
|
+
/** The Session settings the Host takes today. */
|
|
12
|
+
export interface AgentSpec {
|
|
13
|
+
/** The working directory for Bash. Required with `bash`, fixed for the Session's life. */
|
|
14
|
+
workspace?: string;
|
|
15
|
+
/** `openai` or `codex`. Fixed for the Session's life. */
|
|
16
|
+
provider?: NonNullable<Schemas["AgentSpec"]["provider"]>;
|
|
17
|
+
/** May change within the Session's fixed provider. */
|
|
18
|
+
model: string;
|
|
19
|
+
instructions?: string;
|
|
20
|
+
/** JSON output schema object; `null` clears it. */
|
|
21
|
+
outputSchema?: Record<string, unknown> | null;
|
|
22
|
+
/** Tool names the Host offers, such as `bash` and `web_search`. */
|
|
23
|
+
tools: readonly string[];
|
|
24
|
+
/** Tools loaded from the first model request. */
|
|
25
|
+
loadedTools?: readonly string[];
|
|
26
|
+
/** Tokens reserved for model output; null clears the override. */
|
|
27
|
+
outputReservation?: number | null;
|
|
28
|
+
permissionMode: "ask" | "bypass";
|
|
29
|
+
/** Reasoning effort. `null` sends none, so the provider uses its own; only for a model Rui does not list (D111). */
|
|
30
|
+
effort: Schemas["AgentSpec"]["effort"];
|
|
31
|
+
/** Compact the conversation at this many tokens. */
|
|
32
|
+
compactAt: number;
|
|
33
|
+
/** A name shown in lists. `null` clears it. */
|
|
34
|
+
label?: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** The same agent spec accepted by creation and connect-time presets. */
|
|
37
|
+
export type SessionSpec = AgentSpec;
|
|
38
|
+
/** Creation needs a complete spec, or a named preset with explicit overrides. */
|
|
39
|
+
export type SessionCreate = ((AgentSpec & {
|
|
40
|
+
preset?: never;
|
|
41
|
+
}) | ({
|
|
42
|
+
preset: string;
|
|
43
|
+
} & Partial<AgentSpec>)) & {
|
|
44
|
+
/** Create an attached child, inheriting provider, workspace and Bash environment. */
|
|
45
|
+
parent?: string;
|
|
46
|
+
};
|
|
47
|
+
/** The settings `update` may change: everything but the ones fixed at creation. */
|
|
48
|
+
export type SessionUpdate = Partial<Pick<SessionSpec, "model" | "instructions" | "outputSchema" | "tools" | "permissionMode" | "effort" | "compactAt" | "label" | "loadedTools" | "outputReservation">>;
|
|
49
|
+
export interface WriteOptions {
|
|
50
|
+
/** Required on `create` and `send`, built as `kind:driftId`. Other writes get a fresh UUIDv4 when it is left out (D123). */
|
|
51
|
+
idempotencyKey?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface SendOptions {
|
|
54
|
+
idempotencyKey: string;
|
|
55
|
+
/** The environment Bash runs in. Defaults to empty: the caller's own never reaches Bash (D121). */
|
|
56
|
+
env?: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
export type SessionArchiveState = CurrentReport["archive_state"];
|
|
59
|
+
export interface ListSessionsOptions {
|
|
60
|
+
/** Defaults to active, including Sessions still settling an archive request. */
|
|
61
|
+
archive?: Schemas["ListSessionsRequest"]["archive"];
|
|
62
|
+
}
|
|
63
|
+
export interface SessionArchiveResult {
|
|
64
|
+
archived: boolean;
|
|
65
|
+
/** Accepted intent can remain pending until stopped work has really settled. */
|
|
66
|
+
completion: "pending" | "completed";
|
|
67
|
+
}
|
|
68
|
+
export interface SessionSummary {
|
|
69
|
+
id: string;
|
|
70
|
+
label: string | null;
|
|
71
|
+
/** The Session's work state, such as `idle`, `working` or `approval`. */
|
|
72
|
+
state: string;
|
|
73
|
+
archiveState: SessionArchiveState;
|
|
74
|
+
parent: string | null;
|
|
75
|
+
}
|
|
76
|
+
type CurrentReport = Extract<Schemas["InspectSessionReply"], {
|
|
77
|
+
profile: "current";
|
|
78
|
+
work: unknown;
|
|
79
|
+
}>;
|
|
80
|
+
/** Current Host facts. Decimal counts, times and IDs remain lossless strings. */
|
|
81
|
+
export interface SessionStatus {
|
|
82
|
+
archiveState: SessionArchiveState;
|
|
83
|
+
state: CurrentReport["work"]["state"]["kind"];
|
|
84
|
+
since: CurrentReport["work"]["state"]["since"];
|
|
85
|
+
turn: CurrentReport["work"]["turn"];
|
|
86
|
+
operation: CurrentReport["work"]["operation"];
|
|
87
|
+
pendingMessages: CurrentReport["pending_messages"];
|
|
88
|
+
parent: CurrentReport["session"]["parent"];
|
|
89
|
+
label: CurrentReport["session"]["label"];
|
|
90
|
+
model: CurrentReport["session"]["model"];
|
|
91
|
+
effort: CurrentReport["session"]["reasoning_effort"];
|
|
92
|
+
tools: CurrentReport["session"]["tools"];
|
|
93
|
+
permissionMode: CurrentReport["session"]["permission_mode"];
|
|
94
|
+
latestOutcome: CurrentReport["work"]["latest_outcome"];
|
|
95
|
+
}
|
|
96
|
+
export interface ModelSummary {
|
|
97
|
+
provider: string;
|
|
98
|
+
id: string;
|
|
99
|
+
known: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** One plain wire spec for configure and connect-time presets. */
|
|
102
|
+
export declare function agentSpec(spec: Partial<SessionSpec>): Schemas["Configuration"];
|
|
103
|
+
/** `NAME=VALUE\0` for each entry the Host would keep (D121): a name that is not empty, holds no `=` or NUL and does not start with `RUI_`, and a value without NUL. The rest are left out, because after encoding the Host could not tell where an entry ended. */
|
|
104
|
+
export declare function environmentBytes(env: Record<string, string> | undefined): Buffer;
|
|
105
|
+
export declare class Session {
|
|
106
|
+
#private;
|
|
107
|
+
readonly id: string;
|
|
108
|
+
constructor(link: Link, id: string);
|
|
109
|
+
/**
|
|
110
|
+
* The Session's feed, in order, as an async iterator (D9). Without `follow`
|
|
111
|
+
* it ends at the current end. With it, busy Sessions poll with backoff; idle Sessions
|
|
112
|
+
* wait for a local write on this Rui instance (not writes from another process).
|
|
113
|
+
* See `ListActivityOptions.follow`; `signal` abort also ends an idle wait.
|
|
114
|
+
*/
|
|
115
|
+
listActivity(options?: ListActivityOptions): AsyncGenerator<FeedEntry, void, undefined>;
|
|
116
|
+
/** Search public feed text newest first, with bounded snippets. */
|
|
117
|
+
searchActivity(query: string, options?: SearchActivityOptions): AsyncGenerator<ActivitySearchEntry, void, undefined>;
|
|
118
|
+
/** The whole text of an entry (D134). Past about 512 MiB a string cannot hold it and this throws: use `readContentBytes`. */
|
|
119
|
+
readContent(entry: Pick<FeedEntry, "position" | "ordinal">): Promise<string>;
|
|
120
|
+
/** The whole text of an entry as UTF-8 bytes, up to the Host's limit of about 1 GiB. */
|
|
121
|
+
readContentBytes(entry: Pick<FeedEntry, "position" | "ordinal">): Promise<Buffer>;
|
|
122
|
+
/** A Current report's summary (D12). A separate ending is not reported. */
|
|
123
|
+
status(): Promise<SessionStatus>;
|
|
124
|
+
/** Sends a Message. The receipt is its admission number; the answer arrives in the feed (D8). */
|
|
125
|
+
send(text: string, options: SendOptions): Promise<{
|
|
126
|
+
messageId: string;
|
|
127
|
+
}>;
|
|
128
|
+
/** Changes settings. Omitted fields stay as they are. Resolves once the Host accepted the change (D7). */
|
|
129
|
+
update(fields: SessionUpdate, options?: WriteOptions): Promise<void>;
|
|
130
|
+
/** Stops the current and queued work. The Session stays usable (D14). */
|
|
131
|
+
stop(options?: WriteOptions): Promise<void>;
|
|
132
|
+
/** Stops work and archives after real settlement, using one durable intent. */
|
|
133
|
+
archive(options?: WriteOptions): Promise<SessionArchiveResult>;
|
|
134
|
+
/** Restores organization only. Previously stopped work is never resumed. */
|
|
135
|
+
unarchive(options?: WriteOptions): Promise<SessionArchiveResult>;
|
|
136
|
+
/**
|
|
137
|
+
* Cuts off the model call that is current now while the Session keeps going.
|
|
138
|
+
* It takes no key: each call names the work it sees at that moment, so a
|
|
139
|
+
* repeat is a new cut, not a replay. Resolves without effect when no model
|
|
140
|
+
* call is current, or when the call ended before the cut arrived (D167, D171).
|
|
141
|
+
*/
|
|
142
|
+
interrupt(): Promise<void>;
|
|
143
|
+
/** Answers an approval. `requestId` is the Action id of the `approval_requested` entry (D15). */
|
|
144
|
+
decide(requestId: string, decision: "allow" | "deny", options?: WriteOptions): Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
export declare class Sessions {
|
|
147
|
+
#private;
|
|
148
|
+
constructor(link: Link);
|
|
149
|
+
/** Creates a Session. The key is required: a repeat returns the same Session (D7, D17). */
|
|
150
|
+
create(spec: SessionCreate, options: {
|
|
151
|
+
idempotencyKey: string;
|
|
152
|
+
}): Promise<Session>;
|
|
153
|
+
/** Configured Sessions, newest first; archived Sessions are hidden by default. */
|
|
154
|
+
list(options?: ListSessionsOptions): Promise<SessionSummary[]>;
|
|
155
|
+
}
|
|
156
|
+
/** The models each provider offers (D128). */
|
|
157
|
+
export declare function models(link: Link): Promise<ModelSummary[]>;
|
|
158
|
+
/** Holds every new Turn while paused; running work continues (D199). */
|
|
159
|
+
export declare function setPaused(link: Link, paused: boolean): Promise<void>;
|
|
160
|
+
export {};
|