@intx/hub-sessions 0.1.2 → 0.3.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.
Files changed (113) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +82 -1
  3. package/dist/agent-repo.d.ts +93 -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 +189 -0
  7. package/dist/asset-service.d.ts +104 -0
  8. package/dist/asset-service.js +267 -0
  9. package/dist/committed-source-tree.d.ts +10 -0
  10. package/dist/committed-source-tree.js +35 -0
  11. package/dist/credential-push.d.ts +33 -0
  12. package/dist/credential-push.js +109 -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 +365 -0
  17. package/dist/hub-session-lookups.d.ts +135 -0
  18. package/dist/hub-session-lookups.js +663 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +87 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +24 -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 +1829 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +56 -0
  30. package/dist/repo-store/subscribe-kind.js +144 -0
  31. package/dist/repo-store/types.d.ts +571 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +416 -0
  34. package/dist/session-service.js +1191 -0
  35. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  36. package/dist/sidecar-allocation/contracts.js +21 -0
  37. package/dist/sidecar-allocation/index.d.ts +4 -0
  38. package/dist/sidecar-allocation/index.js +3 -0
  39. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  40. package/dist/sidecar-allocation/placement-policy.js +21 -0
  41. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  42. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  43. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  44. package/dist/sidecar-allocation/reconciler.js +431 -0
  45. package/dist/skill-kind.d.ts +41 -0
  46. package/dist/skill-kind.js +292 -0
  47. package/dist/substrate.d.ts +8 -0
  48. package/dist/substrate.js +21 -0
  49. package/dist/workflow-allocation-service.d.ts +58 -0
  50. package/dist/workflow-allocation-service.js +239 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +40 -0
  56. package/dist/workflow-dispatch-service.js +146 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +37 -0
  60. package/dist/workflow-kind.js +310 -0
  61. package/dist/workflow-probe-gate.d.ts +214 -0
  62. package/dist/workflow-probe-gate.js +207 -0
  63. package/dist/workflow-run-event-log.d.ts +21 -0
  64. package/dist/workflow-run-event-log.js +51 -0
  65. package/dist/workflow-run-kind.d.ts +440 -0
  66. package/dist/workflow-run-kind.js +2916 -0
  67. package/dist/workflow-run-reader.d.ts +47 -0
  68. package/dist/workflow-run-reader.js +153 -0
  69. package/dist/workflow-run-restore.d.ts +15 -0
  70. package/dist/workflow-run-restore.js +26 -0
  71. package/dist/workflow-source-closure.d.ts +35 -0
  72. package/dist/workflow-source-closure.js +342 -0
  73. package/dist/ws/index.d.ts +3 -0
  74. package/dist/ws/index.js +3 -0
  75. package/dist/ws/sidecar-events.d.ts +222 -0
  76. package/dist/ws/sidecar-events.js +72 -0
  77. package/dist/ws/sidecar-handler.d.ts +305 -0
  78. package/dist/ws/sidecar-handler.js +2537 -0
  79. package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
  80. package/dist/ws/sidecar-token-authenticator.js +81 -0
  81. package/package.json +35 -12
  82. package/src/agent-repo.test.ts +0 -310
  83. package/src/agent-repo.ts +0 -165
  84. package/src/agent-state-kind.test.ts +0 -247
  85. package/src/agent-state-kind.ts +0 -204
  86. package/src/asset-service.test.ts +0 -540
  87. package/src/asset-service.ts +0 -378
  88. package/src/available-skills-stanza.test.ts +0 -87
  89. package/src/available-skills-stanza.ts +0 -47
  90. package/src/credential-push.ts +0 -65
  91. package/src/event-collector-registry.test.ts +0 -73
  92. package/src/event-collector-registry.ts +0 -171
  93. package/src/event-collector.test.ts +0 -1387
  94. package/src/event-collector.ts +0 -424
  95. package/src/hub-session-lookups.ts +0 -206
  96. package/src/hub-session-orchestrator.test.ts +0 -510
  97. package/src/hub-session-orchestrator.ts +0 -213
  98. package/src/index.ts +0 -78
  99. package/src/repo-store/index.ts +0 -15
  100. package/src/repo-store/store.test.ts +0 -1169
  101. package/src/repo-store/store.ts +0 -428
  102. package/src/repo-store/types.ts +0 -253
  103. package/src/session-service.test.ts +0 -895
  104. package/src/session-service.ts +0 -464
  105. package/src/skill-kind.test.ts +0 -599
  106. package/src/skill-kind.ts +0 -350
  107. package/src/ws/index.ts +0 -18
  108. package/src/ws/sidecar-events.test.ts +0 -96
  109. package/src/ws/sidecar-events.ts +0 -231
  110. package/src/ws/sidecar-handler.test.ts +0 -2217
  111. package/src/ws/sidecar-handler.ts +0 -1574
  112. package/tsconfig.json +0 -4
  113. 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
- });