@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.
Files changed (87) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +84 -1
  3. package/dist/agent-repo.d.ts +89 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +185 -0
  7. package/dist/asset-service.d.ts +123 -0
  8. package/dist/asset-service.js +349 -0
  9. package/dist/available-skills-stanza.d.ts +21 -0
  10. package/dist/available-skills-stanza.js +32 -0
  11. package/dist/credential-push.d.ts +32 -0
  12. package/dist/credential-push.js +85 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +357 -0
  17. package/dist/hub-session-lookups.d.ts +17 -0
  18. package/dist/hub-session-lookups.js +204 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +122 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +16 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1692 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +53 -0
  30. package/dist/repo-store/subscribe-kind.js +179 -0
  31. package/dist/repo-store/types.d.ts +483 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +235 -0
  34. package/dist/session-service.js +997 -0
  35. package/dist/skill-kind.d.ts +41 -0
  36. package/dist/skill-kind.js +288 -0
  37. package/dist/substrate.d.ts +8 -0
  38. package/dist/substrate.js +21 -0
  39. package/dist/workflow-kind.d.ts +21 -0
  40. package/dist/workflow-kind.js +263 -0
  41. package/dist/workflow-run-event-log.d.ts +21 -0
  42. package/dist/workflow-run-event-log.js +51 -0
  43. package/dist/workflow-run-kind.d.ts +326 -0
  44. package/dist/workflow-run-kind.js +2646 -0
  45. package/dist/workflow-run-reader.d.ts +47 -0
  46. package/dist/workflow-run-reader.js +157 -0
  47. package/dist/ws/index.d.ts +3 -0
  48. package/dist/ws/index.js +3 -0
  49. package/dist/ws/sidecar-events.d.ts +134 -0
  50. package/dist/ws/sidecar-events.js +70 -0
  51. package/dist/ws/sidecar-handler.d.ts +184 -0
  52. package/dist/ws/sidecar-handler.js +1603 -0
  53. package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
  54. package/dist/ws/sidecar-token-authenticator.js +24 -0
  55. package/package.json +34 -12
  56. package/src/agent-repo.test.ts +0 -310
  57. package/src/agent-repo.ts +0 -165
  58. package/src/agent-state-kind.test.ts +0 -247
  59. package/src/agent-state-kind.ts +0 -204
  60. package/src/asset-service.test.ts +0 -540
  61. package/src/asset-service.ts +0 -378
  62. package/src/available-skills-stanza.test.ts +0 -87
  63. package/src/available-skills-stanza.ts +0 -47
  64. package/src/credential-push.ts +0 -65
  65. package/src/event-collector-registry.test.ts +0 -73
  66. package/src/event-collector-registry.ts +0 -171
  67. package/src/event-collector.test.ts +0 -1387
  68. package/src/event-collector.ts +0 -424
  69. package/src/hub-session-lookups.ts +0 -206
  70. package/src/hub-session-orchestrator.test.ts +0 -510
  71. package/src/hub-session-orchestrator.ts +0 -213
  72. package/src/index.ts +0 -78
  73. package/src/repo-store/index.ts +0 -15
  74. package/src/repo-store/store.test.ts +0 -1169
  75. package/src/repo-store/store.ts +0 -428
  76. package/src/repo-store/types.ts +0 -253
  77. package/src/session-service.test.ts +0 -895
  78. package/src/session-service.ts +0 -464
  79. package/src/skill-kind.test.ts +0 -599
  80. package/src/skill-kind.ts +0 -350
  81. package/src/ws/index.ts +0 -18
  82. package/src/ws/sidecar-events.test.ts +0 -96
  83. package/src/ws/sidecar-events.ts +0 -231
  84. package/src/ws/sidecar-handler.test.ts +0 -2217
  85. package/src/ws/sidecar-handler.ts +0 -1574
  86. package/tsconfig.json +0 -4
  87. package/tsconfig.tsbuildinfo +0 -1
