@ccmsg/protocol 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 +33 -0
- package/package.json +33 -0
- package/src/attributes.ts +353 -0
- package/src/common/hello.ts +73 -0
- package/src/common/ping.ts +37 -0
- package/src/common/shutdown.ts +13 -0
- package/src/common/topics.ts +99 -0
- package/src/control/agents.ts +64 -0
- package/src/control/files.ts +323 -0
- package/src/control/kv.ts +108 -0
- package/src/control/launcher.ts +100 -0
- package/src/control/llm.ts +494 -0
- package/src/control/peers.ts +127 -0
- package/src/control/sandbox.ts +58 -0
- package/src/control/session-errors.ts +27 -0
- package/src/control/session-status.ts +311 -0
- package/src/control/session.ts +251 -0
- package/src/control/transcript.ts +80 -0
- package/src/control/translate.ts +33 -0
- package/src/envelope.ts +117 -0
- package/src/errors.ts +63 -0
- package/src/identifiers.ts +62 -0
- package/src/index.ts +25 -0
- package/src/messaging/message.ts +92 -0
- package/src/messaging/notify.ts +35 -0
- package/src/messaging/say.ts +35 -0
- package/src/schemas.ts +195 -0
- package/src/upstream.ts +17 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import { request, response, topicFrame } from "../envelope.ts";
|
|
3
|
+
import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
+
import { upstream } from "../upstream.ts";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// quota
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Asks the gateway what quota each credential has left.
|
|
11
|
+
*
|
|
12
|
+
* The instance fetches rather than the client: the gateway is an internal
|
|
13
|
+
* address a browser could not reach anyway, and routing the call keeps that
|
|
14
|
+
* address out of the browser entirely. The reply takes as long as the fetch. */
|
|
15
|
+
export const LlmUsageReadArgs = Type.Object({
|
|
16
|
+
/** Have the gateway ask upstream instead of answering from its cache. Only a
|
|
17
|
+
* probe carries the per-credential limits, and it is the only request that
|
|
18
|
+
* can spend upstream rate limit — an account already limited answers a probe
|
|
19
|
+
* with a refusal that arrives as that credential's error. So this belongs to
|
|
20
|
+
* a deliberate action and never to polling. */
|
|
21
|
+
refresh: Type.Optional(Type.Boolean()),
|
|
22
|
+
});
|
|
23
|
+
export type LlmUsageReadArgs = Static<typeof LlmUsageReadArgs>;
|
|
24
|
+
|
|
25
|
+
/** One rolling quota window of one credential. */
|
|
26
|
+
export const LlmUsageWindow = Type.Object(
|
|
27
|
+
{
|
|
28
|
+
/** Share of the window's quota already spent, from 0 to 1. Values above 1
|
|
29
|
+
* happen and are not clamped. */
|
|
30
|
+
utilization: Type.Number(),
|
|
31
|
+
/** The provider's verdict for the window. An open set, kept as sent: an
|
|
32
|
+
* unknown verdict has to reach the screen as itself rather than be
|
|
33
|
+
* flattened into a wrong one. */
|
|
34
|
+
status: Type.String(),
|
|
35
|
+
/** When the window's counter rolls over. */
|
|
36
|
+
reset_at: Type.Optional(Timestamp),
|
|
37
|
+
/** How long the window is. The provider states it rather than leaving it to
|
|
38
|
+
* be read out of the window's name, because a name can describe a slot
|
|
39
|
+
* whose length differs between providers. Absent means unknown. */
|
|
40
|
+
window_secs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
41
|
+
/** The reading predates the reset, so it is not current consumption. Without
|
|
42
|
+
* this, a week-old figure over 100% reads as a credential that is out of
|
|
43
|
+
* quota right now. */
|
|
44
|
+
expired: Type.Optional(Type.Boolean()),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
$id: "LlmUsageWindow",
|
|
48
|
+
...upstream("llm-gateway", "one quota window as the provider states it"),
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
export type LlmUsageWindow = Static<typeof LlmUsageWindow>;
|
|
52
|
+
|
|
53
|
+
/** Extra-credit spending, which belongs to the credential rather than to any
|
|
54
|
+
* one window: a credential can be out of credits with every window still
|
|
55
|
+
* allowed. */
|
|
56
|
+
export const LlmUsageOverage = Type.Object(
|
|
57
|
+
{
|
|
58
|
+
status: Type.String(),
|
|
59
|
+
disabled_reason: Type.Optional(Type.String()),
|
|
60
|
+
},
|
|
61
|
+
{ $id: "LlmUsageOverage" },
|
|
62
|
+
);
|
|
63
|
+
export type LlmUsageOverage = Static<typeof LlmUsageOverage>;
|
|
64
|
+
|
|
65
|
+
/** One reading of a credential's quota. It can lag the reply that carries it by
|
|
66
|
+
* minutes when the gateway has seen no traffic on that credential, which is why
|
|
67
|
+
* it states when it was taken. */
|
|
68
|
+
export const LlmUsageSnapshot = Type.Object(
|
|
69
|
+
{
|
|
70
|
+
observed_at: Type.Optional(Timestamp),
|
|
71
|
+
overage: Type.Optional(LlmUsageOverage),
|
|
72
|
+
/** Window name to window, names as the provider gives them. A map rather
|
|
73
|
+
* than named fields so a provider that starts reporting a third window
|
|
74
|
+
* appears without a change here. */
|
|
75
|
+
windows: Type.Record(Type.String(), LlmUsageWindow),
|
|
76
|
+
},
|
|
77
|
+
{ $id: "LlmUsageSnapshot" },
|
|
78
|
+
);
|
|
79
|
+
export type LlmUsageSnapshot = Static<typeof LlmUsageSnapshot>;
|
|
80
|
+
|
|
81
|
+
/** A named limit the provider enforces beside the rolling windows. */
|
|
82
|
+
export const LlmUsageLimit = Type.Object(
|
|
83
|
+
{
|
|
84
|
+
/** Which limit this is, in the provider's vocabulary. An open set. */
|
|
85
|
+
kind: Type.String(),
|
|
86
|
+
/** Share spent, as a percentage from 0 to 100 — deliberately not the same
|
|
87
|
+
* unit as a window's utilization, because this is the provider's own figure
|
|
88
|
+
* and the wire stays a faithful copy of it. */
|
|
89
|
+
percent: Type.Number(),
|
|
90
|
+
/** The provider's own verdict. An open set. */
|
|
91
|
+
severity: Type.String(),
|
|
92
|
+
/** Absent for a limit with no scheduled reset. */
|
|
93
|
+
resets_at: Type.Optional(Timestamp),
|
|
94
|
+
/** The model family a scoped limit applies to, as a display name. */
|
|
95
|
+
model: Type.Optional(Type.String()),
|
|
96
|
+
/** The provider is currently counting against this limit. Not a statement
|
|
97
|
+
* that it is blocking anything: an idle limit can sit near its ceiling and
|
|
98
|
+
* an active one near zero. */
|
|
99
|
+
is_active: Type.Optional(Type.Boolean()),
|
|
100
|
+
/** How long the limit's period is, as the provider states it rather than as
|
|
101
|
+
* inferred from its kind. Absent means unknown. */
|
|
102
|
+
window_secs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
103
|
+
},
|
|
104
|
+
{ $id: "LlmUsageLimit" },
|
|
105
|
+
);
|
|
106
|
+
export type LlmUsageLimit = Static<typeof LlmUsageLimit>;
|
|
107
|
+
|
|
108
|
+
/** How the credential's own authentication is holding up. Separate from its
|
|
109
|
+
* quota readings: a credential whose login has expired still has its last
|
|
110
|
+
* snapshot, and nothing in that snapshot says why the numbers stopped moving. */
|
|
111
|
+
export const LlmUsageAuth = Type.Object(
|
|
112
|
+
{
|
|
113
|
+
/** An open set. Anything unfamiliar means there is nothing to announce, not
|
|
114
|
+
* that something is wrong. */
|
|
115
|
+
status: Type.String(),
|
|
116
|
+
/** What to do about it, in the gateway's words. Present when the status is
|
|
117
|
+
* not a healthy one. */
|
|
118
|
+
reason: Type.Optional(Type.String()),
|
|
119
|
+
observed_at: Type.Optional(Timestamp),
|
|
120
|
+
/** Where a person can log in again. Absolute: the gateway states a path,
|
|
121
|
+
* since it does not know the address it is published under, and the instance
|
|
122
|
+
* resolves it against the endpoint it fetched — resolving it a second time
|
|
123
|
+
* against a client's own address would point at the wrong host. Present only
|
|
124
|
+
* for a credential a browser can actually re-authenticate. */
|
|
125
|
+
login_url: Type.Optional(Type.String()),
|
|
126
|
+
},
|
|
127
|
+
{ $id: "LlmUsageAuth" },
|
|
128
|
+
);
|
|
129
|
+
export type LlmUsageAuth = Static<typeof LlmUsageAuth>;
|
|
130
|
+
|
|
131
|
+
export const LlmUsageCredential = Type.Object(
|
|
132
|
+
{
|
|
133
|
+
name: Type.String(),
|
|
134
|
+
/** What kind of credential it is. An open set. */
|
|
135
|
+
type: Type.Optional(Type.String()),
|
|
136
|
+
/** Whether quota is knowable for this credential at all: observed, not
|
|
137
|
+
* applicable, or dependent on something further upstream. An open set. */
|
|
138
|
+
support: Type.String(),
|
|
139
|
+
/** Absent means nothing is known about the authentication, never that it is
|
|
140
|
+
* healthy. */
|
|
141
|
+
auth: Type.Optional(LlmUsageAuth),
|
|
142
|
+
/** Present when quota is observed for this credential. */
|
|
143
|
+
snapshot: Type.Optional(LlmUsageSnapshot),
|
|
144
|
+
/** In the provider's order. Absent means none were reported, which a client
|
|
145
|
+
* shows as nothing rather than as a fault. */
|
|
146
|
+
limits: Type.Optional(Type.Array(LlmUsageLimit)),
|
|
147
|
+
/** Why the last refresh of this credential failed. Whatever snapshot sits
|
|
148
|
+
* beside it is the last good reading, and its time says how old that is. */
|
|
149
|
+
probe_error: Type.Optional(Type.String()),
|
|
150
|
+
},
|
|
151
|
+
{ $id: "LlmUsageCredential" },
|
|
152
|
+
);
|
|
153
|
+
export type LlmUsageCredential = Static<typeof LlmUsageCredential>;
|
|
154
|
+
|
|
155
|
+
export const LlmUsageReadResult = Type.Object({
|
|
156
|
+
/** When the gateway assembled the answer. */
|
|
157
|
+
generated_at: Type.Optional(Timestamp),
|
|
158
|
+
credentials: Type.Array(LlmUsageCredential),
|
|
159
|
+
});
|
|
160
|
+
export type LlmUsageReadResult = Static<typeof LlmUsageReadResult>;
|
|
161
|
+
|
|
162
|
+
export const LlmUsageReadRequest = request("llm_usage_read", LlmUsageReadArgs);
|
|
163
|
+
export const LlmUsageReadResponse = response("llm_usage_read", LlmUsageReadResult);
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// spend
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** Asks the gateway what the host's credentials have cost, by day. */
|
|
170
|
+
export const LlmStatsReadArgs = Type.Object({
|
|
171
|
+
/** How far back to ask for. The gateway narrows a request wider than its own
|
|
172
|
+
* history, so asking for more than it holds is the supported way to say
|
|
173
|
+
* "everything". Absent leaves the gateway's own default. */
|
|
174
|
+
days: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
175
|
+
});
|
|
176
|
+
export type LlmStatsReadArgs = Static<typeof LlmStatsReadArgs>;
|
|
177
|
+
|
|
178
|
+
/** What one model cost on one day under one credential. Every counter is
|
|
179
|
+
* optional and passed through as sent: which counters exist is the gateway's to
|
|
180
|
+
* decide, and a missing one has to read as "not reported" rather than as a zero
|
|
181
|
+
* something would then add up. */
|
|
182
|
+
export const LlmStatsModelUsage = Type.Object(
|
|
183
|
+
{
|
|
184
|
+
requests: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
185
|
+
input_tokens: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
186
|
+
output_tokens: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
187
|
+
cache_creation_input_tokens: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
188
|
+
cache_read_input_tokens: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
189
|
+
/** Spend in USD. */
|
|
190
|
+
usd: Type.Optional(Type.Number()),
|
|
191
|
+
},
|
|
192
|
+
{ $id: "LlmStatsModelUsage" },
|
|
193
|
+
);
|
|
194
|
+
export type LlmStatsModelUsage = Static<typeof LlmStatsModelUsage>;
|
|
195
|
+
|
|
196
|
+
/** One day's spend, by credential and then by model. */
|
|
197
|
+
export const LlmStatsDay = Type.Object(
|
|
198
|
+
{
|
|
199
|
+
credentials: Type.Record(Type.String(), Type.Record(Type.String(), LlmStatsModelUsage)),
|
|
200
|
+
/** The gateway's own total, kept rather than recomputed: it is the
|
|
201
|
+
* authoritative figure, and it can differ from the sum when the gateway
|
|
202
|
+
* counts something it does not break out. */
|
|
203
|
+
total_usd: Type.Optional(Type.Number()),
|
|
204
|
+
},
|
|
205
|
+
{ $id: "LlmStatsDay" },
|
|
206
|
+
);
|
|
207
|
+
export type LlmStatsDay = Static<typeof LlmStatsDay>;
|
|
208
|
+
|
|
209
|
+
export const LlmStatsReadResult = Type.Object({
|
|
210
|
+
generated_at: Type.Optional(Timestamp),
|
|
211
|
+
/** Keyed `YYYY-MM-DD` in the gateway's own timezone. A day here means
|
|
212
|
+
* whatever the gateway means by it; nothing reinterprets the dates. */
|
|
213
|
+
days: Type.Record(Type.String(), LlmStatsDay),
|
|
214
|
+
});
|
|
215
|
+
export type LlmStatsReadResult = Static<typeof LlmStatsReadResult>;
|
|
216
|
+
|
|
217
|
+
export const LlmStatsReadRequest = request("llm_stats_read", LlmStatsReadArgs);
|
|
218
|
+
export const LlmStatsReadResponse = response("llm_stats_read", LlmStatsReadResult);
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// live requests
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/** How long a prompt cache window is assumed to last when an event states
|
|
225
|
+
* neither its deadline nor where the request came from. Both sides count down
|
|
226
|
+
* to the same instant, so the assumption has to be shared; whenever the gateway
|
|
227
|
+
* states the real length, that wins over this. */
|
|
228
|
+
export const LLM_PROMPT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
229
|
+
|
|
230
|
+
/** One request the gateway saw go upstream.
|
|
231
|
+
*
|
|
232
|
+
* The session id is the one the gateway read off the request, which is the same
|
|
233
|
+
* identifier sessions are known by here — that shared key is what lets a client
|
|
234
|
+
* put a countdown on a session. Requests the gateway could not attribute to a
|
|
235
|
+
* session never appear. */
|
|
236
|
+
export const LlmRequestInfo = Type.Object(
|
|
237
|
+
{
|
|
238
|
+
/** When the upstream response headers arrived, which is when the prompt
|
|
239
|
+
* cache starts running down — not when the request was sent. */
|
|
240
|
+
received_at: Timestamp,
|
|
241
|
+
sid: Sid,
|
|
242
|
+
/** The instance whose gateway saw it. */
|
|
243
|
+
instance: InstanceId,
|
|
244
|
+
/** Names the conversation series: a session's subagents travel under the
|
|
245
|
+
* same session id but a different series, and their caches are genuinely
|
|
246
|
+
* separate, so a cache window belongs to the pair and never to the session
|
|
247
|
+
* alone. Absent when the gateway reports none, which collapses those events
|
|
248
|
+
* into one unnamed series per session. Series names are not unique across
|
|
249
|
+
* sessions, which is the other half of why the pair is the key. */
|
|
250
|
+
prefix: Type.Optional(Type.String()),
|
|
251
|
+
/** True for the series the session's own turns keep warm, as opposed to a
|
|
252
|
+
* subagent's. The instance decides it so that every client agrees on which
|
|
253
|
+
* window is the session's. */
|
|
254
|
+
main: Type.Boolean(),
|
|
255
|
+
/** Whose turn issued the request, as the gateway read it. An open set; this
|
|
256
|
+
* is one of the inputs to `main`, and `main` is the verdict clients read. */
|
|
257
|
+
origin: Type.Optional(Type.String()),
|
|
258
|
+
/** When this series' cache goes cold, as the gateway computed it. Absent
|
|
259
|
+
* when the request cached nothing, in which case the window closed as it
|
|
260
|
+
* opened. A keepalive arrives as another event on the same series, so a live
|
|
261
|
+
* window's end keeps moving. */
|
|
262
|
+
cache_expires_at: Type.Optional(Timestamp),
|
|
263
|
+
/** The cache length the gateway asked for. */
|
|
264
|
+
cache_ttl_secs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
265
|
+
/** The gateway is holding off on keepalives for this series. Stated on
|
|
266
|
+
* every event of a series the strategy covers, so absent means the series is
|
|
267
|
+
* not one it keeps alive — never that keepalives are running. */
|
|
268
|
+
cache_paused: Type.Optional(Type.Boolean()),
|
|
269
|
+
/** The real request that began the current keepalive chain. The same value
|
|
270
|
+
* on the request and on every keepalive's return, which is what makes it the
|
|
271
|
+
* chain's fixed origin while the other instants walk forward. */
|
|
272
|
+
cache_since_at: Type.Optional(Timestamp),
|
|
273
|
+
/** Position in the chain: zero on the real request. */
|
|
274
|
+
cache_count: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
275
|
+
/** When the gateway plans to send the next keepalive. Absent when it plans
|
|
276
|
+
* none. */
|
|
277
|
+
next_keepalive_at: Type.Optional(Timestamp),
|
|
278
|
+
/** Where the chain is projected to end, and how many keepalives that takes.
|
|
279
|
+
* A projection recomputed per event, so the newest event's value replaces
|
|
280
|
+
* the previous one rather than being merged with it. */
|
|
281
|
+
cache_until_at: Type.Optional(Timestamp),
|
|
282
|
+
cache_until_count: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
283
|
+
/** Past this, keeping the cache warm costs more than rebuilding it. Omitted
|
|
284
|
+
* for models whose price the gateway does not know. */
|
|
285
|
+
cache_breakeven_until_at: Type.Optional(Timestamp),
|
|
286
|
+
cache_breakeven_count: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
287
|
+
/** Present only on a keepalive's return trip, carrying how the gateway
|
|
288
|
+
* judged it. Its absence is how an ordinary request is told apart. */
|
|
289
|
+
keepalive: Type.Optional(Type.String()),
|
|
290
|
+
ns: Type.Optional(Type.String()),
|
|
291
|
+
model: Type.Optional(Type.String()),
|
|
292
|
+
credential: Type.Optional(Type.String()),
|
|
293
|
+
status: Type.Optional(Type.Integer()),
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
$id: "LlmRequestInfo",
|
|
297
|
+
...upstream("llm-gateway", "one observed request, renamed and re-united from its event"),
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
export type LlmRequestInfo = Static<typeof LlmRequestInfo>;
|
|
301
|
+
|
|
302
|
+
/** When the cache window one request opened closes.
|
|
303
|
+
*
|
|
304
|
+
* The instance prunes by it and clients draw to it, so the arithmetic lives
|
|
305
|
+
* here once instead of once per side. A request that states where it came from
|
|
306
|
+
* but names no deadline is the gateway saying it cached nothing; only an event
|
|
307
|
+
* that states neither falls back to the assumed length. */
|
|
308
|
+
export function llmCacheWindowEndAt(info: {
|
|
309
|
+
received_at: number;
|
|
310
|
+
cache_expires_at?: number;
|
|
311
|
+
origin?: string;
|
|
312
|
+
}): number {
|
|
313
|
+
if (info.cache_expires_at !== undefined) return info.cache_expires_at;
|
|
314
|
+
if (info.origin !== undefined) return info.received_at;
|
|
315
|
+
return info.received_at + LLM_PROMPT_CACHE_TTL_MS;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The `llm_requests` topic: the newest request per conversation series, always
|
|
319
|
+
* the whole unexpired set rather than the one that just arrived. A client that
|
|
320
|
+
* starts listening mid-window still needs the countdown that began before it
|
|
321
|
+
* was there, and one shape serves both that and the live update. An empty set
|
|
322
|
+
* is a legitimate "no session has a warm cache". */
|
|
323
|
+
export const LlmRequestsFrame = topicFrame("llm_requests", Type.Array(LlmRequestInfo));
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// upstream health
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/** The gateway's display verdict, for one service and for the report as a
|
|
330
|
+
* whole. Nothing recomputes it here: the gateway knows which of the two signals
|
|
331
|
+
* below outweighs the other, and a second opinion would disagree with every
|
|
332
|
+
* other reader of the same report. A value outside this set becomes `unknown`
|
|
333
|
+
* rather than travelling, so a future vocabulary cannot arrive as a word
|
|
334
|
+
* nothing can render. */
|
|
335
|
+
export const LlmStatusSeverity = Type.Union(
|
|
336
|
+
[Type.Literal("ok"), Type.Literal("warning"), Type.Literal("critical"), Type.Literal("unknown")],
|
|
337
|
+
{ $id: "LlmStatusSeverity" },
|
|
338
|
+
);
|
|
339
|
+
export type LlmStatusSeverity = Static<typeof LlmStatusSeverity>;
|
|
340
|
+
|
|
341
|
+
/** What the provider's own status page says. A closed vocabulary; anything
|
|
342
|
+
* else, including a page that could not be read, is `unknown`. */
|
|
343
|
+
export const LlmStatusOfficialState = Type.Union(
|
|
344
|
+
[
|
|
345
|
+
Type.Literal("operational"),
|
|
346
|
+
Type.Literal("degraded"),
|
|
347
|
+
Type.Literal("partial_outage"),
|
|
348
|
+
Type.Literal("major_outage"),
|
|
349
|
+
Type.Literal("maintenance"),
|
|
350
|
+
Type.Literal("unknown"),
|
|
351
|
+
],
|
|
352
|
+
{ $id: "LlmStatusOfficialState" },
|
|
353
|
+
);
|
|
354
|
+
export type LlmStatusOfficialState = Static<typeof LlmStatusOfficialState>;
|
|
355
|
+
|
|
356
|
+
/** What the gateway itself saw. Deliberately worded apart from the official
|
|
357
|
+
* vocabulary, so the two signals can never be mistaken for each other. */
|
|
358
|
+
export const LlmStatusObservedState = Type.Union(
|
|
359
|
+
[Type.Literal("reachable"), Type.Literal("failing"), Type.Literal("unknown")],
|
|
360
|
+
{ $id: "LlmStatusObservedState" },
|
|
361
|
+
);
|
|
362
|
+
export type LlmStatusObservedState = Static<typeof LlmStatusObservedState>;
|
|
363
|
+
|
|
364
|
+
export const LlmStatusComponent = Type.Object(
|
|
365
|
+
{
|
|
366
|
+
id: Type.Optional(Type.String()),
|
|
367
|
+
name: Type.String(),
|
|
368
|
+
state: LlmStatusOfficialState,
|
|
369
|
+
},
|
|
370
|
+
{ $id: "LlmStatusComponent" },
|
|
371
|
+
);
|
|
372
|
+
export type LlmStatusComponent = Static<typeof LlmStatusComponent>;
|
|
373
|
+
|
|
374
|
+
/** One unresolved incident from the provider's status page. Everything but the
|
|
375
|
+
* title is optional — a line with a title alone is still worth showing. All of
|
|
376
|
+
* it is the provider's prose, to be shown as text and never as markup. */
|
|
377
|
+
export const LlmStatusIncident = Type.Object(
|
|
378
|
+
{
|
|
379
|
+
id: Type.Optional(Type.String()),
|
|
380
|
+
name: Type.String(),
|
|
381
|
+
/** The provider's own workflow word. An open set, shown as it stands. */
|
|
382
|
+
state: Type.Optional(Type.String()),
|
|
383
|
+
impact: Type.Optional(Type.String()),
|
|
384
|
+
created_at: Type.Optional(Timestamp),
|
|
385
|
+
updated_at: Type.Optional(Timestamp),
|
|
386
|
+
url: Type.Optional(Type.String()),
|
|
387
|
+
latest_update: Type.Optional(Type.String()),
|
|
388
|
+
/** Says the incident carries no component mapping, so it is shown for
|
|
389
|
+
* reference and did not raise this service's severity. */
|
|
390
|
+
scope: Type.Optional(Type.String()),
|
|
391
|
+
},
|
|
392
|
+
{ $id: "LlmStatusIncident" },
|
|
393
|
+
);
|
|
394
|
+
export type LlmStatusIncident = Static<typeof LlmStatusIncident>;
|
|
395
|
+
|
|
396
|
+
export const LlmStatusOfficial = Type.Object(
|
|
397
|
+
{
|
|
398
|
+
state: LlmStatusOfficialState,
|
|
399
|
+
/** How the gateway obtained it. */
|
|
400
|
+
source: Type.Optional(Type.String()),
|
|
401
|
+
/** The human-readable status page. */
|
|
402
|
+
source_url: Type.Optional(Type.String()),
|
|
403
|
+
observed_at: Type.Optional(Timestamp),
|
|
404
|
+
/** The reading is older than the gateway's freshness bound: what is shown
|
|
405
|
+
* is the last success, not a current reading. */
|
|
406
|
+
stale: Type.Optional(Type.Boolean()),
|
|
407
|
+
components: Type.Array(LlmStatusComponent),
|
|
408
|
+
incidents: Type.Array(LlmStatusIncident),
|
|
409
|
+
/** Why the last read failed. The previous good state is kept beside it
|
|
410
|
+
* rather than replaced by the failure. */
|
|
411
|
+
error: Type.Optional(Type.String()),
|
|
412
|
+
},
|
|
413
|
+
{ $id: "LlmStatusOfficial" },
|
|
414
|
+
);
|
|
415
|
+
export type LlmStatusOfficial = Static<typeof LlmStatusOfficial>;
|
|
416
|
+
|
|
417
|
+
export const LlmStatusObserved = Type.Object(
|
|
418
|
+
{
|
|
419
|
+
state: LlmStatusObservedState,
|
|
420
|
+
observed_at: Type.Optional(Timestamp),
|
|
421
|
+
/** When the observation stops counting and the state falls back to
|
|
422
|
+
* unknown. */
|
|
423
|
+
expires_at: Type.Optional(Timestamp),
|
|
424
|
+
last_success_at: Type.Optional(Timestamp),
|
|
425
|
+
last_failure: Type.Optional(
|
|
426
|
+
Type.Object({
|
|
427
|
+
at: Type.Optional(Timestamp),
|
|
428
|
+
/** What kind of failure it was. An open set. */
|
|
429
|
+
kind: Type.Optional(Type.String()),
|
|
430
|
+
/** The HTTP status, when the failure had one. */
|
|
431
|
+
status: Type.Optional(Type.Integer()),
|
|
432
|
+
}),
|
|
433
|
+
),
|
|
434
|
+
},
|
|
435
|
+
{ $id: "LlmStatusObserved" },
|
|
436
|
+
);
|
|
437
|
+
export type LlmStatusObserved = Static<typeof LlmStatusObserved>;
|
|
438
|
+
|
|
439
|
+
/** One upstream service, with the two signals kept apart. Both may be absent —
|
|
440
|
+
* a gateway can report a service it has neither read about nor exercised —
|
|
441
|
+
* while the severity is always there, since that is what a display is chosen
|
|
442
|
+
* from. */
|
|
443
|
+
export const LlmStatusService = Type.Object(
|
|
444
|
+
{
|
|
445
|
+
id: Type.String(),
|
|
446
|
+
name: Type.String(),
|
|
447
|
+
severity: LlmStatusSeverity,
|
|
448
|
+
/** Which configured routes draw on this service. */
|
|
449
|
+
routes: Type.Array(Type.String()),
|
|
450
|
+
official: Type.Optional(LlmStatusOfficial),
|
|
451
|
+
observed: Type.Optional(LlmStatusObserved),
|
|
452
|
+
},
|
|
453
|
+
{ $id: "LlmStatusService" },
|
|
454
|
+
);
|
|
455
|
+
export type LlmStatusService = Static<typeof LlmStatusService>;
|
|
456
|
+
|
|
457
|
+
/** The worst severity across services, with the breakdown that keeps "one
|
|
458
|
+
* critical among many healthy" from reading as "everything is down". */
|
|
459
|
+
export const LlmStatusOverall = Type.Object(
|
|
460
|
+
{
|
|
461
|
+
severity: LlmStatusSeverity,
|
|
462
|
+
/** Severity to how many services hold it. A map rather than named fields so
|
|
463
|
+
* a future severity needs no change here. */
|
|
464
|
+
service_counts: Type.Record(Type.String(), Type.Integer({ minimum: 0 })),
|
|
465
|
+
},
|
|
466
|
+
{ $id: "LlmStatusOverall" },
|
|
467
|
+
);
|
|
468
|
+
export type LlmStatusOverall = Static<typeof LlmStatusOverall>;
|
|
469
|
+
|
|
470
|
+
/** The gateway's report on the services behind it.
|
|
471
|
+
*
|
|
472
|
+
* It has no op of its own: a client subscribes and receives the current report
|
|
473
|
+
* as the snapshot, then a fresh one whenever the gateway reports trouble and
|
|
474
|
+
* the instance re-reads. That moment is exactly when a display has to change
|
|
475
|
+
* and the one moment a client cannot anticipate. */
|
|
476
|
+
export const LlmStatusReport = Type.Object(
|
|
477
|
+
{
|
|
478
|
+
/** The gateway's own version of this document's shape. Passed through
|
|
479
|
+
* rather than gated on: every field degrades on its own, so a newer document
|
|
480
|
+
* arrives as unknowns instead of as nothing. */
|
|
481
|
+
schema_version: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
482
|
+
generated_at: Type.Optional(Timestamp),
|
|
483
|
+
overall: LlmStatusOverall,
|
|
484
|
+
services: Type.Array(LlmStatusService),
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
$id: "LlmStatusReport",
|
|
488
|
+
...upstream("llm-gateway", "the gateway's upstream-service report, renamed only"),
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
export type LlmStatusReport = Static<typeof LlmStatusReport>;
|
|
492
|
+
|
|
493
|
+
/** The `llm_status` topic. Whole-value: each frame replaces the last. */
|
|
494
|
+
export const LlmStatusFrame = topicFrame("llm_status", LlmStatusReport);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import { topicFrame } from "../envelope.ts";
|
|
3
|
+
import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
+
|
|
5
|
+
/** A client of one session whose greeting was refused.
|
|
6
|
+
*
|
|
7
|
+
* Overwritten rather than accumulated: such a client reconnects every few
|
|
8
|
+
* seconds, so what is worth reading is that it is still happening, not how
|
|
9
|
+
* often it has tried. The session itself is fine — some other process of the
|
|
10
|
+
* same session is the stale one, and it is invisible otherwise, retrying
|
|
11
|
+
* forever with nothing in sight moving. */
|
|
12
|
+
export const StaleClientInfo = Type.Object(
|
|
13
|
+
{
|
|
14
|
+
last_seen_at: Timestamp,
|
|
15
|
+
/** The build it reported, when it got far enough to report one. */
|
|
16
|
+
version: Type.Optional(Type.String()),
|
|
17
|
+
/** The generation it announced. Absent means it was refused before it could
|
|
18
|
+
* be read as a greeting at all. */
|
|
19
|
+
protocol_version: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
20
|
+
},
|
|
21
|
+
{ $id: "StaleClientInfo" },
|
|
22
|
+
);
|
|
23
|
+
export type StaleClientInfo = Static<typeof StaleClientInfo>;
|
|
24
|
+
|
|
25
|
+
/** One connected session.
|
|
26
|
+
*
|
|
27
|
+
* The paths are the host's, so the entry names the instance holding them:
|
|
28
|
+
* `peers` carries every instance's sessions in one list, and two hosts' paths
|
|
29
|
+
* would otherwise be indistinguishable. */
|
|
30
|
+
export const PeerInfo = Type.Object(
|
|
31
|
+
{
|
|
32
|
+
sid: Sid,
|
|
33
|
+
instance: InstanceId,
|
|
34
|
+
repo: Type.String(),
|
|
35
|
+
ws: Type.String(),
|
|
36
|
+
cwd: Type.String(),
|
|
37
|
+
/** Present when the session announced a transcript the instance accepted,
|
|
38
|
+
* which is what decides whether its transcript can be read at all. */
|
|
39
|
+
transcript_path: Type.Optional(Type.String()),
|
|
40
|
+
/** Present when the session announced a repository container the instance
|
|
41
|
+
* accepted. File browsing is rooted here rather than at the working
|
|
42
|
+
* directory, so sibling workspaces are reachable. */
|
|
43
|
+
repo_root: Type.Optional(Type.String()),
|
|
44
|
+
branch: Type.Optional(Type.String()),
|
|
45
|
+
/** When this session first registered with the instance. Stable across its
|
|
46
|
+
* reconnections, and reset when the instance restarts. */
|
|
47
|
+
connected_at: Type.Optional(Timestamp),
|
|
48
|
+
/** The session's most recent request on any of its connections. */
|
|
49
|
+
last_activity_at: Type.Optional(Timestamp),
|
|
50
|
+
/** When a person last put something into this session: a prompt they typed,
|
|
51
|
+
* or a message they sent it. Distinct from the activity above, which every
|
|
52
|
+
* request the session makes on its own re-stamps — this one moves only when
|
|
53
|
+
* a person speaks, which is what an attention-ordered list wants. Absent
|
|
54
|
+
* while none has been found; a client orders such a session after every
|
|
55
|
+
* session that has one rather than treating it as long ago. */
|
|
56
|
+
last_user_input_at: Type.Optional(Timestamp),
|
|
57
|
+
/** Whether the asking session can reach this one with the harness's own
|
|
58
|
+
* cross-session messaging, which does not cross config homes. Computed
|
|
59
|
+
* against the asker, so it never appears on the asker's own entry nor for a
|
|
60
|
+
* person, who has no config home to compare. Absent means one side's config
|
|
61
|
+
* home is unknown: an unflagged peer merely costs a message sent the long
|
|
62
|
+
* way, a wrongly flagged one costs a message that never arrives. */
|
|
63
|
+
send_message: Type.Optional(Type.Boolean()),
|
|
64
|
+
/** The build of the client that last greeted for this session. */
|
|
65
|
+
client_version: Type.Optional(Type.String()),
|
|
66
|
+
/** The generation that client speaks. Always stated: a client that does not
|
|
67
|
+
* announce one is refused, so there is no connected session whose
|
|
68
|
+
* generation is a guess. */
|
|
69
|
+
protocol_version: Type.Integer({ minimum: 1 }),
|
|
70
|
+
/** Set while some client of this session is being refused. */
|
|
71
|
+
stale_client: Type.Optional(StaleClientInfo),
|
|
72
|
+
},
|
|
73
|
+
{ $id: "PeerInfo" },
|
|
74
|
+
);
|
|
75
|
+
export type PeerInfo = Static<typeof PeerInfo>;
|
|
76
|
+
|
|
77
|
+
/** One session that was connected when its instance last saw it, and has not
|
|
78
|
+
* come back.
|
|
79
|
+
*
|
|
80
|
+
* The connection fields are a frozen copy of what that session looked like at
|
|
81
|
+
* the last snapshot, not a live reading — by definition it is not connected
|
|
82
|
+
* while it appears here. The model and effort are the exception: they are read
|
|
83
|
+
* back from the transcript's last turn, because what the session must resume as
|
|
84
|
+
* is a property of where it actually stopped, not of when the snapshot was
|
|
85
|
+
* written. An entry leaves this list the moment its session registers again, so
|
|
86
|
+
* a fully recovered host shows none. */
|
|
87
|
+
export const LastLiveSession = Type.Object(
|
|
88
|
+
{
|
|
89
|
+
sid: Sid,
|
|
90
|
+
instance: InstanceId,
|
|
91
|
+
repo: Type.String(),
|
|
92
|
+
ws: Type.String(),
|
|
93
|
+
cwd: Type.String(),
|
|
94
|
+
/** Also where the model and effort below were read from. */
|
|
95
|
+
transcript_path: Type.Optional(Type.String()),
|
|
96
|
+
repo_root: Type.Optional(Type.String()),
|
|
97
|
+
branch: Type.Optional(Type.String()),
|
|
98
|
+
/** The session's own title as of the snapshot, when one was known. Absent
|
|
99
|
+
* means not known, never untitled. */
|
|
100
|
+
title: Type.Optional(Type.String()),
|
|
101
|
+
connected_at: Type.Optional(Timestamp),
|
|
102
|
+
/** The newest instant this session is known to have been alive. */
|
|
103
|
+
last_seen_at: Timestamp,
|
|
104
|
+
/** What its last turn ran as, in the transcript's own spelling. A resume
|
|
105
|
+
* must not quietly switch the session to something else. */
|
|
106
|
+
model: Type.Optional(Type.String()),
|
|
107
|
+
effort: Type.Optional(Type.String()),
|
|
108
|
+
},
|
|
109
|
+
{ $id: "LastLiveSession" },
|
|
110
|
+
);
|
|
111
|
+
export type LastLiveSession = Static<typeof LastLiveSession>;
|
|
112
|
+
|
|
113
|
+
/** The `peers` topic.
|
|
114
|
+
*
|
|
115
|
+
* Whole-value per instance: a frame replaces everything previously known from
|
|
116
|
+
* the instance that sent it and leaves other instances' entries alone, which is
|
|
117
|
+
* what lets several instances each state their whole list without colliding.
|
|
118
|
+
*
|
|
119
|
+
* Both lists travel together because a session registering is exactly what
|
|
120
|
+
* moves it from one to the other. */
|
|
121
|
+
export const PeersFrame = topicFrame(
|
|
122
|
+
"peers",
|
|
123
|
+
Type.Object({
|
|
124
|
+
peers: Type.Array(PeerInfo),
|
|
125
|
+
last_live: Type.Array(LastLiveSession),
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import { request, response } from "../envelope.ts";
|
|
3
|
+
import { Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
+
import { FileKind } from "./files.ts";
|
|
5
|
+
|
|
6
|
+
/** Mints a URL on the sandbox origin for one file.
|
|
7
|
+
*
|
|
8
|
+
* A file served from the instance's own origin cannot be handed its real
|
|
9
|
+
* content type without letting it act as part of the application, so a client
|
|
10
|
+
* that wants to open a file as a page, or download it as itself, asks for a URL
|
|
11
|
+
* on a separate origin instead.
|
|
12
|
+
*
|
|
13
|
+
* The grant widens nothing: the same check the matching read performs runs both
|
|
14
|
+
* when the URL is minted and on every request it serves, so this can fail only
|
|
15
|
+
* the way that read would and can never reach a file the caller could not
|
|
16
|
+
* already read. */
|
|
17
|
+
export const SandboxGrantArgs = Type.Object({
|
|
18
|
+
sid: Sid,
|
|
19
|
+
kind: FileKind,
|
|
20
|
+
path: Type.String({ minLength: 1 }),
|
|
21
|
+
});
|
|
22
|
+
export type SandboxGrantArgs = Static<typeof SandboxGrantArgs>;
|
|
23
|
+
|
|
24
|
+
export const SandboxGrantResult = Type.Object({
|
|
25
|
+
/** Names the grant, and separates its origin from every other grant's. It
|
|
26
|
+
* travels inside a hostname and is therefore seen by every resolver on the
|
|
27
|
+
* way, so it is not a secret and authorizes nothing by itself. */
|
|
28
|
+
gid: Type.String({ minLength: 1 }),
|
|
29
|
+
/** The secret that does authorize a request. Returned so a client can build
|
|
30
|
+
* a sibling URL — a download beside a preview — without minting again. */
|
|
31
|
+
token: Type.String({ minLength: 1 }),
|
|
32
|
+
/** Ready to open as it stands. */
|
|
33
|
+
url: Type.String({ minLength: 1 }),
|
|
34
|
+
/** When the grant stops working. Minting the same scope again returns this
|
|
35
|
+
* same grant with a later expiry, so an open preview keeps working. */
|
|
36
|
+
expires_at: Timestamp,
|
|
37
|
+
});
|
|
38
|
+
export type SandboxGrantResult = Static<typeof SandboxGrantResult>;
|
|
39
|
+
|
|
40
|
+
export const SandboxGrantRequest = request("sandbox_grant", SandboxGrantArgs);
|
|
41
|
+
export const SandboxGrantResponse = response("sandbox_grant", SandboxGrantResult);
|
|
42
|
+
|
|
43
|
+
/** Ends a grant early, as when a preview is closed.
|
|
44
|
+
*
|
|
45
|
+
* Best effort by design. The expiry is what actually bounds a grant's life, so
|
|
46
|
+
* an unknown or already-expired grant still succeeds: there is nothing a caller
|
|
47
|
+
* would do differently, and reporting "no such grant" would make this an
|
|
48
|
+
* existence oracle. */
|
|
49
|
+
export const SandboxRevokeArgs = Type.Object({
|
|
50
|
+
gid: Type.String({ minLength: 1 }),
|
|
51
|
+
});
|
|
52
|
+
export type SandboxRevokeArgs = Static<typeof SandboxRevokeArgs>;
|
|
53
|
+
|
|
54
|
+
export const SandboxRevokeResult = Type.Object({});
|
|
55
|
+
export type SandboxRevokeResult = Static<typeof SandboxRevokeResult>;
|
|
56
|
+
|
|
57
|
+
export const SandboxRevokeRequest = request("sandbox_revoke", SandboxRevokeArgs);
|
|
58
|
+
export const SandboxRevokeResponse = response("sandbox_revoke", SandboxRevokeResult);
|