@cubos/agent-sdk 0.0.1136563
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 +446 -0
- package/dist/admin/agents.d.ts +191 -0
- package/dist/admin/ai-providers.d.ts +122 -0
- package/dist/admin/channels.d.ts +261 -0
- package/dist/admin/client-tools.d.ts +74 -0
- package/dist/admin/component-libraries.d.ts +63 -0
- package/dist/admin/conversations.d.ts +434 -0
- package/dist/admin/global.d.ts +137 -0
- package/dist/admin/index.d.ts +67 -0
- package/dist/admin/knowledge-bases.d.ts +142 -0
- package/dist/admin/mcps.d.ts +134 -0
- package/dist/admin/paths.d.ts +9 -0
- package/dist/admin/skills.d.ts +43 -0
- package/dist/admin/task-templates.d.ts +185 -0
- package/dist/admin/users.d.ts +208 -0
- package/dist/cache.d.ts +68 -0
- package/dist/client-tools.d.ts +96 -0
- package/dist/client.d.ts +427 -0
- package/dist/errors.d.ts +52 -0
- package/dist/generated/schema.d.ts +13564 -0
- package/dist/http.d.ts +42 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1826 -0
- package/dist/index.js.map +30 -0
- package/dist/mapping.d.ts +170 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/sse.d.ts +48 -0
- package/dist/sse.js +209 -0
- package/dist/sse.js.map +11 -0
- package/dist/types.d.ts +263 -0
- package/package.json +51 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Activity, Block, ClientToolCall, Conversation, Message, PlanSnapshot, Todo, ToolActivity } from "./types.js";
|
|
2
|
+
/** Partial view of the server's `Conversation` DTO. */
|
|
3
|
+
export interface WireConversation {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string | null;
|
|
6
|
+
generated_title: string | null;
|
|
7
|
+
agent: {
|
|
8
|
+
slug: string;
|
|
9
|
+
} | null;
|
|
10
|
+
last_activity_at: string;
|
|
11
|
+
archived_at: string | null;
|
|
12
|
+
is_processing: boolean;
|
|
13
|
+
has_pending_turn: boolean;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
}
|
|
17
|
+
/** Partial view of the server's `ConversationEventAttachment` DTO. Unexported:
|
|
18
|
+
* it is reached only through `WireEvent`, and the public shape is `Attachment`. */
|
|
19
|
+
interface WireAttachment {
|
|
20
|
+
id: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
mime_type: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
label: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** The server's `TurnStatus`, carried as `conversation_status` frames on the
|
|
27
|
+
* event stream. */
|
|
28
|
+
export interface WireTurnStatus {
|
|
29
|
+
is_processing: boolean;
|
|
30
|
+
has_pending_turn: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Partial view of the server's `ConversationEvent` DTO. */
|
|
33
|
+
export interface WireEvent {
|
|
34
|
+
id: string;
|
|
35
|
+
tool_call_id?: string | null;
|
|
36
|
+
tool_name?: string | null;
|
|
37
|
+
seq: number;
|
|
38
|
+
change_seq: number;
|
|
39
|
+
type: string;
|
|
40
|
+
content: string | null;
|
|
41
|
+
tentative: boolean;
|
|
42
|
+
discarded_at: string | null;
|
|
43
|
+
created_at: string;
|
|
44
|
+
/** Absent on older servers and on the many event types that never carry
|
|
45
|
+
* media, so it is read defensively rather than required. */
|
|
46
|
+
attachments?: WireAttachment[];
|
|
47
|
+
/** Absent on every event that isn't an agent message, and on servers older
|
|
48
|
+
* than the feature — hence optional rather than required. */
|
|
49
|
+
blocks?: Block[];
|
|
50
|
+
/** Free-form per event type. Read here for `media_transcription`'s
|
|
51
|
+
* `source_event_id` and as a tool's return value. */
|
|
52
|
+
result?: unknown;
|
|
53
|
+
/** Set on a `tool_result` the tool failed. */
|
|
54
|
+
error?: string | null;
|
|
55
|
+
/** Server-measured wall clock, on the event types that time something. */
|
|
56
|
+
duration_ms?: number | null;
|
|
57
|
+
/** The provider exchange behind an `llm_call`. Read here for one thing only:
|
|
58
|
+
* the arguments of the tool calls it issued. */
|
|
59
|
+
llm_call_data?: WireLlmCallData | null;
|
|
60
|
+
}
|
|
61
|
+
/** The sliver of the server's `LlmCallData` this file reads. Deliberately not
|
|
62
|
+
* the whole snapshot: the rest is the prompt, and nothing on this surface
|
|
63
|
+
* should start depending on its shape. */
|
|
64
|
+
interface WireLlmCallData {
|
|
65
|
+
response?: {
|
|
66
|
+
messages?: Array<{
|
|
67
|
+
type?: string;
|
|
68
|
+
tool_call_id?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
arguments?: unknown;
|
|
71
|
+
}> | null;
|
|
72
|
+
} | null;
|
|
73
|
+
}
|
|
74
|
+
export declare function toConversation(w: WireConversation): Conversation;
|
|
75
|
+
/** The turn status, as the event stream reports it — ordered against the log
|
|
76
|
+
* rather than racing it on a second connection, which is why the conversation
|
|
77
|
+
* row's copy of the same pair is not what a client should watch. */
|
|
78
|
+
export declare function toTurnStatus(w: WireTurnStatus): Activity;
|
|
79
|
+
/**
|
|
80
|
+
* A message, or null for the many event types that aren't one (tool calls, LLM
|
|
81
|
+
* bookkeeping, compaction markers…).
|
|
82
|
+
*
|
|
83
|
+
* `tentative` rows are written by the harness mid-turn and may be rolled back;
|
|
84
|
+
* `discarded_at` marks one that was. Neither belongs in a transcript, and the
|
|
85
|
+
* server re-emits both over SSE precisely so clients can drop them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function toMessage(w: WireEvent): Message | null;
|
|
88
|
+
/**
|
|
89
|
+
* Folds each transcription into the media it belongs to, so a voice message is
|
|
90
|
+
* one bubble with a player and its text rather than two.
|
|
91
|
+
*
|
|
92
|
+
* The harness names the source event, and that is what pairs them. A
|
|
93
|
+
* transcription of an *image* is the fallback vision model's description, not
|
|
94
|
+
* the user's words — the picture is already on screen, so it is dropped rather
|
|
95
|
+
* than shown as something the user said.
|
|
96
|
+
*
|
|
97
|
+
* Rows written before the server recorded a source pair oldest-first instead,
|
|
98
|
+
* the same FIFO rule the harness used then: a user routinely types something
|
|
99
|
+
* between the recording and its transcription, so "the message directly before"
|
|
100
|
+
* is not a safe match.
|
|
101
|
+
*
|
|
102
|
+
* Expects `messages` ordered by `seq`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function mergeVoiceMessages(messages: Message[]): Message[];
|
|
105
|
+
/**
|
|
106
|
+
* The agent's whole current plan, or null when this event isn't one.
|
|
107
|
+
*
|
|
108
|
+
* `todo_update` carries a JSON-encoded `TodoUpdatePayload` in `content` (not in
|
|
109
|
+
* `result` — it's a pseudo-tool, so it writes its own side-effect event), and
|
|
110
|
+
* each call overwrites the entire list rather than patching it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function toTodos(w: WireEvent): Todo[] | null;
|
|
113
|
+
/**
|
|
114
|
+
* The plan revision this event carries, tagged with where it sits in the log.
|
|
115
|
+
*
|
|
116
|
+
* Same content as `toTodos`, plus the `seq` a reader needs to say *which*
|
|
117
|
+
* answer a plan belongs to — without it, a client holding several turns of
|
|
118
|
+
* history can only ever show the newest plan against all of them.
|
|
119
|
+
*/
|
|
120
|
+
export declare function toPlanSnapshot(w: WireEvent): PlanSnapshot | null;
|
|
121
|
+
/**
|
|
122
|
+
* The `seq` of a turn that has finished, or null for every other event.
|
|
123
|
+
*
|
|
124
|
+
* The honest end of a turn, and not the same thing as `isProcessing` going
|
|
125
|
+
* false: a client tool *suspends* the turn while the app answers, and the
|
|
126
|
+
* conversation stops being "processing" for as long as that takes. A UI keyed
|
|
127
|
+
* on activity alone therefore stops its own spinner in the middle of the work
|
|
128
|
+
* and starts it again a second later — which is the flicker this exists to
|
|
129
|
+
* prevent.
|
|
130
|
+
*/
|
|
131
|
+
export declare function toTurnDone(w: WireEvent): number | null;
|
|
132
|
+
/** The agent called a client tool, which suspends its turn until someone
|
|
133
|
+
* answers. Null for every other event. */
|
|
134
|
+
export declare function toClientToolCall(w: WireEvent): ClientToolCall | null;
|
|
135
|
+
/**
|
|
136
|
+
* The agent reaching for a tool, or the outcome of one. Null for every other
|
|
137
|
+
* event.
|
|
138
|
+
*
|
|
139
|
+
* Unlike a message, a `tentative` row is exactly what this wants. A
|
|
140
|
+
* `tool_result` is written tentative and only promoted when the turn
|
|
141
|
+
* consolidates, so dropping those would hide the trail until the turn was over
|
|
142
|
+
* — the opposite of showing what the agent is doing right now. `discarded_at`
|
|
143
|
+
* still disqualifies: that turn was rolled back, so its steps never happened.
|
|
144
|
+
*/
|
|
145
|
+
export declare function toToolActivity(w: WireEvent): ToolActivity | null;
|
|
146
|
+
/**
|
|
147
|
+
* The arguments the model passed, read off the `llm_call` that issued the
|
|
148
|
+
* calls — the only event that carries them for a server-side tool, since a
|
|
149
|
+
* `tool_result` records the outcome and never the input.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately **not** an activity. A pseudo-tool (`send_message`,
|
|
152
|
+
* `yield_turn`, `update_todo`) writes its own side-effect event instead of a
|
|
153
|
+
* `tool_result`, so minting an entry from every call here would leave three
|
|
154
|
+
* steps per turn stuck at `running` forever. These only ever fill in the
|
|
155
|
+
* arguments of an entry that already exists.
|
|
156
|
+
*/
|
|
157
|
+
export declare function toToolCallArguments(w: WireEvent): ToolCallArguments[];
|
|
158
|
+
export interface ToolCallArguments {
|
|
159
|
+
toolCallId: string;
|
|
160
|
+
arguments: Record<string, unknown>;
|
|
161
|
+
}
|
|
162
|
+
/** Fills in what an activity could not know about itself. Never overwrites: a
|
|
163
|
+
* client tool reports its own arguments, and those are what it was actually
|
|
164
|
+
* dispatched with. */
|
|
165
|
+
export declare function withToolArguments(activity: ToolActivity, known: Map<string, Record<string, unknown>>): ToolActivity;
|
|
166
|
+
/** Folds each call together with its result, so one tool is one entry.
|
|
167
|
+
*
|
|
168
|
+
* Expects activities in any order and returns them by `seq`. */
|
|
169
|
+
export declare function mergeToolActivities(activities: ToolActivity[]): ToolActivity[];
|
|
170
|
+
export {};
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Just the call signature, not `typeof fetch` — that also demands runtime
|
|
2
|
+
* extras (Bun's `preconnect`, undici's statics) a caller's wrapper won't have. */
|
|
3
|
+
export type FetchLike = (input: string, init?: {
|
|
4
|
+
method?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
body?: string | FormData;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}) => Promise<Response>;
|
|
9
|
+
export interface SseOptions<T> {
|
|
10
|
+
url: string;
|
|
11
|
+
/**
|
|
12
|
+
* Only frames with one of these `event:` names are delivered; the rest are
|
|
13
|
+
* dropped. Several names on one connection is how a stream keeps two kinds of
|
|
14
|
+
* frame in a single order — split across two connections there is none, and a
|
|
15
|
+
* client cannot tell which came first.
|
|
16
|
+
*/
|
|
17
|
+
event: string | string[];
|
|
18
|
+
onEvent: (data: T, event: string) => void;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
/** Called before each (re)connect, so the caller can mint a fresh token. */
|
|
21
|
+
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
22
|
+
/** Called once per successful connect, before any frame is delivered. The
|
|
23
|
+
* hook a caller needs to catch up on state the stream won't replay: it fires
|
|
24
|
+
* on every reconnect too, so a gap the backoff swallowed is covered as
|
|
25
|
+
* well. Not awaited — a slow catch-up must not stall frame delivery. */
|
|
26
|
+
onOpen?: () => void;
|
|
27
|
+
/** Resume cursor for the first connect. Later reconnects use the last `id:`
|
|
28
|
+
* the server actually sent. */
|
|
29
|
+
lastEventId?: string;
|
|
30
|
+
fetchImpl?: FetchLike;
|
|
31
|
+
onError?: (err: unknown) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads `url` until `signal` aborts, reconnecting through any drop — clean
|
|
35
|
+
* close from the server included, since intermediaries (Cloudflare et al.)
|
|
36
|
+
* close idle SSE connections without warning and the caller would otherwise
|
|
37
|
+
* silently stop receiving.
|
|
38
|
+
*
|
|
39
|
+
* `Last-Event-ID` is replayed from the last frame the server sent, so the
|
|
40
|
+
* backend's catch-up query fills whatever the gap swallowed. 4xx ends the loop
|
|
41
|
+
* — a bad token or a deleted conversation won't fix itself by retrying.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readSse<T>(opts: SseOptions<T>): Promise<void>;
|
|
44
|
+
export declare function parseFrame<T>(frame: string, expectedEvent: string | string[]): {
|
|
45
|
+
data: T;
|
|
46
|
+
id: string | null;
|
|
47
|
+
event: string;
|
|
48
|
+
} | null;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
class AgentError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "AgentError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class AgentApiError extends AgentError {
|
|
10
|
+
status;
|
|
11
|
+
requestId;
|
|
12
|
+
body;
|
|
13
|
+
constructor(message, status, requestId = null, body = "") {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AgentApiError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.requestId = requestId;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
json() {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(this.body);
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get isAuthError() {
|
|
28
|
+
return this.status === 401 || this.status === 403;
|
|
29
|
+
}
|
|
30
|
+
get isNotFound() {
|
|
31
|
+
return this.status === 404;
|
|
32
|
+
}
|
|
33
|
+
get isConflict() {
|
|
34
|
+
return this.status === 409;
|
|
35
|
+
}
|
|
36
|
+
get isRetryable() {
|
|
37
|
+
return this.status === 429 || this.status >= 500;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class AgentConfigError extends AgentError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "AgentConfigError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class AgentNetworkError extends AgentError {
|
|
49
|
+
cause;
|
|
50
|
+
timedOut;
|
|
51
|
+
constructor(message, cause, timedOut = false) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "AgentNetworkError";
|
|
54
|
+
this.cause = cause;
|
|
55
|
+
this.timedOut = timedOut;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
var DEFAULT_MESSAGES = {
|
|
59
|
+
400: "Invalid request.",
|
|
60
|
+
401: "Not authenticated.",
|
|
61
|
+
403: "Not allowed.",
|
|
62
|
+
404: "Not found.",
|
|
63
|
+
409: "Conflicts with the current state.",
|
|
64
|
+
413: "Payload too large.",
|
|
65
|
+
429: "Rate limited."
|
|
66
|
+
};
|
|
67
|
+
async function raiseForStatus(res, fallback) {
|
|
68
|
+
if (res.ok)
|
|
69
|
+
return;
|
|
70
|
+
let detail = "";
|
|
71
|
+
try {
|
|
72
|
+
detail = await res.text();
|
|
73
|
+
} catch {
|
|
74
|
+
detail = "";
|
|
75
|
+
}
|
|
76
|
+
const base = DEFAULT_MESSAGES[res.status] ?? fallback;
|
|
77
|
+
throw new AgentApiError(detail ? `${base} (${detail.slice(0, 500)})` : base, res.status, res.headers.get("x-request-id"), detail);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/sse.ts
|
|
81
|
+
var MAX_BACKOFF_MS = 30000;
|
|
82
|
+
|
|
83
|
+
class Fatal extends Error {
|
|
84
|
+
cause;
|
|
85
|
+
constructor(cause) {
|
|
86
|
+
super("fatal stream error");
|
|
87
|
+
this.cause = cause;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readSse(opts) {
|
|
91
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
92
|
+
let lastEventId = opts.lastEventId;
|
|
93
|
+
let attempt = 0;
|
|
94
|
+
while (!opts.signal.aborted) {
|
|
95
|
+
let madeProgress = false;
|
|
96
|
+
try {
|
|
97
|
+
const headers = {
|
|
98
|
+
...await opts.headers?.(),
|
|
99
|
+
Accept: "text/event-stream"
|
|
100
|
+
};
|
|
101
|
+
if (lastEventId !== undefined)
|
|
102
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
103
|
+
const res = await doFetch(opts.url, { headers, signal: opts.signal });
|
|
104
|
+
if (opts.signal.aborted)
|
|
105
|
+
return;
|
|
106
|
+
if (res.ok && res.body)
|
|
107
|
+
opts.onOpen?.();
|
|
108
|
+
if (!res.ok || !res.body) {
|
|
109
|
+
if (res.status >= 400 && res.status < 500) {
|
|
110
|
+
await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {
|
|
111
|
+
throw new Fatal(err);
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`stream open failed with HTTP ${res.status}`);
|
|
116
|
+
}
|
|
117
|
+
for await (const frame of frames(res.body, opts.signal)) {
|
|
118
|
+
const parsed = parseFrame(frame, opts.event);
|
|
119
|
+
if (parsed === null)
|
|
120
|
+
continue;
|
|
121
|
+
if (parsed.id !== null)
|
|
122
|
+
lastEventId = parsed.id;
|
|
123
|
+
opts.onEvent(parsed.data, parsed.event);
|
|
124
|
+
madeProgress = true;
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (opts.signal.aborted)
|
|
128
|
+
return;
|
|
129
|
+
if (err instanceof Fatal)
|
|
130
|
+
throw err.cause;
|
|
131
|
+
opts.onError?.(err);
|
|
132
|
+
}
|
|
133
|
+
if (opts.signal.aborted)
|
|
134
|
+
return;
|
|
135
|
+
if (madeProgress)
|
|
136
|
+
attempt = 0;
|
|
137
|
+
const delayMs = Math.min(MAX_BACKOFF_MS, 1000 * 2 ** attempt);
|
|
138
|
+
attempt += 1;
|
|
139
|
+
await sleep(delayMs, opts.signal);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function* frames(body, signal) {
|
|
143
|
+
const reader = body.getReader();
|
|
144
|
+
const decoder = new TextDecoder;
|
|
145
|
+
let buffer = "";
|
|
146
|
+
try {
|
|
147
|
+
while (!signal.aborted) {
|
|
148
|
+
const { value, done } = await reader.read();
|
|
149
|
+
if (done)
|
|
150
|
+
return;
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
for (;; ) {
|
|
153
|
+
const sep = buffer.indexOf(`
|
|
154
|
+
|
|
155
|
+
`);
|
|
156
|
+
if (sep === -1)
|
|
157
|
+
break;
|
|
158
|
+
yield buffer.slice(0, sep);
|
|
159
|
+
buffer = buffer.slice(sep + 2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
reader.cancel().catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function parseFrame(frame, expectedEvent) {
|
|
167
|
+
let dataLine = null;
|
|
168
|
+
let id = null;
|
|
169
|
+
let eventName = "message";
|
|
170
|
+
for (const line of frame.split(`
|
|
171
|
+
`)) {
|
|
172
|
+
if (line.startsWith(":"))
|
|
173
|
+
continue;
|
|
174
|
+
if (line.startsWith("data:"))
|
|
175
|
+
dataLine = line.slice(5).trimStart();
|
|
176
|
+
else if (line.startsWith("event:"))
|
|
177
|
+
eventName = line.slice(6).trim();
|
|
178
|
+
else if (line.startsWith("id:"))
|
|
179
|
+
id = line.slice(3).trim();
|
|
180
|
+
}
|
|
181
|
+
const wanted = typeof expectedEvent === "string" ? eventName === expectedEvent : expectedEvent.includes(eventName);
|
|
182
|
+
if (!wanted || dataLine === null)
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
return { data: JSON.parse(dataLine), id, event: eventName };
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function sleep(ms, signal) {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const onAbort = () => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
resolve();
|
|
195
|
+
};
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
signal.removeEventListener("abort", onAbort);
|
|
198
|
+
resolve();
|
|
199
|
+
}, ms);
|
|
200
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
export {
|
|
204
|
+
readSse,
|
|
205
|
+
parseFrame
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
//# debugId=85857292E5DF4F6B64756E2164756E21
|
|
209
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/errors.ts", "../src/sse.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** Base for everything this SDK throws, so `catch (e) { if (e instanceof\n * AgentError) }` covers both an HTTP failure and a connection that never got\n * there. */\nexport class AgentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AgentError\";\n }\n}\n\n/** The server answered, and said no. */\nexport class AgentApiError extends AgentError {\n readonly status: number;\n /** Server's `x-request-id`, when present. Worth quoting in a bug report. */\n readonly requestId: string | null;\n /** The response body, verbatim and untruncated. Some routes explain the\n * failure in there (an MCP probe's reason, a rejected cron) in a form worth\n * showing the user; `message` only carries a truncated preview. */\n readonly body: string;\n\n constructor(message: string, status: number, requestId: string | null = null, body = \"\") {\n super(message);\n this.name = \"AgentApiError\";\n this.status = status;\n this.requestId = requestId;\n this.body = body;\n }\n\n /** The body parsed as JSON, or `undefined` when it isn't. */\n json<T = unknown>(): T | undefined {\n try {\n return JSON.parse(this.body) as T;\n } catch {\n return undefined;\n }\n }\n\n /** The token was rejected. The client already retried once with a fresh one,\n * so seeing this means `getToken` is handing back something unusable. */\n get isAuthError(): boolean {\n return this.status === 401 || this.status === 403;\n }\n\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** The request clashed with current state — a slug already taken, a\n * conversation that belongs to a channel, a user already blocked. */\n get isConflict(): boolean {\n return this.status === 409;\n }\n\n /** Worth retrying after a pause: the server is overloaded or briefly down. */\n get isRetryable(): boolean {\n return this.status === 429 || this.status >= 500;\n }\n}\n\n/**\n * The client was constructed wrong — a `baseUrl` with no scheme, most often.\n * Thrown at construction, not on the first call, so the stack points at the\n * mistake.\n */\nexport class AgentConfigError extends AgentError {\n constructor(message: string) {\n super(message);\n this.name = \"AgentConfigError\";\n }\n}\n\n/**\n * The request never produced a response: DNS, TLS, a refused connection, CORS,\n * or the timeout below.\n *\n * Without this, `fetch` rejects with a bare `TypeError: fetch failed` and the\n * caller cannot tell a wrong `baseUrl` from a server that said 500 — the two\n * need completely different fixes.\n */\nexport class AgentNetworkError extends AgentError {\n /** Whatever `fetch` (or the abort) threw. */\n readonly cause: unknown;\n /** True when the SDK's own timeout fired rather than the network failing. */\n readonly timedOut: boolean;\n\n constructor(message: string, cause: unknown, timedOut = false) {\n super(message);\n this.name = \"AgentNetworkError\";\n this.cause = cause;\n this.timedOut = timedOut;\n }\n}\n\nconst DEFAULT_MESSAGES: Record<number, string> = {\n 400: \"Invalid request.\",\n 401: \"Not authenticated.\",\n 403: \"Not allowed.\",\n 404: \"Not found.\",\n 409: \"Conflicts with the current state.\",\n 413: \"Payload too large.\",\n 429: \"Rate limited.\",\n};\n\nexport async function raiseForStatus(res: Response, fallback: string): Promise<void> {\n if (res.ok) return;\n let detail = \"\";\n try {\n detail = await res.text();\n } catch {\n detail = \"\";\n }\n const base = DEFAULT_MESSAGES[res.status] ?? fallback;\n throw new AgentApiError(\n detail ? `${base} (${detail.slice(0, 500)})` : base,\n res.status,\n res.headers.get(\"x-request-id\"),\n detail,\n );\n}\n",
|
|
6
|
+
"// A `fetch`-based Server-Sent Events reader. Not `EventSource`: that can't\n// attach an Authorization header, which every stream here requires.\n//\n// Runtime-agnostic on purpose — `fetch`, `ReadableStream` and `AbortController`\n// only, no DOM. Exported as its own entry point (`@cubos/agent-sdk/sse`) so the\n// operator dashboard can reuse it without adopting the rest of the client.\n\nimport { raiseForStatus } from \"./errors.js\";\n\n/** Just the call signature, not `typeof fetch` — that also demands runtime\n * extras (Bun's `preconnect`, undici's statics) a caller's wrapper won't have. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string | FormData;\n signal?: AbortSignal;\n },\n) => Promise<Response>;\n\nexport interface SseOptions<T> {\n url: string;\n /**\n * Only frames with one of these `event:` names are delivered; the rest are\n * dropped. Several names on one connection is how a stream keeps two kinds of\n * frame in a single order — split across two connections there is none, and a\n * client cannot tell which came first.\n */\n event: string | string[];\n onEvent: (data: T, event: string) => void;\n signal: AbortSignal;\n /** Called before each (re)connect, so the caller can mint a fresh token. */\n headers?: () => Promise<Record<string, string>> | Record<string, string>;\n /** Called once per successful connect, before any frame is delivered. The\n * hook a caller needs to catch up on state the stream won't replay: it fires\n * on every reconnect too, so a gap the backoff swallowed is covered as\n * well. Not awaited — a slow catch-up must not stall frame delivery. */\n onOpen?: () => void;\n /** Resume cursor for the first connect. Later reconnects use the last `id:`\n * the server actually sent. */\n lastEventId?: string;\n fetchImpl?: FetchLike;\n onError?: (err: unknown) => void;\n}\n\nconst MAX_BACKOFF_MS = 30_000;\n\n/** Marks an error the reconnect loop must not swallow. */\nclass Fatal extends Error {\n override readonly cause: unknown;\n constructor(cause: unknown) {\n super(\"fatal stream error\");\n this.cause = cause;\n }\n}\n\n/**\n * Reads `url` until `signal` aborts, reconnecting through any drop — clean\n * close from the server included, since intermediaries (Cloudflare et al.)\n * close idle SSE connections without warning and the caller would otherwise\n * silently stop receiving.\n *\n * `Last-Event-ID` is replayed from the last frame the server sent, so the\n * backend's catch-up query fills whatever the gap swallowed. 4xx ends the loop\n * — a bad token or a deleted conversation won't fix itself by retrying.\n */\nexport async function readSse<T>(opts: SseOptions<T>): Promise<void> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n let lastEventId = opts.lastEventId;\n let attempt = 0;\n\n while (!opts.signal.aborted) {\n let madeProgress = false;\n try {\n const headers: Record<string, string> = {\n ...(await opts.headers?.()),\n Accept: \"text/event-stream\",\n };\n if (lastEventId !== undefined) headers[\"Last-Event-ID\"] = lastEventId;\n\n const res = await doFetch(opts.url, { headers, signal: opts.signal });\n if (opts.signal.aborted) return;\n if (res.ok && res.body) opts.onOpen?.();\n\n if (!res.ok || !res.body) {\n if (res.status >= 400 && res.status < 500) {\n // Wrapped so the catch below can tell it apart from a transient\n // failure and rethrow instead of reconnecting forever.\n await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {\n throw new Fatal(err);\n });\n return;\n }\n throw new Error(`stream open failed with HTTP ${res.status}`);\n }\n\n for await (const frame of frames(res.body, opts.signal)) {\n const parsed = parseFrame<T>(frame, opts.event);\n if (parsed === null) continue;\n if (parsed.id !== null) lastEventId = parsed.id;\n opts.onEvent(parsed.data, parsed.event);\n madeProgress = true;\n }\n } catch (err) {\n if (opts.signal.aborted) return;\n // A bad token or a deleted conversation won't fix itself by retrying.\n if (err instanceof Fatal) throw err.cause;\n // Everything else is transient (network, 5xx, proxy hangup) — report and\n // back off rather than end the subscription.\n opts.onError?.(err);\n }\n\n if (opts.signal.aborted) return;\n\n // Any delivered frame resets the backoff: a long-lived stream that did real\n // work and then dropped should come back fast, not wait out the ceiling.\n if (madeProgress) attempt = 0;\n const delayMs = Math.min(MAX_BACKOFF_MS, 1_000 * 2 ** attempt);\n attempt += 1;\n await sleep(delayMs, opts.signal);\n }\n}\n\nasync function* frames(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n): AsyncGenerator<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n while (!signal.aborted) {\n const { value, done } = await reader.read();\n if (done) return;\n buffer += decoder.decode(value, { stream: true });\n for (;;) {\n const sep = buffer.indexOf(\"\\n\\n\");\n if (sep === -1) break;\n yield buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n }\n }\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n\nexport function parseFrame<T>(\n frame: string,\n expectedEvent: string | string[],\n): { data: T; id: string | null; event: string } | null {\n let dataLine: string | null = null;\n let id: string | null = null;\n let eventName = \"message\";\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\":\")) continue;\n if (line.startsWith(\"data:\")) dataLine = line.slice(5).trimStart();\n else if (line.startsWith(\"event:\")) eventName = line.slice(6).trim();\n else if (line.startsWith(\"id:\")) id = line.slice(3).trim();\n }\n const wanted =\n typeof expectedEvent === \"string\"\n ? eventName === expectedEvent\n : expectedEvent.includes(eventName);\n if (!wanted || dataLine === null) return null;\n try {\n return { data: JSON.parse(dataLine) as T, id, event: eventName };\n } catch {\n return null;\n }\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "85857292E5DF4F6B64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|