@@ -1,510 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
-
3
- import type { DB } from "@intx/db";
4
- import type { GrantRule, GrantStore } from "@intx/types/authz";
5
- import type { InferenceEvent, InferenceSource } from "@intx/types/runtime";
6
-
7
- import type { AgentRepoStore, DeployContent } from "./agent-repo";
8
- import type { RepoStore } from "./repo-store";
9
- import type { EventCollectorRegistry } from "./event-collector-registry";
10
- import { createHubSessionOrchestrator } from "./hub-session-orchestrator";
11
- import {
12
- createSidecarEmitter,
13
- type SidecarEventEmitter,
14
- } from "./ws/sidecar-events";
15
-
16
- // ---------------------------------------------------------------------------
17
- // Fixtures and mocks
18
- // ---------------------------------------------------------------------------
19
-
20
- const TENANT_ID = "tnt_1";
21
- const PRINCIPAL_ID = "prn_1";
22
- const INSTANCE_ID = "ins_1";
23
- const AGENT_ID = "agt_1";
24
- const AGENT_ADDRESS = "ins_1@tenant.local";
25
- const SESSION_ID = "ses_1";
26
-
27
- type InstanceStatus = "deployed" | "running" | "updating" | "error" | "stopped";
28
-
29
- type InstanceRow = {
30
- id: string;
31
- agentId: string;
32
- tenantId: string;
33
- principalId: string;
34
- address: string;
35
- status: InstanceStatus;
36
- sessionId: string | null;
37
- publicKey: string | null;
38
- createdAt: Date;
39
- updatedAt: Date;
40
- endedAt: Date | null;
41
- };
42
-
43
- function makeInstance(overrides: Partial<InstanceRow> = {}): InstanceRow {
44
- return {
45
- id: INSTANCE_ID,
46
- agentId: AGENT_ID,
47
- tenantId: TENANT_ID,
48
- principalId: PRINCIPAL_ID,
49
- address: AGENT_ADDRESS,
50
- status: "running",
51
- sessionId: SESSION_ID,
52
- publicKey: null,
53
- createdAt: new Date("2026-01-01"),
54
- updatedAt: new Date("2026-01-01"),
55
- endedAt: null,
56
- ...overrides,
57
- };
58
- }
59
-
60
- type UpdateCall = { table: string; set: Record<string, unknown> };
61
-
62
- type MockDBOpts = {
63
- instance?: InstanceRow | undefined;
64
- recordUpdates?: UpdateCall[];
65
- /** When set, `resolveInstanceSources` queries fan out into these
66
- * tables; the helper returns empty arrays so the orchestrator's
67
- * credential push is a no-op. */
68
- emptyProviderTables?: boolean;
69
- };
70
-
71
- function createMockDB(opts: MockDBOpts) {
72
- const updates = opts.recordUpdates ?? [];
73
-
74
- function tableName(t: unknown): string {
75
- if (t && typeof t === "object" && "name" in t && typeof t.name === "string")
76
- return t.name;
77
- return "<unknown>";
78
- }
79
-
80
- /* eslint-disable @typescript-eslint/no-unsafe-type-assertion --
81
- * drizzle PgDatabase type cannot be structurally satisfied in tests */
82
- return {
83
- query: {
84
- agentInstance: {
85
- findFirst: async () => opts.instance,
86
- },
87
- agent: { findFirst: async () => undefined },
88
- agentSession: { findFirst: async () => undefined },
89
- provider: { findFirst: async () => undefined, findMany: async () => [] },
90
- credential: {
91
- findFirst: async () => undefined,
92
- findMany: async () => [],
93
- },
94
- oauthClient: { findFirst: async () => undefined },
95
- tenant: { findFirst: async () => undefined },
96
- },
97
- update(t: unknown) {
98
- return {
99
- set(values: Record<string, unknown>) {
100
- return {
101
- where: async () => {
102
- updates.push({ table: tableName(t), set: values });
103
- },
104
- };
105
- },
106
- };
107
- },
108
- select() {
109
- return {
110
- from: () => ({
111
- where: () => ({
112
- limit: () => Promise.resolve([]),
113
- orderBy: () => ({ limit: () => Promise.resolve([]) }),
114
- }),
115
- innerJoin: () => ({
116
- where: () => ({ limit: () => Promise.resolve([]) }),
117
- }),
118
- }),
119
- };
120
- },
121
- } as unknown as DB["db"];
122
- /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
123
- }
124
-
125
- type RouterCall =
126
- | { kind: "sendGrantsUpdate"; addr: string; grants: GrantRule[] }
127
- | {
128
- kind: "sendSourcesUpdate";
129
- addr: string;
130
- sources: InferenceSource[];
131
- defaultSource: string;
132
- }
133
- | {
134
- kind: "sendPack";
135
- addr: string;
136
- pack: Uint8Array;
137
- ref: string;
138
- sha: string;
139
- }
140
- | { kind: "dispatchAgentEvent"; addr: string; event: unknown };
141
-
142
- function createRouterFacade(): {
143
- facade: Parameters<typeof createHubSessionOrchestrator>[0]["router"];
144
- calls: RouterCall[];
145
- } {
146
- const calls: RouterCall[] = [];
147
- return {
148
- calls,
149
- facade: {
150
- async sendGrantsUpdate(addr, grants) {
151
- calls.push({ kind: "sendGrantsUpdate", addr, grants });
152
- },
153
- async sendSourcesUpdate(addr, sources, defaultSource) {
154
- calls.push({ kind: "sendSourcesUpdate", addr, sources, defaultSource });
155
- },
156
- async sendPack(addr, pack, ref, sha) {
157
- calls.push({ kind: "sendPack", addr, pack, ref, sha });
158
- },
159
- dispatchAgentEvent(addr, event) {
160
- calls.push({ kind: "dispatchAgentEvent", addr, event });
161
- },
162
- },
163
- };
164
- }
165
-
166
- type CollectorCall =
167
- | { kind: "create"; addr: string; sessionId: string }
168
- | { kind: "dispatch"; addr: string; event: InferenceEvent }
169
- | { kind: "abandon"; addr: string };
170
-
171
- function createCollectorRegistry(
172
- initiallyHas: Set<string> = new Set<string>(),
173
- ): {
174
- registry: EventCollectorRegistry;
175
- calls: CollectorCall[];
176
- has: Set<string>;
177
- } {
178
- const calls: CollectorCall[] = [];
179
- const has = new Set(initiallyHas);
180
- return {
181
- calls,
182
- has,
183
- registry: {
184
- create(addr, _tenantId, sessionId, _instanceId) {
185
- has.add(addr);
186
- calls.push({ kind: "create", addr, sessionId });
187
- },
188
- dispatch(addr, event) {
189
- calls.push({ kind: "dispatch", addr, event });
190
- },
191
- abandon(addr) {
192
- has.delete(addr);
193
- calls.push({ kind: "abandon", addr });
194
- },
195
- has: (addr) => has.has(addr),
196
- getStatus: () => undefined,
197
- getAccumulatedText: () => undefined,
198
- getCurrentTurnId: () => undefined,
199
- getLastTurnId: () => undefined,
200
- },
201
- };
202
- }
203
-
204
- function createGrantStoreStub(grants: GrantRule[] = []): GrantStore {
205
- return {
206
- collectGrants: async () => grants,
207
- };
208
- }
209
-
210
- type RepoCall = { kind: "createDeployPack"; agentId: string };
211
-
212
- function createRepoStoreStub(): {
213
- store: AgentRepoStore;
214
- calls: RepoCall[];
215
- } {
216
- const calls: RepoCall[] = [];
217
- return {
218
- calls,
219
- store: {
220
- async writeDeployTree(_agentId: string, _content: DeployContent) {
221
- throw new Error("mock: writeDeployTree not implemented");
222
- },
223
- async createDeployPack(agentId: string) {
224
- calls.push({ kind: "createDeployPack", agentId });
225
- return {
226
- pack: new Uint8Array([1, 2, 3]),
227
- commitSha: "c".repeat(40),
228
- ref: "refs/heads/deploy",
229
- };
230
- },
231
- async receiveStatePack() {
232
- throw new Error("mock: receiveStatePack not implemented");
233
- },
234
- getSigningPublicKey() {
235
- return new Uint8Array(32);
236
- },
237
- async getDeployRef() {
238
- return null;
239
- },
240
- repoStore: unusedRepoStore(),
241
- },
242
- };
243
- }
244
-
245
- function unusedRepoStore(): RepoStore {
246
- // The orchestrator tests do not touch the substrate; a throwing
247
- // stub keeps the AgentRepoStore surface fully populated without
248
- // pulling a real on-disk store into orchestrator-level unit tests.
249
- const unused = () =>
250
- Promise.reject(new Error("mock AgentRepoStore.repoStore is not wired"));
251
- return {
252
- initRepo: unused,
253
- writeTree: unused,
254
- receivePack: unused,
255
- createPack: unused,
256
- resolveRef: unused,
257
- listRefs: unused,
258
- resolveHead: unused,
259
- getRepoDir: () => {
260
- throw new Error("mock AgentRepoStore.repoStore is not wired");
261
- },
262
- };
263
- }
264
-
265
- type Harness = {
266
- events: SidecarEventEmitter;
267
- router: ReturnType<typeof createRouterFacade>;
268
- collectors: ReturnType<typeof createCollectorRegistry>;
269
- repo: ReturnType<typeof createRepoStoreStub>;
270
- updates: UpdateCall[];
271
- dispose: () => void;
272
- };
273
-
274
- function setup(opts: MockDBOpts & { grants?: GrantRule[] } = {}): Harness {
275
- const updates: UpdateCall[] = [];
276
- const db = createMockDB({ ...opts, recordUpdates: updates });
277
- const events = createSidecarEmitter();
278
- const router = createRouterFacade();
279
- const collectors = createCollectorRegistry();
280
- const repo = createRepoStoreStub();
281
- const grantStore = createGrantStoreStub(opts.grants);
282
-
283
- const orchestrator = createHubSessionOrchestrator({
284
- events,
285
- router: router.facade,
286
- db,
287
- eventCollectors: collectors.registry,
288
- grantStore,
289
- agentRepoStore: repo.store,
290
- });
291
-
292
- return {
293
- events,
294
- router,
295
- collectors,
296
- repo,
297
- updates,
298
- dispose: orchestrator.dispose,
299
- };
300
- }
301
-
302
- // ---------------------------------------------------------------------------
303
- // Tests
304
- // ---------------------------------------------------------------------------
305
-
306
- describe("createHubSessionOrchestrator", () => {
307
- let harness: Harness;
308
-
309
- beforeEach(() => {
310
- harness = setup({ instance: makeInstance() });
311
- });
312
-
313
- describe("agent.event", () => {
314
- test("valid event is dispatched to the collector registry", () => {
315
- // reactor.start is a valid InferenceEvent that arktype's
316
- // parseInferenceEvent will accept; the orchestrator dispatches
317
- // it to the collector unchanged.
318
- harness.events.emit("agent.event", {
319
- agentAddress: AGENT_ADDRESS,
320
- sessionId: SESSION_ID,
321
- event: { type: "reactor.start", seq: 1, data: { tools: [] } },
322
- });
323
- const dispatched = harness.collectors.calls.find(
324
- (c) => c.kind === "dispatch",
325
- );
326
- expect(dispatched).toBeDefined();
327
- });
328
-
329
- test("invalid event is dropped (no dispatch call)", () => {
330
- harness.events.emit("agent.event", {
331
- agentAddress: AGENT_ADDRESS,
332
- sessionId: SESSION_ID,
333
- event: { not_a_real_event: true },
334
- });
335
- const dispatched = harness.collectors.calls.find(
336
- (c) => c.kind === "dispatch",
337
- );
338
- expect(dispatched).toBeUndefined();
339
- });
340
- });
341
-
342
- describe("sidecar.disconnect", () => {
343
- test("abandons every collector for the closed connection", () => {
344
- harness.events.emit("sidecar.disconnect", {
345
- agentAddresses: ["a@x", "b@x", "c@x"],
346
- });
347
- const abandonedAddrs = harness.collectors.calls
348
- .filter((c) => c.kind === "abandon")
349
- .map((c) => (c.kind === "abandon" ? c.addr : null));
350
- expect(abandonedAddrs).toEqual(["a@x", "b@x", "c@x"]);
351
- });
352
- });
353
-
354
- describe("agent.deploy.ack", () => {
355
- test("stores the public key on the active instance", async () => {
356
- await harness.events.emitAndAwait("agent.deploy.ack", {
357
- agentAddress: AGENT_ADDRESS,
358
- publicKey: "deadbeef",
359
- });
360
- expect(harness.updates).toHaveLength(1);
361
- expect(harness.updates[0]?.set).toEqual({ publicKey: "deadbeef" });
362
- });
363
- });
364
-
365
- describe("agent.reconnected", () => {
366
- test("refreshes grants and skips status update when already running", async () => {
367
- const grant: GrantRule = {
368
- id: "grant-1",
369
- resource: "x:y",
370
- action: "read",
371
- effect: "allow",
372
- origin: "system",
373
- conditions: null,
374
- expiresAt: null,
375
- roleId: null,
376
- principalId: PRINCIPAL_ID,
377
- };
378
- harness = setup({ instance: makeInstance(), grants: [grant] });
379
-
380
- await harness.events.emitAndAwait("agent.reconnected", {
381
- agentAddress: AGENT_ADDRESS,
382
- });
383
-
384
- const grantsCall = harness.router.calls.find(
385
- (c) => c.kind === "sendGrantsUpdate",
386
- );
387
- expect(grantsCall).toBeDefined();
388
- if (grantsCall?.kind === "sendGrantsUpdate") {
389
- expect(grantsCall.grants).toEqual([grant]);
390
- }
391
-
392
- // status was already "running", no update should fire
393
- expect(harness.updates).toHaveLength(0);
394
- });
395
-
396
- test("flips status to running and restores collector when missing", async () => {
397
- harness = setup({ instance: makeInstance({ status: "deployed" }) });
398
-
399
- await harness.events.emitAndAwait("agent.reconnected", {
400
- agentAddress: AGENT_ADDRESS,
401
- });
402
-
403
- const statusUpdate = harness.updates.find(
404
- (u) => u.set["status"] === "running",
405
- );
406
- expect(statusUpdate).toBeDefined();
407
-
408
- const created = harness.collectors.calls.find((c) => c.kind === "create");
409
- expect(created).toBeDefined();
410
- if (created?.kind === "create") {
411
- expect(created.addr).toBe(AGENT_ADDRESS);
412
- expect(created.sessionId).toBe(SESSION_ID);
413
- }
414
- });
415
-
416
- test("throws when the instance has no active session", async () => {
417
- harness = setup({ instance: makeInstance({ sessionId: null }) });
418
-
419
- await expect(
420
- harness.events.emitAndAwait("agent.reconnected", {
421
- agentAddress: AGENT_ADDRESS,
422
- }),
423
- ).rejects.toThrow(/no active session/);
424
- });
425
-
426
- test("throws when no active instance exists", async () => {
427
- harness = setup({ instance: undefined });
428
-
429
- await expect(
430
- harness.events.emitAndAwait("agent.reconnected", {
431
- agentAddress: AGENT_ADDRESS,
432
- }),
433
- ).rejects.toThrow(/No active instance/);
434
- });
435
- });
436
-
437
- describe("deploy.ref.stale", () => {
438
- test("creates a deploy pack and pushes it via the router", async () => {
439
- await harness.events.emitAndAwait("deploy.ref.stale", {
440
- agentAddress: AGENT_ADDRESS,
441
- });
442
-
443
- expect(harness.repo.calls).toEqual([
444
- { kind: "createDeployPack", agentId: "ins_1" },
445
- ]);
446
-
447
- const sendPack = harness.router.calls.find((c) => c.kind === "sendPack");
448
- expect(sendPack).toBeDefined();
449
- if (sendPack?.kind === "sendPack") {
450
- expect(sendPack.addr).toBe(AGENT_ADDRESS);
451
- expect(sendPack.ref).toBe("refs/heads/deploy");
452
- }
453
- });
454
- });
455
-
456
- describe("mail.persisted", () => {
457
- test("dispatches a mail.delivered agent event for the recipient", () => {
458
- const raw = new TextEncoder().encode(
459
- [
460
- "From: sender@x",
461
- "To: recipient@x",
462
- "Subject: hello",
463
- "",
464
- "body",
465
- "",
466
- ].join("\r\n"),
467
- );
468
- const createdAt = new Date("2026-01-01T00:00:00.000Z");
469
-
470
- harness.events.emit("mail.persisted", {
471
- id: "mail_1",
472
- raw,
473
- createdAt,
474
- direction: "inbound",
475
- instanceId: INSTANCE_ID,
476
- address: AGENT_ADDRESS,
477
- });
478
-
479
- const dispatched = harness.router.calls.find(
480
- (c) => c.kind === "dispatchAgentEvent",
481
- );
482
- expect(dispatched).toBeDefined();
483
- if (dispatched?.kind !== "dispatchAgentEvent") return;
484
- expect(dispatched.addr).toBe(AGENT_ADDRESS);
485
-
486
- const evt = dispatched.event;
487
- expect(evt).toMatchObject({
488
- type: "mail.delivered",
489
- data: {
490
- id: "mail_1",
491
- direction: "inbound",
492
- receivedAt: "2026-01-01T00:00:00.000Z",
493
- },
494
- });
495
- });
496
- });
497
-
498
- describe("dispose", () => {
499
- test("removes all subscriptions so later emits are inert", () => {
500
- harness.dispose();
501
- harness.events.emit("sidecar.disconnect", {
502
- agentAddresses: [AGENT_ADDRESS],
503
- });
504
- const abandoned = harness.collectors.calls.find(
505
- (c) => c.kind === "abandon",
506
- );
507
- expect(abandoned).toBeUndefined();
508
- });
509
- });
510
- });