@nebutra/agent-runtime 0.2.1 → 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 (101) hide show
  1. package/LICENSE +21 -676
  2. package/dist/adapters/index.d.ts +24 -5
  3. package/dist/adapters/index.js +27 -0
  4. package/dist/adapters/index.js.map +1 -1
  5. package/dist/chunk-Q62VKHIT.js +178 -0
  6. package/dist/chunk-Q62VKHIT.js.map +1 -0
  7. package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
  8. package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +14 -17
  11. package/dist/index.js.map +1 -1
  12. package/dist/orchestration.d.ts +42 -1
  13. package/dist/orchestration.js +7 -3
  14. package/dist/sandbox.js +1 -1
  15. package/package.json +80 -30
  16. package/.turbo/turbo-build.log +0 -130
  17. package/.turbo/turbo-test.log +0 -47
  18. package/.turbo/turbo-typecheck.log +0 -4
  19. package/CHANGELOG.md +0 -250
  20. package/dist/chunk-MX2WL43P.js +0 -90
  21. package/dist/chunk-MX2WL43P.js.map +0 -1
  22. package/examples/pulsar-quickstart.ts +0 -35
  23. package/examples/resume-branch.ts +0 -45
  24. package/examples/subagent-fanout.ts +0 -20
  25. package/src/adapters/dispatcher-sse.test.ts +0 -218
  26. package/src/adapters/dispatcher-sse.ts +0 -222
  27. package/src/adapters/index.ts +0 -18
  28. package/src/adapters/mcp-catalog.test.ts +0 -213
  29. package/src/adapters/mcp-catalog.ts +0 -188
  30. package/src/adapters/prisma-rollout.test.ts +0 -153
  31. package/src/adapters/prisma-rollout.ts +0 -104
  32. package/src/agent-runtime.test.ts +0 -176
  33. package/src/artifact-stream.test.ts +0 -330
  34. package/src/artifact-stream.ts +0 -453
  35. package/src/channel-gateway.test.ts +0 -432
  36. package/src/channel-gateway.ts +0 -357
  37. package/src/cli.ts +0 -49
  38. package/src/code-review.test.ts +0 -501
  39. package/src/code-review.ts +0 -495
  40. package/src/command-suggestions.test.ts +0 -251
  41. package/src/command-suggestions.ts +0 -338
  42. package/src/commands.test.ts +0 -184
  43. package/src/commands.ts +0 -140
  44. package/src/commit-message.test.ts +0 -249
  45. package/src/commit-message.ts +0 -180
  46. package/src/context-compaction.test.ts +0 -522
  47. package/src/context-compaction.ts +0 -438
  48. package/src/definitions.test.ts +0 -78
  49. package/src/definitions.ts +0 -190
  50. package/src/deployment-status.test.ts +0 -215
  51. package/src/deployment-status.ts +0 -227
  52. package/src/design-context.test.ts +0 -195
  53. package/src/design-context.ts +0 -198
  54. package/src/dispatcher.test.ts +0 -234
  55. package/src/dispatcher.ts +0 -189
  56. package/src/durable-turn.test.ts +0 -209
  57. package/src/durable-turn.ts +0 -135
  58. package/src/edit-planner.test.ts +0 -204
  59. package/src/edit-planner.ts +0 -325
  60. package/src/fuzzy-match.test.ts +0 -311
  61. package/src/fuzzy-match.ts +0 -444
  62. package/src/hook-pipeline.test.ts +0 -279
  63. package/src/hook-pipeline.ts +0 -373
  64. package/src/inbound-admission.test.ts +0 -394
  65. package/src/inbound-admission.ts +0 -246
  66. package/src/index.ts +0 -49
  67. package/src/loop.test.ts +0 -161
  68. package/src/loop.ts +0 -211
  69. package/src/mcp-bridge.test.ts +0 -165
  70. package/src/mcp-bridge.ts +0 -81
  71. package/src/memory-provider.test.ts +0 -232
  72. package/src/memory-provider.ts +0 -257
  73. package/src/model.ts +0 -168
  74. package/src/orchestration.test.ts +0 -53
  75. package/src/orchestration.ts +0 -146
  76. package/src/permission-ruleset.test.ts +0 -301
  77. package/src/permission-ruleset.ts +0 -1
  78. package/src/policy.ts +0 -151
  79. package/src/project-repo.test.ts +0 -232
  80. package/src/project-repo.ts +0 -311
  81. package/src/protocol.ts +0 -159
  82. package/src/pulsar.test.ts +0 -156
  83. package/src/pulsar.ts +0 -322
  84. package/src/rollout-store-persistent.test.ts +0 -217
  85. package/src/rollout-store-persistent.ts +0 -166
  86. package/src/rollout.ts +0 -150
  87. package/src/sandbox.ts +0 -113
  88. package/src/session-share.test.ts +0 -360
  89. package/src/session-share.ts +0 -310
  90. package/src/skill-distillation.test.ts +0 -177
  91. package/src/skill-distillation.ts +0 -369
  92. package/src/skills.test.ts +0 -277
  93. package/src/skills.ts +0 -277
  94. package/src/subagents.test.ts +0 -290
  95. package/src/subagents.ts +0 -332
  96. package/src/tools.test.ts +0 -12
  97. package/src/tools.ts +0 -129
  98. package/src/workbench.test.ts +0 -0
  99. package/src/workbench.ts +0 -0
  100. package/tsconfig.json +0 -12
  101. package/tsup.config.ts +0 -36
