ruicode 0.0.0-stage → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -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 +238 -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 +4083 -0
- package/dist/wire.js +5 -0
- package/package.json +34 -5
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Sessions: create, list, status, send, update, stop, interrupt and decide, plus the
|
|
2
|
+
// model list and Host pause (D7, D8, D14 to D17, D121, D123, D128, D167,
|
|
3
|
+
// D171). Every call is one request to a Host route that exists today.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { RuiRequestError } from "./errors.js";
|
|
6
|
+
import { listActivity, searchActivity, readContent, readContentBytes } from "./feed.js";
|
|
7
|
+
import { WIRE_VERSION } from "./transport.js";
|
|
8
|
+
/** One plain wire spec for configure and connect-time presets. */
|
|
9
|
+
export function agentSpec(spec) {
|
|
10
|
+
return {
|
|
11
|
+
...(spec.workspace === undefined ? {} : { workspace: spec.workspace }),
|
|
12
|
+
...(spec.provider === undefined ? {} : { provider: spec.provider }),
|
|
13
|
+
...(spec.model === undefined ? {} : { model: spec.model }),
|
|
14
|
+
...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
|
|
15
|
+
...(spec.tools === undefined ? {} : { tools: [...new Set(spec.tools)].sort() }),
|
|
16
|
+
...(spec.loadedTools === undefined ? {} : { loadedTools: [...new Set(spec.loadedTools)].sort() }),
|
|
17
|
+
...(spec.permissionMode === undefined ? {} : { permissionMode: spec.permissionMode }),
|
|
18
|
+
...(spec.outputSchema === undefined ? {} : { outputSchema: spec.outputSchema }),
|
|
19
|
+
...(spec.compactAt === undefined ? {} : { compactAt: spec.compactAt }),
|
|
20
|
+
...(spec.outputReservation === undefined ? {} : { outputReservation: spec.outputReservation }),
|
|
21
|
+
...(spec.effort === undefined ? {} : { effort: spec.effort }),
|
|
22
|
+
...(spec.label === undefined ? {} : { label: spec.label }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function configureBody(store, key, session, spec) {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
version: WIRE_VERSION,
|
|
28
|
+
kind: "configure",
|
|
29
|
+
store,
|
|
30
|
+
key,
|
|
31
|
+
session,
|
|
32
|
+
configuration: { ...agentSpec(spec), ...(spec.parent === undefined ? {} : { parent: spec.parent }) },
|
|
33
|
+
...(spec.preset === undefined ? {} : { preset: spec.preset }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function key(options) {
|
|
37
|
+
return options?.idempotencyKey ?? randomUUID();
|
|
38
|
+
}
|
|
39
|
+
function parse(reply) {
|
|
40
|
+
return JSON.parse(reply.toString("utf8"));
|
|
41
|
+
}
|
|
42
|
+
/** Throws the Host's rejection or conflict; returns the accepted answer. */
|
|
43
|
+
function accepted(answer) {
|
|
44
|
+
if (answer.status !== "accepted")
|
|
45
|
+
throw new RuiRequestError(answer.status, answer.code, "message" in answer ? answer.message : undefined);
|
|
46
|
+
return answer;
|
|
47
|
+
}
|
|
48
|
+
/** `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. */
|
|
49
|
+
export function environmentBytes(env) {
|
|
50
|
+
const forwarded = Object.entries(env ?? {}).filter(([name, entry]) => name !== "" && !name.startsWith("RUI_") && !/[=\0]/.test(name) && !entry.includes("\0"));
|
|
51
|
+
return Buffer.from(forwarded.map(([name, entry]) => `${name}=${entry}\0`).join(""), "utf8");
|
|
52
|
+
}
|
|
53
|
+
export class Session {
|
|
54
|
+
#link;
|
|
55
|
+
id;
|
|
56
|
+
constructor(link, id) {
|
|
57
|
+
this.#link = link;
|
|
58
|
+
this.id = id;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The Session's feed, in order, as an async iterator (D9). Without `follow`
|
|
62
|
+
* it ends at the current end. With it, busy Sessions poll with backoff; idle Sessions
|
|
63
|
+
* wait for a local write on this Rui instance (not writes from another process).
|
|
64
|
+
* See `ListActivityOptions.follow`; `signal` abort also ends an idle wait.
|
|
65
|
+
*/
|
|
66
|
+
listActivity(options) {
|
|
67
|
+
return listActivity(this.#link, this.id, options);
|
|
68
|
+
}
|
|
69
|
+
/** Search public feed text newest first, with bounded snippets. */
|
|
70
|
+
searchActivity(query, options) {
|
|
71
|
+
return searchActivity(this.#link, this.id, query, options);
|
|
72
|
+
}
|
|
73
|
+
/** The whole text of an entry (D134). Past about 512 MiB a string cannot hold it and this throws: use `readContentBytes`. */
|
|
74
|
+
readContent(entry) {
|
|
75
|
+
return readContent(this.#link, this.id, entry);
|
|
76
|
+
}
|
|
77
|
+
/** The whole text of an entry as UTF-8 bytes, up to the Host's limit of about 1 GiB. */
|
|
78
|
+
readContentBytes(entry) {
|
|
79
|
+
return readContentBytes(this.#link, this.id, entry);
|
|
80
|
+
}
|
|
81
|
+
/** A Current report's summary (D12). A separate ending is not reported. */
|
|
82
|
+
async status() {
|
|
83
|
+
const reply = parse(await this.#link.exchange("/v1/inspect-session", (store) => JSON.stringify({
|
|
84
|
+
version: WIRE_VERSION,
|
|
85
|
+
kind: "inspect_session",
|
|
86
|
+
store,
|
|
87
|
+
session: this.id,
|
|
88
|
+
})));
|
|
89
|
+
if (reply.session === null)
|
|
90
|
+
throw new RuiRequestError("invocation_error", "session_not_found");
|
|
91
|
+
return {
|
|
92
|
+
archiveState: reply.archive_state,
|
|
93
|
+
state: reply.work.state.kind,
|
|
94
|
+
since: reply.work.state.since,
|
|
95
|
+
turn: reply.work.turn,
|
|
96
|
+
operation: reply.work.operation,
|
|
97
|
+
pendingMessages: reply.pending_messages,
|
|
98
|
+
parent: reply.session.parent,
|
|
99
|
+
label: reply.session.label,
|
|
100
|
+
model: reply.session.model,
|
|
101
|
+
effort: reply.session.reasoning_effort,
|
|
102
|
+
tools: reply.session.tools,
|
|
103
|
+
permissionMode: reply.session.permission_mode,
|
|
104
|
+
latestOutcome: reply.work.latest_outcome,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Sends a Message. The receipt is its admission number; the answer arrives in the feed (D8). */
|
|
108
|
+
async send(text, options) {
|
|
109
|
+
const body = (store) => JSON.stringify({
|
|
110
|
+
version: WIRE_VERSION,
|
|
111
|
+
kind: "message",
|
|
112
|
+
store,
|
|
113
|
+
key: options.idempotencyKey,
|
|
114
|
+
session: this.id,
|
|
115
|
+
sender: null,
|
|
116
|
+
sender_handle: null,
|
|
117
|
+
text: { state: "value", value: text },
|
|
118
|
+
});
|
|
119
|
+
const reply = parse(await this.#link.exchange("/v1/message", body, environmentBytes(options.env)));
|
|
120
|
+
const messageId = accepted(reply.answer).admission;
|
|
121
|
+
this.#link.activity?.notify(this.id);
|
|
122
|
+
return { messageId };
|
|
123
|
+
}
|
|
124
|
+
/** Changes settings. Omitted fields stay as they are. Resolves once the Host accepted the change (D7). */
|
|
125
|
+
async update(fields, options) {
|
|
126
|
+
const idempotencyKey = key(options);
|
|
127
|
+
const reply = parse(await this.#link.exchange("/v1/configure", (store) => configureBody(store, idempotencyKey, this.id, fields)));
|
|
128
|
+
accepted(reply.answer);
|
|
129
|
+
this.#link.activity?.notify(this.id);
|
|
130
|
+
}
|
|
131
|
+
/** Stops the current and queued work. The Session stays usable (D14). */
|
|
132
|
+
async stop(options) {
|
|
133
|
+
const idempotencyKey = key(options);
|
|
134
|
+
const body = (store) => JSON.stringify({
|
|
135
|
+
version: WIRE_VERSION,
|
|
136
|
+
kind: "session_stop",
|
|
137
|
+
store,
|
|
138
|
+
key: idempotencyKey,
|
|
139
|
+
session: this.id,
|
|
140
|
+
});
|
|
141
|
+
accepted(parse(await this.#link.exchange("/v1/control/session-stop", body)).answer);
|
|
142
|
+
this.#link.activity?.notify(this.id);
|
|
143
|
+
}
|
|
144
|
+
/** Stops work and archives after real settlement, using one durable intent. */
|
|
145
|
+
archive(options) {
|
|
146
|
+
return this.#setArchived(true, options);
|
|
147
|
+
}
|
|
148
|
+
/** Restores organization only. Previously stopped work is never resumed. */
|
|
149
|
+
unarchive(options) {
|
|
150
|
+
return this.#setArchived(false, options);
|
|
151
|
+
}
|
|
152
|
+
async #setArchived(archived, options) {
|
|
153
|
+
const idempotencyKey = key(options);
|
|
154
|
+
const reply = parse(await this.#link.exchange("/v1/control/session-archive", (store) => JSON.stringify({
|
|
155
|
+
version: WIRE_VERSION,
|
|
156
|
+
kind: "session_archive",
|
|
157
|
+
store,
|
|
158
|
+
key: idempotencyKey,
|
|
159
|
+
session: this.id,
|
|
160
|
+
archived,
|
|
161
|
+
})));
|
|
162
|
+
const answer = accepted(reply.answer);
|
|
163
|
+
const completion = reply.completion.status;
|
|
164
|
+
if (completion === "unavailable")
|
|
165
|
+
throw new RuiRequestError("invocation_error", "invalid_response");
|
|
166
|
+
this.#link.activity?.notify(this.id);
|
|
167
|
+
return { archived: answer.archived, completion };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Cuts off the model call that is current now while the Session keeps going.
|
|
171
|
+
* It takes no key: each call names the work it sees at that moment, so a
|
|
172
|
+
* repeat is a new cut, not a replay. Resolves without effect when no model
|
|
173
|
+
* call is current, or when the call ended before the cut arrived (D167, D171).
|
|
174
|
+
*/
|
|
175
|
+
async interrupt() {
|
|
176
|
+
const status = await this.status();
|
|
177
|
+
const { turn, operation } = status;
|
|
178
|
+
if (turn === null || operation === null || !modelCallCurrent(status.state)) {
|
|
179
|
+
this.#link.activity?.notify(this.id);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const idempotencyKey = randomUUID();
|
|
183
|
+
const body = (store) => JSON.stringify({
|
|
184
|
+
version: WIRE_VERSION,
|
|
185
|
+
kind: "model_interruption",
|
|
186
|
+
store,
|
|
187
|
+
key: idempotencyKey,
|
|
188
|
+
target: { session: this.id, turn, operation },
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
accepted(parse(await this.#link.exchange("/v1/control/model-interruption", body)).answer);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
// The call ended before the cut reached it: nothing is left to cut.
|
|
195
|
+
const gone = error instanceof RuiRequestError && (error.code === "operation_resolved" || error.code === "target_mismatch");
|
|
196
|
+
if (!gone)
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
this.#link.activity?.notify(this.id);
|
|
200
|
+
}
|
|
201
|
+
/** Answers an approval. `requestId` is the Action id of the `approval_requested` entry (D15). */
|
|
202
|
+
async decide(requestId, decision, options) {
|
|
203
|
+
const idempotencyKey = key(options);
|
|
204
|
+
const wire = decision === "allow" ? "allow_once" : "deny";
|
|
205
|
+
const body = (store) => JSON.stringify({
|
|
206
|
+
version: WIRE_VERSION,
|
|
207
|
+
kind: "permission_decision",
|
|
208
|
+
store,
|
|
209
|
+
key: idempotencyKey,
|
|
210
|
+
session: this.id,
|
|
211
|
+
action: requestId,
|
|
212
|
+
decision: wire,
|
|
213
|
+
});
|
|
214
|
+
accepted(parse(await this.#link.exchange("/v1/control/permission-decision", body)).answer);
|
|
215
|
+
this.#link.activity?.notify(this.id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Work states in which a model call is the current thing. */
|
|
219
|
+
function modelCallCurrent(kind) {
|
|
220
|
+
return kind === "working" || kind === "retrying" || kind === "pending";
|
|
221
|
+
}
|
|
222
|
+
export class Sessions {
|
|
223
|
+
#link;
|
|
224
|
+
constructor(link) {
|
|
225
|
+
this.#link = link;
|
|
226
|
+
}
|
|
227
|
+
/** Creates a Session. The key is required: a repeat returns the same Session (D7, D17). */
|
|
228
|
+
async create(spec, options) {
|
|
229
|
+
const reply = parse(await this.#link.exchange("/v1/configure", (store) => configureBody(store, options.idempotencyKey, "", spec)));
|
|
230
|
+
return new Session(this.#link, accepted(reply.answer).session);
|
|
231
|
+
}
|
|
232
|
+
/** Configured Sessions, newest first; archived Sessions are hidden by default. */
|
|
233
|
+
async list(options) {
|
|
234
|
+
const summaries = [];
|
|
235
|
+
let after = "0";
|
|
236
|
+
let ceiling = "0";
|
|
237
|
+
for (;;) {
|
|
238
|
+
const wireAfter = after;
|
|
239
|
+
const wireCeiling = ceiling;
|
|
240
|
+
const page = parse(await this.#link.exchange("/v1/list-sessions", (store) => JSON.stringify({
|
|
241
|
+
version: WIRE_VERSION,
|
|
242
|
+
kind: "list_sessions",
|
|
243
|
+
store,
|
|
244
|
+
workspace: null,
|
|
245
|
+
archive: options?.archive ?? "active",
|
|
246
|
+
after: wireAfter,
|
|
247
|
+
ceiling: wireCeiling,
|
|
248
|
+
})));
|
|
249
|
+
for (const row of page.sessions) {
|
|
250
|
+
summaries.push({ id: row.reference, label: row.label, state: row.work.state.kind, archiveState: row.archive_state, parent: row.parent });
|
|
251
|
+
}
|
|
252
|
+
if (page.next === null || !("after" in page.next))
|
|
253
|
+
return summaries.reverse();
|
|
254
|
+
after = page.next.after;
|
|
255
|
+
ceiling = page.next.ceiling;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** The models each provider offers (D128). */
|
|
260
|
+
export async function models(link) {
|
|
261
|
+
const reply = parse(await link.exchange("/v1/model-list", (store) => JSON.stringify({
|
|
262
|
+
version: WIRE_VERSION,
|
|
263
|
+
kind: "model_list",
|
|
264
|
+
store,
|
|
265
|
+
}), undefined, {
|
|
266
|
+
// A live provider list can take longer than the default response wait.
|
|
267
|
+
responseMs: 30_000,
|
|
268
|
+
}));
|
|
269
|
+
return reply.providers.flatMap((entry) => entry.models.map((model) => ({ provider: entry.provider, id: model.id, known: model.known })));
|
|
270
|
+
}
|
|
271
|
+
/** Holds every new Turn while paused; running work continues (D199). */
|
|
272
|
+
export async function setPaused(link, paused) {
|
|
273
|
+
await link.exchange("/v1/control/host-pause", (store) => JSON.stringify({
|
|
274
|
+
version: WIRE_VERSION,
|
|
275
|
+
kind: "host_pause",
|
|
276
|
+
store,
|
|
277
|
+
paused,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The wire version openapi.json describes. A test keeps the two equal. */
|
|
2
|
+
export declare const WIRE_VERSION = "13";
|
|
3
|
+
export interface Post {
|
|
4
|
+
socketPath: string;
|
|
5
|
+
path: string;
|
|
6
|
+
/** The serialized request JSON. */
|
|
7
|
+
body: string;
|
|
8
|
+
/** The Host instance from host-info; a different instance refuses the call. */
|
|
9
|
+
instance?: string;
|
|
10
|
+
/** Client environment bytes sent ahead of the JSON (`/v1/message`). */
|
|
11
|
+
environment?: Buffer;
|
|
12
|
+
/** `prefix`: resolve with the first this-many bytes of a successful reply and drop the rest. `max`: the most a reply may hold; the default fits the Host's control replies. */
|
|
13
|
+
limits?: ReplyLimits;
|
|
14
|
+
}
|
|
15
|
+
export interface ReplyLimits {
|
|
16
|
+
prefix?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
/** The longest silence before the first reply byte, once the request is sent. Default 5 s. */
|
|
19
|
+
responseMs?: number;
|
|
20
|
+
/** The longest a transfer may make no byte of progress. Default 60 s. */
|
|
21
|
+
idleMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** A failure to reach the Host at all, as opposed to a refusal from it. */
|
|
24
|
+
export declare class ConnectionError extends Error {
|
|
25
|
+
readonly name = "ConnectionError";
|
|
26
|
+
}
|
|
27
|
+
export declare function post(call: Post): Promise<Buffer>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// One POST over the Host's Unix socket. The Host speaks HTTP/1.1, answers
|
|
2
|
+
// once and closes the connection. Nothing here is exported from the package
|
|
3
|
+
// (D19).
|
|
4
|
+
import { request } from "node:http";
|
|
5
|
+
import { RuiRequestError } from "./errors.js";
|
|
6
|
+
/** The wire version openapi.json describes. A test keeps the two equal. */
|
|
7
|
+
export const WIRE_VERSION = "13";
|
|
8
|
+
/** The largest reply body read. The Host's own replies stay far below it. */
|
|
9
|
+
const MAX_REPLY_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
/** After the request is sent, the longest the Host may stay silent before its first reply byte. */
|
|
11
|
+
const RESPONSE_WAIT_MS = 5_000;
|
|
12
|
+
/** The longest a transfer, in either direction, may make no byte of progress. */
|
|
13
|
+
const TRANSFER_IDLE_MS = 60_000;
|
|
14
|
+
/** A failure to reach the Host at all, as opposed to a refusal from it. */
|
|
15
|
+
export class ConnectionError extends Error {
|
|
16
|
+
name = "ConnectionError";
|
|
17
|
+
}
|
|
18
|
+
export function post(call) {
|
|
19
|
+
const json = Buffer.from(call.body);
|
|
20
|
+
const payload = call.environment ? Buffer.concat([call.environment, json]) : json;
|
|
21
|
+
const headers = {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Content-Length": String(payload.length),
|
|
24
|
+
"X-Rui-Wire-Version": WIRE_VERSION,
|
|
25
|
+
};
|
|
26
|
+
if (call.instance !== undefined)
|
|
27
|
+
headers["X-Rui-Host-Instance"] = call.instance;
|
|
28
|
+
if (call.environment !== undefined)
|
|
29
|
+
headers["X-Rui-Environment-Bytes"] = String(call.environment.length);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const outgoing = request({ socketPath: call.socketPath, path: call.path, method: "POST", headers, agent: false });
|
|
32
|
+
const idle = call.limits?.idleMs ?? TRANSFER_IDLE_MS;
|
|
33
|
+
outgoing.setTimeout(idle);
|
|
34
|
+
outgoing.on("timeout", () => outgoing.destroy(new Error("the Host made no progress in time")));
|
|
35
|
+
// Silence after the whole request went out gets the short budget; any byte of progress renews the long one.
|
|
36
|
+
outgoing.on("finish", () => outgoing.setTimeout(call.limits?.responseMs ?? RESPONSE_WAIT_MS));
|
|
37
|
+
outgoing.on("error", (error) => reject(new ConnectionError(error.message, { cause: error })));
|
|
38
|
+
outgoing.on("response", (incoming) => {
|
|
39
|
+
outgoing.setTimeout(idle);
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let length = 0;
|
|
42
|
+
incoming.on("data", (chunk) => {
|
|
43
|
+
if (call.limits?.prefix !== undefined && incoming.statusCode === 200) {
|
|
44
|
+
chunks.push(chunk);
|
|
45
|
+
length += chunk.length;
|
|
46
|
+
if (length >= (call.limits?.prefix ?? Infinity)) {
|
|
47
|
+
incoming.destroy();
|
|
48
|
+
resolve(Buffer.concat(chunks).subarray(0, call.limits?.prefix));
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
length += chunk.length;
|
|
53
|
+
const max = call.limits?.max ?? MAX_REPLY_BYTES;
|
|
54
|
+
if (length > max) {
|
|
55
|
+
incoming.destroy(new Error(`the reply exceeds ${max} bytes`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
});
|
|
60
|
+
incoming.on("error", (error) => reject(new ConnectionError(error.message, { cause: error })));
|
|
61
|
+
incoming.on("end", () => {
|
|
62
|
+
const reply = Buffer.concat(chunks);
|
|
63
|
+
if (incoming.statusCode === 200)
|
|
64
|
+
return resolve(reply);
|
|
65
|
+
reject(refusal(incoming.statusCode, reply));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
outgoing.end(payload);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function refusal(status, reply) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(reply.toString("utf8"));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Fall through: a reply that is not an ErrorReply is reported by status.
|
|
78
|
+
}
|
|
79
|
+
if (parsed && typeof parsed.code === "string" && typeof parsed.type === "string") {
|
|
80
|
+
return new RuiRequestError(parsed.type, parsed.code);
|
|
81
|
+
}
|
|
82
|
+
// A reused key with different input answers 409 with the command's own envelope.
|
|
83
|
+
if (parsed?.answer && typeof parsed.answer.code === "string" && typeof parsed.answer.status === "string") {
|
|
84
|
+
return new RuiRequestError(parsed.answer.status, parsed.answer.code);
|
|
85
|
+
}
|
|
86
|
+
return new RuiRequestError("invocation_error", `http_${status ?? "unknown"}`);
|
|
87
|
+
}
|