@intx/hub-sessions 0.1.2 → 0.2.2
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 +176 -0
- package/README.md +84 -1
- package/dist/agent-repo.d.ts +89 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +185 -0
- package/dist/asset-service.d.ts +123 -0
- package/dist/asset-service.js +349 -0
- package/dist/available-skills-stanza.d.ts +21 -0
- package/dist/available-skills-stanza.js +32 -0
- package/dist/credential-push.d.ts +32 -0
- package/dist/credential-push.js +85 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +357 -0
- package/dist/hub-session-lookups.d.ts +17 -0
- package/dist/hub-session-lookups.js +204 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +122 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1692 -0
- package/dist/repo-store/subscribe-kind.d.ts +53 -0
- package/dist/repo-store/subscribe-kind.js +179 -0
- package/dist/repo-store/types.d.ts +483 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +235 -0
- package/dist/session-service.js +997 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +288 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-kind.d.ts +21 -0
- package/dist/workflow-kind.js +263 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +326 -0
- package/dist/workflow-run-kind.js +2646 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +157 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +134 -0
- package/dist/ws/sidecar-events.js +70 -0
- package/dist/ws/sidecar-handler.d.ts +184 -0
- package/dist/ws/sidecar-handler.js +1603 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
- package/dist/ws/sidecar-token-authenticator.js +24 -0
- package/package.json +34 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/src/event-collector.ts
DELETED
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
// Persists assistant inference turns from the InferenceEvent stream.
|
|
2
|
-
//
|
|
3
|
-
// One collector per active session. Events are written eagerly to the DB
|
|
4
|
-
// so data survives crashes. The collector does not block the websocket
|
|
5
|
-
// message loop — callers should fire-and-forget and log errors.
|
|
6
|
-
|
|
7
|
-
import { eq } from "drizzle-orm";
|
|
8
|
-
|
|
9
|
-
import { inferenceTurn, turnPart } from "@intx/db/schema";
|
|
10
|
-
import { getLogger } from "@intx/log";
|
|
11
|
-
import type { InferenceEvent, ContentBlock } from "@intx/types/runtime";
|
|
12
|
-
import { type DB, parseTurnPartType } from "@intx/db";
|
|
13
|
-
|
|
14
|
-
import { generateId } from "@intx/hub-common";
|
|
15
|
-
|
|
16
|
-
const log = getLogger(["hub", "event-collector"]);
|
|
17
|
-
|
|
18
|
-
export type TurnToolCall = {
|
|
19
|
-
name: string;
|
|
20
|
-
arguments: Record<string, unknown>;
|
|
21
|
-
result: string;
|
|
22
|
-
isError: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type TurnFinalized = {
|
|
26
|
-
turnId: string;
|
|
27
|
-
status: "completed" | "failed";
|
|
28
|
-
text: string;
|
|
29
|
-
hadReply: boolean;
|
|
30
|
-
hadError: boolean;
|
|
31
|
-
errors: { category: string; message: string }[];
|
|
32
|
-
toolCalls: TurnToolCall[];
|
|
33
|
-
toolErrors: { name: string; content: string }[];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type EventCollector = {
|
|
37
|
-
onEvent(event: InferenceEvent): Promise<void>;
|
|
38
|
-
abandon(): Promise<void>;
|
|
39
|
-
getAccumulatedText(): string;
|
|
40
|
-
getCurrentTurnId(): string | null;
|
|
41
|
-
getLastTurnId(): string | null;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type EventCollectorConfig = {
|
|
45
|
-
db: DB["db"];
|
|
46
|
-
sessionId: string;
|
|
47
|
-
instanceId: string;
|
|
48
|
-
tenantId: string;
|
|
49
|
-
onTurnFinalized?: (turn: TurnFinalized) => void;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function createEventCollector(
|
|
53
|
-
config: EventCollectorConfig,
|
|
54
|
-
): EventCollector {
|
|
55
|
-
const { db, sessionId, instanceId, tenantId, onTurnFinalized } = config;
|
|
56
|
-
|
|
57
|
-
// Current inference turn being accumulated. A new turn is created on each
|
|
58
|
-
// inference.start. Finalized on connector.reply, reactor.done,
|
|
59
|
-
// reactor.error (fatal), or abandon. Null when no turn is active.
|
|
60
|
-
let currentTurnId: string | null = null;
|
|
61
|
-
// Most recent turn ID, set in beginTurn. Unlike currentTurnId this is NOT
|
|
62
|
-
// cleared on finalization — the SSE replay endpoint needs the turn ID
|
|
63
|
-
// after the turn commits but before the collector is removed.
|
|
64
|
-
let lastTurnId: string | null = null;
|
|
65
|
-
let ordinal = 0;
|
|
66
|
-
// Prevents double-finalization when reactor.done and abandon() race.
|
|
67
|
-
let finalized = false;
|
|
68
|
-
// Set when inference.error fires so connector.reply knows to persist its
|
|
69
|
-
// content (on normal turns, inference.done already persisted the text).
|
|
70
|
-
let pendingError = false;
|
|
71
|
-
// Accumulated visible text content for the current turn. Only text blocks
|
|
72
|
-
// from inference.done (not thinking/reasoning) are included. Reset on each
|
|
73
|
-
// new turn.
|
|
74
|
-
let accumulatedText = "";
|
|
75
|
-
// In-progress text from inference.text.delta events during the current
|
|
76
|
-
// inference step. Reset on inference.done (when accumulatedText takes over).
|
|
77
|
-
let streamingText = "";
|
|
78
|
-
// Set when inference.error fires. Unlike pendingError (which resets on
|
|
79
|
-
// connector.reply), this persists until finalization so the callback can
|
|
80
|
-
// report whether an inference error occurred during the turn.
|
|
81
|
-
let turnHadError = false;
|
|
82
|
-
// Structured error details accumulated during the turn for inclusion in
|
|
83
|
-
// TurnFinalized. Reset on each new turn.
|
|
84
|
-
let accumulatedErrors: { category: string; message: string }[] = [];
|
|
85
|
-
// Maps tool call IDs to tool names for correlating tool results with their
|
|
86
|
-
// originating calls. Populated from inference.done tool_call blocks.
|
|
87
|
-
const callNames = new Map<string, string>();
|
|
88
|
-
const callArgs = new Map<string, Record<string, unknown>>();
|
|
89
|
-
// Tool calls accumulated for TurnFinalized.
|
|
90
|
-
let accumulatedToolCalls: TurnToolCall[] = [];
|
|
91
|
-
// Tool results that reported isError, accumulated for TurnFinalized.
|
|
92
|
-
let accumulatedToolErrors: { name: string; content: string }[] = [];
|
|
93
|
-
|
|
94
|
-
async function onEvent(event: InferenceEvent): Promise<void> {
|
|
95
|
-
switch (event.type) {
|
|
96
|
-
case "inference.start":
|
|
97
|
-
await beginTurn(event.data.model);
|
|
98
|
-
await insertPart("step-start", null, { model: event.data.model });
|
|
99
|
-
break;
|
|
100
|
-
case "inference.text.delta":
|
|
101
|
-
streamingText += event.data.token;
|
|
102
|
-
break;
|
|
103
|
-
case "inference.done":
|
|
104
|
-
await handleInferenceDone(event.data.turn.content);
|
|
105
|
-
streamingText = "";
|
|
106
|
-
break;
|
|
107
|
-
case "tool.done": {
|
|
108
|
-
const callId = event.data.result.callId;
|
|
109
|
-
const isError = event.data.result.isError ?? false;
|
|
110
|
-
await insertPart("tool", null, {
|
|
111
|
-
kind: "result",
|
|
112
|
-
callId,
|
|
113
|
-
content: event.data.result.content,
|
|
114
|
-
isError,
|
|
115
|
-
});
|
|
116
|
-
const name = callNames.get(callId) ?? callId;
|
|
117
|
-
const raw = event.data.result.content;
|
|
118
|
-
const content = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
119
|
-
accumulatedToolCalls.push({
|
|
120
|
-
name,
|
|
121
|
-
arguments: callArgs.get(callId) ?? {},
|
|
122
|
-
result: content,
|
|
123
|
-
isError,
|
|
124
|
-
});
|
|
125
|
-
if (isError) {
|
|
126
|
-
accumulatedToolErrors.push({ name, content });
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
case "inference.error":
|
|
131
|
-
pendingError = true;
|
|
132
|
-
turnHadError = true;
|
|
133
|
-
await insertPart("error", event.data.error.message, {
|
|
134
|
-
category: event.data.error.category,
|
|
135
|
-
...(event.data.error.statusCode !== undefined
|
|
136
|
-
? { statusCode: event.data.error.statusCode }
|
|
137
|
-
: {}),
|
|
138
|
-
});
|
|
139
|
-
accumulatedErrors.push({
|
|
140
|
-
category: event.data.error.category,
|
|
141
|
-
message: event.data.error.message,
|
|
142
|
-
});
|
|
143
|
-
break;
|
|
144
|
-
case "connector.reply":
|
|
145
|
-
if (finalized) break;
|
|
146
|
-
// Only persist reply content when it originated from an error path.
|
|
147
|
-
// On normal turns inference.done already persisted the text parts.
|
|
148
|
-
if (pendingError) {
|
|
149
|
-
accumulatedText += event.data.content;
|
|
150
|
-
await insertPart("text", event.data.content, null);
|
|
151
|
-
pendingError = false;
|
|
152
|
-
}
|
|
153
|
-
await finalizeTurn(turnHadError ? "failed" : "completed", true, true);
|
|
154
|
-
break;
|
|
155
|
-
case "reactor.done":
|
|
156
|
-
await finalizeTurn("completed", true, false);
|
|
157
|
-
break;
|
|
158
|
-
case "reactor.error":
|
|
159
|
-
if (event.data.fatal && !finalized) {
|
|
160
|
-
// The reactor failed before any inference started (e.g., context
|
|
161
|
-
// store load failure), but the user needs to see why their agent
|
|
162
|
-
// failed, and without a turn there is no container for the error.
|
|
163
|
-
if (currentTurnId === null) {
|
|
164
|
-
await beginTurn("unknown");
|
|
165
|
-
}
|
|
166
|
-
turnHadError = true;
|
|
167
|
-
// Push after beginTurn so the error survives the array reset.
|
|
168
|
-
accumulatedErrors.push({
|
|
169
|
-
category: "reactor_error",
|
|
170
|
-
message: event.data.error,
|
|
171
|
-
});
|
|
172
|
-
await insertPart("error", event.data.error, {
|
|
173
|
-
category: "reactor_error",
|
|
174
|
-
});
|
|
175
|
-
await finalizeTurn("failed", true, false);
|
|
176
|
-
} else if (!event.data.fatal && !finalized) {
|
|
177
|
-
if (currentTurnId === null) {
|
|
178
|
-
await beginTurn("unknown");
|
|
179
|
-
}
|
|
180
|
-
turnHadError = true;
|
|
181
|
-
accumulatedErrors.push({
|
|
182
|
-
category: "reactor_error",
|
|
183
|
-
message: event.data.error,
|
|
184
|
-
});
|
|
185
|
-
await insertPart("error", event.data.error, {
|
|
186
|
-
category: "reactor_error",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
break;
|
|
190
|
-
default:
|
|
191
|
-
// reactor.start, streaming deltas, usage, and other events are
|
|
192
|
-
// not persisted.
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function beginTurn(model: string): Promise<void> {
|
|
198
|
-
// A previous turn is still open — this is normal in multi-step tool-use
|
|
199
|
-
// loops where inference.start fires again after tools return.
|
|
200
|
-
if (currentTurnId !== null && !finalized) {
|
|
201
|
-
await finalizeTurn("completed", true, false);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
currentTurnId = generateId("inferenceTurn");
|
|
205
|
-
lastTurnId = currentTurnId;
|
|
206
|
-
ordinal = 0;
|
|
207
|
-
finalized = false;
|
|
208
|
-
pendingError = false;
|
|
209
|
-
accumulatedText = "";
|
|
210
|
-
streamingText = "";
|
|
211
|
-
turnHadError = false;
|
|
212
|
-
accumulatedErrors = [];
|
|
213
|
-
callNames.clear();
|
|
214
|
-
callArgs.clear();
|
|
215
|
-
accumulatedToolCalls = [];
|
|
216
|
-
accumulatedToolErrors = [];
|
|
217
|
-
|
|
218
|
-
await db.insert(inferenceTurn).values({
|
|
219
|
-
id: currentTurnId,
|
|
220
|
-
sessionId,
|
|
221
|
-
instanceId,
|
|
222
|
-
tenantId,
|
|
223
|
-
model,
|
|
224
|
-
status: "running",
|
|
225
|
-
startedAt: new Date(),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function handleInferenceDone(content: ContentBlock[]): Promise<void> {
|
|
230
|
-
for (const block of content) {
|
|
231
|
-
switch (block.type) {
|
|
232
|
-
case "text":
|
|
233
|
-
accumulatedText += block.text;
|
|
234
|
-
await insertPart("text", block.text, null);
|
|
235
|
-
break;
|
|
236
|
-
case "thinking":
|
|
237
|
-
await insertPart("reasoning", block.thinking, null);
|
|
238
|
-
break;
|
|
239
|
-
case "redacted_thinking":
|
|
240
|
-
// The opaque `data` blob is meaningless to humans and
|
|
241
|
-
// must be preserved verbatim for echo-back on follow-up
|
|
242
|
-
// turns; persisting it as a reasoning row would invite
|
|
243
|
-
// truncation or display. Skip and let the adapter layer
|
|
244
|
-
// own the round-trip.
|
|
245
|
-
break;
|
|
246
|
-
case "refusal":
|
|
247
|
-
// Refusal blocks carry human-readable text the model
|
|
248
|
-
// emitted when it declined a structured-output request.
|
|
249
|
-
// A dedicated `refusal` part kind keeps the signal
|
|
250
|
-
// distinct from ordinary `text` (the model produced
|
|
251
|
-
// schema-conformant content) and from `error` (the HTTP
|
|
252
|
-
// call failed or the protocol mismatched). Session
|
|
253
|
-
// readers can branch on the part type to render policy
|
|
254
|
-
// declines differently from regular assistant output.
|
|
255
|
-
await insertPart("refusal", block.reason, null);
|
|
256
|
-
break;
|
|
257
|
-
case "tool_call":
|
|
258
|
-
callNames.set(block.id, block.name);
|
|
259
|
-
callArgs.set(block.id, block.arguments);
|
|
260
|
-
await insertPart("tool", null, {
|
|
261
|
-
kind: "call",
|
|
262
|
-
callId: block.id,
|
|
263
|
-
name: block.name,
|
|
264
|
-
arguments: block.arguments,
|
|
265
|
-
});
|
|
266
|
-
break;
|
|
267
|
-
case "tool_result":
|
|
268
|
-
// Tool results in the content block are echoes of earlier
|
|
269
|
-
// tool.done events. Skip to avoid duplication.
|
|
270
|
-
break;
|
|
271
|
-
case "citation":
|
|
272
|
-
// Citations annotate model output by reference (URI or
|
|
273
|
-
// document index). Persistence semantics — whether the
|
|
274
|
-
// cited source warrants an audit row of its own — are not
|
|
275
|
-
// yet settled; skip until they are.
|
|
276
|
-
break;
|
|
277
|
-
case "code_execution_request":
|
|
278
|
-
case "code_execution_result":
|
|
279
|
-
// Server-side code execution requests and results.
|
|
280
|
-
// Persistence semantics — whether the code and its output
|
|
281
|
-
// warrant audit rows of their own, and how to relate the
|
|
282
|
-
// pair through the requestId back-pointer — are not yet
|
|
283
|
-
// settled; skip until they are.
|
|
284
|
-
break;
|
|
285
|
-
case "image":
|
|
286
|
-
case "audio":
|
|
287
|
-
case "video":
|
|
288
|
-
case "document": {
|
|
289
|
-
// All media block variants persist into the generic "file"
|
|
290
|
-
// part bucket. The block's own `type` distinguishes the
|
|
291
|
-
// semantic role; `mimeType` distinguishes the encoding; the
|
|
292
|
-
// MediaSource discriminant distinguishes inline vs reference.
|
|
293
|
-
const source = block.source;
|
|
294
|
-
if (source.kind === "base64") {
|
|
295
|
-
await insertPart("file", null, {
|
|
296
|
-
kind: "base64",
|
|
297
|
-
mimeType: source.mimeType,
|
|
298
|
-
dataLength: source.data.length,
|
|
299
|
-
});
|
|
300
|
-
} else if (source.kind === "file-reference") {
|
|
301
|
-
await insertPart("file", null, {
|
|
302
|
-
kind: "file-reference",
|
|
303
|
-
mimeType: source.mimeType,
|
|
304
|
-
reference: source.reference,
|
|
305
|
-
});
|
|
306
|
-
} else if (source.kind === "url") {
|
|
307
|
-
// The `url` MediaSource variant carries a self-contained
|
|
308
|
-
// dereferenceable HTTP(S) URL (Gemini accepts these in
|
|
309
|
-
// `fileData/fileUri`; other adapters route similarly).
|
|
310
|
-
// The session record keeps the URL on a distinct `url`
|
|
311
|
-
// field rather than reusing the `reference` slot that
|
|
312
|
-
// `file-reference` uses: a session reader that filters
|
|
313
|
-
// or joins on `metadata->>'reference'` should see
|
|
314
|
-
// provider-opaque file ids only, with public URLs
|
|
315
|
-
// surfaced under their own field. The discriminant
|
|
316
|
-
// `kind` is the structural source of truth, and the
|
|
317
|
-
// field-per-variant shape keeps naive substring queries
|
|
318
|
-
// honest.
|
|
319
|
-
await insertPart("file", null, {
|
|
320
|
-
kind: "url",
|
|
321
|
-
mimeType: source.mimeType,
|
|
322
|
-
url: source.url,
|
|
323
|
-
});
|
|
324
|
-
} else {
|
|
325
|
-
source satisfies never;
|
|
326
|
-
throw new Error(`unreachable: unknown MediaSource kind`);
|
|
327
|
-
}
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Mark the end of this inference step.
|
|
334
|
-
await insertPart("step-finish", null, null);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function finalizeTurn(
|
|
338
|
-
status: "completed" | "failed",
|
|
339
|
-
notify: boolean,
|
|
340
|
-
hadReply: boolean,
|
|
341
|
-
): Promise<void> {
|
|
342
|
-
if (currentTurnId === null || finalized) return;
|
|
343
|
-
finalized = true;
|
|
344
|
-
|
|
345
|
-
const turnId = currentTurnId;
|
|
346
|
-
|
|
347
|
-
await db
|
|
348
|
-
.update(inferenceTurn)
|
|
349
|
-
.set({ status, endedAt: new Date() })
|
|
350
|
-
.where(eq(inferenceTurn.id, turnId));
|
|
351
|
-
|
|
352
|
-
if (notify && onTurnFinalized) {
|
|
353
|
-
onTurnFinalized({
|
|
354
|
-
turnId,
|
|
355
|
-
status,
|
|
356
|
-
text: accumulatedText,
|
|
357
|
-
hadReply,
|
|
358
|
-
hadError: turnHadError,
|
|
359
|
-
errors: [...accumulatedErrors],
|
|
360
|
-
toolCalls: [...accumulatedToolCalls],
|
|
361
|
-
toolErrors: [...accumulatedToolErrors],
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
currentTurnId = null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async function abandon(): Promise<void> {
|
|
369
|
-
if (currentTurnId === null || finalized) return;
|
|
370
|
-
|
|
371
|
-
log.warn`Abandoning running turn ${currentTurnId} for session ${sessionId}`;
|
|
372
|
-
|
|
373
|
-
await finalizeTurn("failed", false, false);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function insertPart(
|
|
377
|
-
partType: string,
|
|
378
|
-
content: string | null,
|
|
379
|
-
metadata: Record<string, unknown> | null,
|
|
380
|
-
): Promise<void> {
|
|
381
|
-
if (currentTurnId === null) {
|
|
382
|
-
log.warn`Dropping ${partType} part: no active turn for session ${sessionId}`;
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const values: typeof turnPart.$inferInsert = {
|
|
387
|
-
id: generateId("turnPart"),
|
|
388
|
-
turnId: currentTurnId,
|
|
389
|
-
sessionId,
|
|
390
|
-
type: parseTurnPartType(partType),
|
|
391
|
-
ordinal: ordinal++,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
if (content !== null) {
|
|
395
|
-
values.content = content;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (metadata !== null) {
|
|
399
|
-
values.metadata = metadata;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await db.insert(turnPart).values(values);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function getAccumulatedText(): string {
|
|
406
|
-
return accumulatedText + streamingText;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function getCurrentTurnId(): string | null {
|
|
410
|
-
return currentTurnId;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function getLastTurnId(): string | null {
|
|
414
|
-
return lastTurnId;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
onEvent,
|
|
419
|
-
abandon,
|
|
420
|
-
getAccumulatedText,
|
|
421
|
-
getCurrentTurnId,
|
|
422
|
-
getLastTurnId,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// Lookups that the sidecar wire layer issues against host state. These
|
|
2
|
-
// are queries (one answer per question) rather than events (broadcast
|
|
3
|
-
// notifications), so they live separately from the event emitter.
|
|
4
|
-
//
|
|
5
|
-
// Each lookup is a stateless DB or repo call. They are gathered into a
|
|
6
|
-
// single struct that the hub app passes to `createSidecarRouter` as
|
|
7
|
-
// `lookups`.
|
|
8
|
-
|
|
9
|
-
import { eq, and, isNull } from "drizzle-orm";
|
|
10
|
-
import type { DB } from "@intx/db";
|
|
11
|
-
import { agentInstance, sessionMail } from "@intx/db/schema";
|
|
12
|
-
import { getLogger } from "@intx/log";
|
|
13
|
-
|
|
14
|
-
import type { AgentRepoStore } from "./agent-repo";
|
|
15
|
-
import { generateId } from "@intx/hub-common";
|
|
16
|
-
import type { SidecarLookups } from "./ws/sidecar-events";
|
|
17
|
-
|
|
18
|
-
const logger = getLogger(["hub", "lookups"]);
|
|
19
|
-
|
|
20
|
-
export type HubSessionLookupsDeps = {
|
|
21
|
-
db: DB["db"];
|
|
22
|
-
agentRepoStore: AgentRepoStore;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function createHubSessionLookups(
|
|
26
|
-
deps: HubSessionLookupsDeps,
|
|
27
|
-
): Required<SidecarLookups> {
|
|
28
|
-
const { db, agentRepoStore } = deps;
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
async lookupPublicKey(agentAddress) {
|
|
32
|
-
const row = await db
|
|
33
|
-
.select({ publicKey: agentInstance.publicKey })
|
|
34
|
-
.from(agentInstance)
|
|
35
|
-
.where(
|
|
36
|
-
and(
|
|
37
|
-
eq(agentInstance.address, agentAddress),
|
|
38
|
-
isNull(agentInstance.endedAt),
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
.limit(1)
|
|
42
|
-
.then((rows) => rows[0]);
|
|
43
|
-
if (!row) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return row.publicKey;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async lookupDeployRef(agentAddress) {
|
|
50
|
-
const agentId = parseAgentId(agentAddress);
|
|
51
|
-
return agentRepoStore.getDeployRef(agentId);
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
async persistMail({ senderAddress, recipients, raw }) {
|
|
55
|
-
const senderInstance = await requireInstance(db, senderAddress);
|
|
56
|
-
if (!senderInstance.sessionId) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Instance ${senderInstance.id} has no session for address "${senderAddress}"`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
const createdAt = new Date();
|
|
62
|
-
|
|
63
|
-
// Outbound record on the sender's session.
|
|
64
|
-
const outboundId = generateId("sessionMail");
|
|
65
|
-
const outboundRecord = {
|
|
66
|
-
id: outboundId,
|
|
67
|
-
sessionId: senderInstance.sessionId,
|
|
68
|
-
instanceId: senderInstance.id,
|
|
69
|
-
tenantId: senderInstance.tenantId,
|
|
70
|
-
direction: "outbound" as const,
|
|
71
|
-
status: "delivered" as const,
|
|
72
|
-
raw,
|
|
73
|
-
createdAt,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Inbound records for each recipient that has an active agent
|
|
77
|
-
// instance. Recipients that are not agent instances (e.g. human
|
|
78
|
-
// user addresses) are skipped.
|
|
79
|
-
const recipientResults = await Promise.all(
|
|
80
|
-
recipients.map(async (addr) => {
|
|
81
|
-
const row = await db.query.agentInstance.findFirst({
|
|
82
|
-
where: and(
|
|
83
|
-
eq(agentInstance.address, addr),
|
|
84
|
-
isNull(agentInstance.endedAt),
|
|
85
|
-
),
|
|
86
|
-
});
|
|
87
|
-
if (row === undefined) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
if (row.sessionId === null) {
|
|
91
|
-
logger.warn`Active instance ${row.id} for "${addr}" has no session; skipping inbound record`;
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
return { addr, instance: row, sessionId: row.sessionId };
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
const recipientInstances = recipientResults.filter(
|
|
98
|
-
(r): r is NonNullable<typeof r> => r !== null,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const inboundEntries = recipientInstances.map(
|
|
102
|
-
({ addr, instance, sessionId }) => {
|
|
103
|
-
const id = generateId("sessionMail");
|
|
104
|
-
return {
|
|
105
|
-
record: {
|
|
106
|
-
id,
|
|
107
|
-
sessionId,
|
|
108
|
-
instanceId: instance.id,
|
|
109
|
-
tenantId: instance.tenantId,
|
|
110
|
-
direction: "inbound" as const,
|
|
111
|
-
status: "delivered" as const,
|
|
112
|
-
raw,
|
|
113
|
-
createdAt,
|
|
114
|
-
},
|
|
115
|
-
result: {
|
|
116
|
-
id,
|
|
117
|
-
direction: "inbound" as const,
|
|
118
|
-
instanceId: instance.id,
|
|
119
|
-
address: addr,
|
|
120
|
-
createdAt,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
await db
|
|
127
|
-
.insert(sessionMail)
|
|
128
|
-
.values([outboundRecord, ...inboundEntries.map((e) => e.record)]);
|
|
129
|
-
|
|
130
|
-
return [
|
|
131
|
-
{
|
|
132
|
-
id: outboundId,
|
|
133
|
-
direction: "outbound" as const,
|
|
134
|
-
instanceId: senderInstance.id,
|
|
135
|
-
address: senderInstance.address,
|
|
136
|
-
createdAt,
|
|
137
|
-
},
|
|
138
|
-
...inboundEntries.map((e) => e.result),
|
|
139
|
-
];
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
async receiveStatePack(repoId, pack, ref, commitSha) {
|
|
143
|
-
if (repoId.kind !== "agent-state") {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`hub-session lookups received unsupported repo kind ${JSON.stringify(repoId.kind)}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
const agentAddress = repoId.id;
|
|
149
|
-
const agentId = parseAgentId(agentAddress);
|
|
150
|
-
try {
|
|
151
|
-
await agentRepoStore.receiveStatePack(
|
|
152
|
-
{ kind: "agent-state", id: agentId },
|
|
153
|
-
pack,
|
|
154
|
-
ref,
|
|
155
|
-
commitSha,
|
|
156
|
-
);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
-
if (msg.startsWith("path_violation")) {
|
|
160
|
-
logger.warn`State pack rejected for ${agentAddress}: ${msg}`;
|
|
161
|
-
return { accepted: false, reason: "path_violation" as const };
|
|
162
|
-
}
|
|
163
|
-
// Any other failure from the repo subsystem reaches the
|
|
164
|
-
// WebSocket handler as an unhandled rejection unless we catch
|
|
165
|
-
// it here. Transient failures during receivePack (the agent
|
|
166
|
-
// directory being torn down concurrently with an in-flight
|
|
167
|
-
// pack write, filesystem errors mid-rename, etc.) are
|
|
168
|
-
// recoverable from the sender's perspective — the sender can
|
|
169
|
-
// re-push. Surface every such failure as a structured pack
|
|
170
|
-
// rejection (`corrupt` is the closest existing reason — from
|
|
171
|
-
// the sender's perspective the pack failed to index) and log
|
|
172
|
-
// the underlying error so the cause stays traceable on the
|
|
173
|
-
// hub side.
|
|
174
|
-
logger.error`State pack receive failed for ${agentAddress}: ${msg}`;
|
|
175
|
-
return { accepted: false, reason: "corrupt" as const };
|
|
176
|
-
}
|
|
177
|
-
return { accepted: true };
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function parseAgentId(agentAddress: string): string {
|
|
183
|
-
const atIdx = agentAddress.indexOf("@");
|
|
184
|
-
if (atIdx === -1) {
|
|
185
|
-
throw new Error(`Invalid agent address: "${agentAddress}"`);
|
|
186
|
-
}
|
|
187
|
-
return agentAddress.substring(0, atIdx);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function requireInstance(
|
|
191
|
-
db: DB["db"],
|
|
192
|
-
agentAddress: string,
|
|
193
|
-
): Promise<
|
|
194
|
-
NonNullable<Awaited<ReturnType<typeof db.query.agentInstance.findFirst>>>
|
|
195
|
-
> {
|
|
196
|
-
const row = await db.query.agentInstance.findFirst({
|
|
197
|
-
where: and(
|
|
198
|
-
eq(agentInstance.address, agentAddress),
|
|
199
|
-
isNull(agentInstance.endedAt),
|
|
200
|
-
),
|
|
201
|
-
});
|
|
202
|
-
if (!row) {
|
|
203
|
-
throw new Error(`No active instance found for address "${agentAddress}"`);
|
|
204
|
-
}
|
|
205
|
-
return row;
|
|
206
|
-
}
|