@@ -1,432 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import {
4
- asChannelId,
5
- type ChannelAdapter,
6
- ChannelGateway,
7
- type ChannelMeta,
8
- ChannelRegistry,
9
- DuplicateChannelError,
10
- type InboundMessage,
11
- type OutboundMessage,
12
- replyTo,
13
- TenantMismatchError,
14
- UnknownChannelError,
15
- } from "./channel-gateway.js";
16
-
17
- /* -------------------------------------------------------------------------- */
18
- /* Fixtures */
19
- /* -------------------------------------------------------------------------- */
20
-
21
- function meta(id: string, over?: Partial<ChannelMeta>): ChannelMeta {
22
- return {
23
- id: asChannelId(id),
24
- displayName: `Channel ${id}`,
25
- capabilities: { threads: true, reactions: false, streaming: true },
26
- ...over,
27
- };
28
- }
29
-
30
- /**
31
- * Fake adapter: parses a minimal "envelope" payload into an InboundMessage.
32
- * Returns null for non-message events (joins, typing, …) and never throws on
33
- * junk. `sendOutbound` records every dispatched message.
34
- */
35
- function fakeAdapter(
36
- id: string,
37
- metaOver?: Partial<ChannelMeta>,
38
- ): ChannelAdapter & {
39
- sent: OutboundMessage[];
40
- } {
41
- const sent: OutboundMessage[] = [];
42
- return {
43
- sent,
44
- meta: meta(id, metaOver),
45
- parseInbound(tenantId, raw) {
46
- if (typeof raw !== "object" || raw === null) return null;
47
- const r = raw as Record<string, unknown>;
48
- // Non-message events → null (never throw).
49
- if (r["type"] !== "message") return null;
50
- if (typeof r["chatId"] !== "string" || typeof r["text"] !== "string") return null;
51
- const inbound: InboundMessage = {
52
- tenantId,
53
- channelId: asChannelId(id),
54
- chatId: r["chatId"],
55
- senderId: typeof r["senderId"] === "string" ? r["senderId"] : "anon",
56
- text: r["text"],
57
- receivedAt: "2026-05-17T00:00:00.000Z",
58
- raw,
59
- ...(typeof r["threadId"] === "string" ? { threadId: r["threadId"] } : {}),
60
- ...(typeof r["senderLabel"] === "string" ? { senderLabel: r["senderLabel"] } : {}),
61
- };
62
- return inbound;
63
- },
64
- async sendOutbound(msg) {
65
- sent.push(msg);
66
- return { messageId: `${id}-${sent.length}` };
67
- },
68
- };
69
- }
70
-
71
- /** Adapter whose parseInbound returns a structurally-invalid object. */
72
- function malformedAdapter(id: string): ChannelAdapter {
73
- return {
74
- meta: meta(id),
75
- // @ts-expect-error — intentionally returns a non-conforming shape to
76
- // exercise the gateway's zod boundary validation.
77
- parseInbound() {
78
- return { tenantId: "", chatId: 123, text: null };
79
- },
80
- async sendOutbound() {
81
- return { messageId: "x" };
82
- },
83
- };
84
- }
85
-
86
- /* -------------------------------------------------------------------------- */
87
- /* asChannelId / branded type */
88
- /* -------------------------------------------------------------------------- */
89
-
90
- describe("asChannelId", () => {
91
- it("brands a non-empty string", () => {
92
- expect(asChannelId("slack")).toBe("slack");
93
- });
94
-
95
- it("rejects empty / blank ids", () => {
96
- expect(() => asChannelId("")).toThrow(/channel/i);
97
- expect(() => asChannelId(" ")).toThrow(/channel/i);
98
- });
99
- });
100
-
101
- /* -------------------------------------------------------------------------- */
102
- /* ChannelAdapter.parseInbound */
103
- /* -------------------------------------------------------------------------- */
104
-
105
- describe("ChannelAdapter.parseInbound", () => {
106
- it("returns null for junk without throwing", () => {
107
- const a = fakeAdapter("c1");
108
- expect(a.parseInbound("org_a", null)).toBeNull();
109
- expect(a.parseInbound("org_a", "garbage")).toBeNull();
110
- expect(a.parseInbound("org_a", { type: "typing" })).toBeNull();
111
- expect(a.parseInbound("org_a", { type: "join", user: "u1" })).toBeNull();
112
- });
113
-
114
- it("stamps the tenantId onto the normalized result", () => {
115
- const a = fakeAdapter("c1");
116
- const msg = a.parseInbound("org_a", { type: "message", chatId: "ch1", text: "hi" });
117
- expect(msg).not.toBeNull();
118
- expect(msg?.tenantId).toBe("org_a");
119
- expect(msg?.channelId).toBe("c1");
120
- });
121
- });
122
-
123
- /* -------------------------------------------------------------------------- */
124
- /* ChannelRegistry */
125
- /* -------------------------------------------------------------------------- */
126
-
127
- describe("ChannelRegistry", () => {
128
- it("registers and looks up an adapter by channelId for a tenant", () => {
129
- const reg = new ChannelRegistry();
130
- const a = fakeAdapter("c1");
131
- reg.register("org_a", a);
132
- expect(reg.lookup("org_a", asChannelId("c1"))).toBe(a);
133
- });
134
-
135
- it("throws DuplicateChannelError on duplicate registration", () => {
136
- const reg = new ChannelRegistry();
137
- reg.register("org_a", fakeAdapter("c1"));
138
- expect(() => reg.register("org_a", fakeAdapter("c1"))).toThrow(DuplicateChannelError);
139
- });
140
-
141
- it("throws UnknownChannelError on unknown lookup", () => {
142
- const reg = new ChannelRegistry();
143
- expect(() => reg.lookup("org_a", asChannelId("nope"))).toThrow(UnknownChannelError);
144
- });
145
-
146
- it("fails closed on empty tenantId for register and lookup", () => {
147
- const reg = new ChannelRegistry();
148
- expect(() => reg.register("", fakeAdapter("c1"))).toThrow(/tenant/i);
149
- expect(() => reg.register(" ", fakeAdapter("c1"))).toThrow(/tenant/i);
150
- expect(() => reg.lookup("", asChannelId("c1"))).toThrow(/tenant/i);
151
- });
152
-
153
- it("isolates tenants — A's adapter is invisible to B (foreign tenant fails closed)", () => {
154
- const reg = new ChannelRegistry();
155
- reg.register("org_a", fakeAdapter("c1"));
156
- expect(() => reg.lookup("org_b", asChannelId("c1"))).toThrow(UnknownChannelError);
157
- // Same channelId may be registered independently per tenant.
158
- expect(() => reg.register("org_b", fakeAdapter("c1"))).not.toThrow();
159
- });
160
-
161
- it("lists registered channel ids per tenant", () => {
162
- const reg = new ChannelRegistry();
163
- reg.register("org_a", fakeAdapter("c1"));
164
- reg.register("org_a", fakeAdapter("c2"));
165
- reg.register("org_b", fakeAdapter("c3"));
166
- expect([...reg.list("org_a")].sort()).toEqual(["c1", "c2"]);
167
- expect(reg.list("org_b")).toEqual(["c3"]);
168
- expect(reg.list("org_unknown")).toEqual([]);
169
- });
170
- });
171
-
172
- /* -------------------------------------------------------------------------- */
173
- /* ChannelGateway.ingest */
174
- /* -------------------------------------------------------------------------- */
175
-
176
- describe("ChannelGateway.ingest", () => {
177
- it("normalizes a valid inbound message tenant-stamped", () => {
178
- const reg = new ChannelRegistry();
179
- reg.register("org_a", fakeAdapter("c1"));
180
- const gw = new ChannelGateway(reg);
181
-
182
- const msg = gw.ingest("org_a", asChannelId("c1"), {
183
- type: "message",
184
- chatId: "ch1",
185
- text: "hello",
186
- threadId: "t1",
187
- });
188
- expect(msg).not.toBeNull();
189
- expect(msg?.tenantId).toBe("org_a");
190
- expect(msg?.channelId).toBe("c1");
191
- expect(msg?.chatId).toBe("ch1");
192
- expect(msg?.threadId).toBe("t1");
193
- });
194
-
195
- it("swallows non-message events as null (never throws to caller)", () => {
196
- const reg = new ChannelRegistry();
197
- reg.register("org_a", fakeAdapter("c1"));
198
- const gw = new ChannelGateway(reg);
199
-
200
- expect(gw.ingest("org_a", asChannelId("c1"), { type: "typing" })).toBeNull();
201
- expect(gw.ingest("org_a", asChannelId("c1"), "junk")).toBeNull();
202
- expect(gw.ingest("org_a", asChannelId("c1"), null)).toBeNull();
203
- });
204
-
205
- it("rejects a malformed adapter result via zod (typed, no crash)", () => {
206
- const reg = new ChannelRegistry();
207
- reg.register("org_a", malformedAdapter("c1"));
208
- const gw = new ChannelGateway(reg);
209
-
210
- expect(() => gw.ingest("org_a", asChannelId("c1"), { type: "message" })).toThrow();
211
- // Does not surface as a raw TypeError — it is a validation error.
212
- try {
213
- gw.ingest("org_a", asChannelId("c1"), { type: "message" });
214
- } catch (e) {
215
- expect(e).toBeInstanceOf(Error);
216
- expect((e as Error).message).toMatch(/inbound|valid/i);
217
- }
218
- });
219
-
220
- it("throws UnknownChannelError when the channel is not registered", () => {
221
- const gw = new ChannelGateway(new ChannelRegistry());
222
- expect(() => gw.ingest("org_a", asChannelId("ghost"), { type: "message" })).toThrow(
223
- UnknownChannelError,
224
- );
225
- });
226
-
227
- it("fails closed on empty / foreign tenant", () => {
228
- const reg = new ChannelRegistry();
229
- reg.register("org_a", fakeAdapter("c1"));
230
- const gw = new ChannelGateway(reg);
231
- expect(() => gw.ingest("", asChannelId("c1"), { type: "message" })).toThrow(/tenant/i);
232
- expect(() => gw.ingest("org_b", asChannelId("c1"), { type: "message" })).toThrow(
233
- UnknownChannelError,
234
- );
235
- });
236
- });
237
-
238
- /* -------------------------------------------------------------------------- */
239
- /* ChannelGateway.route */
240
- /* -------------------------------------------------------------------------- */
241
-
242
- describe("ChannelGateway.route", () => {
243
- it("sends a reply back through the originating channel", async () => {
244
- const reg = new ChannelRegistry();
245
- const a = fakeAdapter("c1");
246
- const b = fakeAdapter("c2");
247
- reg.register("org_a", a);
248
- reg.register("org_a", b);
249
- const gw = new ChannelGateway(reg);
250
-
251
- const res = await gw.route({
252
- tenantId: "org_a",
253
- channelId: asChannelId("c2"),
254
- chatId: "ch9",
255
- text: "pong",
256
- });
257
-
258
- expect(res.messageId).toBe("c2-1");
259
- expect(b.sent).toHaveLength(1);
260
- expect(a.sent).toHaveLength(0);
261
- expect(b.sent[0]).toMatchObject({ chatId: "ch9", text: "pong" });
262
- });
263
-
264
- it("throws UnknownChannelError when routing to an unknown channel", async () => {
265
- const gw = new ChannelGateway(new ChannelRegistry());
266
- await expect(
267
- gw.route({ tenantId: "org_a", channelId: asChannelId("ghost"), chatId: "c", text: "x" }),
268
- ).rejects.toThrow(UnknownChannelError);
269
- });
270
-
271
- it("fails closed on empty tenantId for route", async () => {
272
- const reg = new ChannelRegistry();
273
- reg.register("org_a", fakeAdapter("c1"));
274
- const gw = new ChannelGateway(reg);
275
- await expect(
276
- gw.route({ tenantId: "", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
277
- ).rejects.toThrow(/tenant/i);
278
- });
279
-
280
- it("rejects a foreign tenant routing through another tenant's channel", async () => {
281
- const reg = new ChannelRegistry();
282
- reg.register("org_a", fakeAdapter("c1"));
283
- const gw = new ChannelGateway(reg);
284
- await expect(
285
- gw.route({ tenantId: "org_b", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
286
- ).rejects.toThrow(UnknownChannelError);
287
- });
288
-
289
- it("rejects a tenant mismatch between reply and resolved adapter scope", async () => {
290
- const reg = new ChannelRegistry();
291
- reg.register("org_a", fakeAdapter("c1"));
292
- const gw = new ChannelGateway(reg);
293
- // Whitespace tenant — fail closed before any send.
294
- await expect(
295
- gw.route({ tenantId: " ", channelId: asChannelId("c1"), chatId: "c", text: "x" }),
296
- ).rejects.toThrow(/tenant/i);
297
- });
298
-
299
- it("rejects a structurally-invalid OutboundMessage via zod", async () => {
300
- const reg = new ChannelRegistry();
301
- const a = fakeAdapter("c1");
302
- reg.register("org_a", a);
303
- const gw = new ChannelGateway(reg);
304
- await expect(
305
- // @ts-expect-error — text missing on purpose to hit the zod boundary.
306
- gw.route({ tenantId: "org_a", channelId: asChannelId("c1"), chatId: "c" }),
307
- ).rejects.toThrow();
308
- expect(a.sent).toHaveLength(0);
309
- });
310
- });
311
-
312
- /* -------------------------------------------------------------------------- */
313
- /* replyTo (pure helper) */
314
- /* -------------------------------------------------------------------------- */
315
-
316
- describe("replyTo", () => {
317
- const inbound: InboundMessage = {
318
- tenantId: "org_a",
319
- channelId: asChannelId("c1"),
320
- chatId: "ch1",
321
- threadId: "t1",
322
- senderId: "u1",
323
- text: "ping",
324
- receivedAt: "2026-05-17T00:00:00.000Z",
325
- raw: { id: "m1" },
326
- };
327
-
328
- it("derives a correctly-addressed OutboundMessage on the same channel/chat/thread", () => {
329
- const out = replyTo(inbound, "pong");
330
- expect(out).toEqual({
331
- tenantId: "org_a",
332
- channelId: asChannelId("c1"),
333
- chatId: "ch1",
334
- threadId: "t1",
335
- text: "pong",
336
- });
337
- });
338
-
339
- it("wires replyToMessageId when provided", () => {
340
- const out = replyTo(inbound, "pong", { replyToMessageId: "m1" });
341
- expect(out.replyToMessageId).toBe("m1");
342
- });
343
-
344
- it("does not mutate the inbound message", () => {
345
- const snapshot = JSON.parse(JSON.stringify(inbound));
346
- replyTo(inbound, "pong", { replyToMessageId: "m1" });
347
- expect(inbound).toEqual(snapshot);
348
- });
349
-
350
- it("omits threadId when the inbound has none (exactOptional-safe)", () => {
351
- const noThread: InboundMessage = {
352
- tenantId: "org_a",
353
- channelId: asChannelId("c1"),
354
- chatId: "ch1",
355
- senderId: "u1",
356
- text: "ping",
357
- receivedAt: "2026-05-17T00:00:00.000Z",
358
- raw: {},
359
- };
360
- const out = replyTo(noThread, "pong");
361
- expect("threadId" in out).toBe(false);
362
- });
363
-
364
- it("fails closed when the inbound carries an empty tenantId", () => {
365
- const bad: InboundMessage = { ...inbound, tenantId: "" };
366
- expect(() => replyTo(bad, "pong")).toThrow(/tenant/i);
367
- });
368
-
369
- it("a replyTo result routes back through the same channel end-to-end", async () => {
370
- const reg = new ChannelRegistry();
371
- const a = fakeAdapter("c1");
372
- reg.register("org_a", a);
373
- const gw = new ChannelGateway(reg);
374
-
375
- const out = replyTo(inbound, "pong", { replyToMessageId: "m1" });
376
- const res = await gw.route(out);
377
- expect(res.messageId).toBe("c1-1");
378
- expect(a.sent[0]).toMatchObject({
379
- chatId: "ch1",
380
- threadId: "t1",
381
- text: "pong",
382
- replyToMessageId: "m1",
383
- });
384
- });
385
- });
386
-
387
- /* -------------------------------------------------------------------------- */
388
- /* Capabilities surfaced from meta */
389
- /* -------------------------------------------------------------------------- */
390
-
391
- describe("ChannelGateway capabilities", () => {
392
- it("surfaces channel capabilities from the adapter meta", () => {
393
- const reg = new ChannelRegistry();
394
- reg.register(
395
- "org_a",
396
- fakeAdapter("c1", { capabilities: { threads: false, reactions: true, streaming: false } }),
397
- );
398
- const gw = new ChannelGateway(reg);
399
- const caps = gw.capabilities("org_a", asChannelId("c1"));
400
- expect(caps).toEqual({ threads: false, reactions: true, streaming: false });
401
- });
402
-
403
- it("describe() returns the full meta for a registered channel", () => {
404
- const reg = new ChannelRegistry();
405
- reg.register("org_a", fakeAdapter("c1"));
406
- const gw = new ChannelGateway(reg);
407
- const m = gw.describe("org_a", asChannelId("c1"));
408
- expect(m.id).toBe("c1");
409
- expect(m.displayName).toBe("Channel c1");
410
- expect(m.capabilities).toEqual({ threads: true, reactions: false, streaming: true });
411
- });
412
-
413
- it("capabilities() throws UnknownChannelError for an unregistered channel", () => {
414
- const gw = new ChannelGateway(new ChannelRegistry());
415
- expect(() => gw.capabilities("org_a", asChannelId("ghost"))).toThrow(UnknownChannelError);
416
- });
417
-
418
- it("TenantMismatchError is exported and distinct from UnknownChannelError", () => {
419
- expect(new TenantMismatchError("x").name).toBe("TenantMismatchError");
420
- expect(new TenantMismatchError("x")).not.toBeInstanceOf(UnknownChannelError);
421
- });
422
-
423
- it("does not log to console during normal operation", () => {
424
- const spy = vi.spyOn(console, "log");
425
- const reg = new ChannelRegistry();
426
- reg.register("org_a", fakeAdapter("c1"));
427
- const gw = new ChannelGateway(reg);
428
- gw.ingest("org_a", asChannelId("c1"), { type: "message", chatId: "c", text: "hi" });
429
- expect(spy).not.toHaveBeenCalled();
430
- spy.mockRestore();
431
- });
432
- });