@dobby.ai/dobby 0.1.1 → 0.1.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 (136) hide show
  1. package/README.md +20 -7
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. package/tsconfig.json +0 -18
@@ -1,404 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { dirname, isAbsolute, resolve } from "node:path";
4
- import { z } from "zod";
5
- import { isDobbyRepoRoot } from "../shared/dobby-repo.js";
6
- import { BUILTIN_HOST_SANDBOX_ID } from "./types.js";
7
- import type {
8
- BindingConfig,
9
- BindingResolution,
10
- BindingSource,
11
- ConnectorsConfig,
12
- DefaultBindingConfig,
13
- ExtensionInstanceConfig,
14
- ExtensionsConfig,
15
- GatewayConfig,
16
- ProvidersConfig,
17
- RouteDefaultConfig,
18
- RouteProfile,
19
- RouteResolution,
20
- RoutesConfig,
21
- SandboxesConfig,
22
- } from "./types.js";
23
-
24
- const extensionItemSchema = z.object({
25
- type: z.string().trim().min(1),
26
- }).catchall(z.unknown());
27
-
28
- const routeDefaultSchema = z.object({
29
- projectRoot: z.string().trim().min(1).optional(),
30
- provider: z.string().trim().min(1).optional(),
31
- sandbox: z.string().trim().min(1).optional(),
32
- tools: z.enum(["full", "readonly"]).optional(),
33
- mentions: z.enum(["required", "optional"]).optional(),
34
- }).strict();
35
-
36
- const routeItemSchema = z.object({
37
- projectRoot: z.string().trim().min(1).optional(),
38
- tools: z.enum(["full", "readonly"]).optional(),
39
- mentions: z.enum(["required", "optional"]).optional(),
40
- provider: z.string().trim().min(1).optional(),
41
- sandbox: z.string().trim().min(1).optional(),
42
- systemPromptFile: z
43
- .string()
44
- .optional()
45
- .transform((value) => (value && value.trim().length > 0 ? value : undefined)),
46
- }).strict();
47
-
48
- const bindingSourceSchema = z.object({
49
- type: z.enum(["channel", "chat"]),
50
- id: z.string().trim().min(1),
51
- }).strict();
52
-
53
- const bindingItemSchema = z.object({
54
- connector: z.string().trim().min(1),
55
- source: bindingSourceSchema,
56
- route: z.string().trim().min(1),
57
- }).strict();
58
-
59
- const defaultBindingSchema = z.object({
60
- route: z.string().trim().min(1),
61
- }).strict();
62
-
63
- const gatewayConfigSchema = z.object({
64
- extensions: z.object({
65
- allowList: z
66
- .array(
67
- z.object({
68
- package: z.string().trim().min(1),
69
- enabled: z.boolean().default(true),
70
- }).strict(),
71
- )
72
- .default([]),
73
- }).strict(),
74
- providers: z.object({
75
- default: z.string().trim().min(1),
76
- items: z.record(z.string(), extensionItemSchema),
77
- }).strict(),
78
- connectors: z.object({
79
- items: z.record(z.string(), extensionItemSchema),
80
- }).strict(),
81
- sandboxes: z.object({
82
- default: z.string().trim().min(1).optional(),
83
- items: z.record(z.string(), extensionItemSchema).default({}),
84
- }).strict(),
85
- routes: z.object({
86
- default: routeDefaultSchema.default({}),
87
- items: z.record(z.string(), routeItemSchema),
88
- }).strict(),
89
- bindings: z.object({
90
- default: defaultBindingSchema.optional(),
91
- items: z.record(z.string(), bindingItemSchema).default({}),
92
- }).strict(),
93
- data: z.object({
94
- rootDir: z.string().default("./data"),
95
- dedupTtlMs: z.number().int().positive().default(7 * 24 * 60 * 60 * 1000),
96
- }).strict(),
97
- }).strict();
98
-
99
- type ParsedGatewayConfig = z.infer<typeof gatewayConfigSchema>;
100
- type ParsedRouteItem = z.infer<typeof routeItemSchema>;
101
- type ParsedExtensionItem = z.infer<typeof extensionItemSchema>;
102
-
103
- const FORBIDDEN_CONNECTOR_CONFIG_KEYS: Record<string, string> = {
104
- botChannelMap: "Use bindings.items to map connector sources to routes.",
105
- chatRouteMap: "Use bindings.items to map connector sources to routes.",
106
- botTokenEnv: "Set botToken directly in connector config or inject it before the config is loaded.",
107
- };
108
-
109
- function resolveConfigBaseDir(configPath: string): string {
110
- const absoluteConfigPath = resolve(configPath);
111
- const configDir = dirname(absoluteConfigPath);
112
- const repoRoot = dirname(configDir);
113
-
114
- if (absoluteConfigPath === resolve(repoRoot, "config", "gateway.json") && isDobbyRepoRoot(repoRoot)) {
115
- return repoRoot;
116
- }
117
-
118
- return configDir;
119
- }
120
-
121
- function resolveMaybeAbsolute(baseDir: string, value: string): string {
122
- const expanded = expandHome(value);
123
- return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
124
- }
125
-
126
- function expandHome(value: string): string {
127
- if (value === "~") {
128
- return homedir();
129
- }
130
-
131
- if (value.startsWith("~/") || value.startsWith("~\\")) {
132
- return resolve(homedir(), value.slice(2));
133
- }
134
-
135
- return value;
136
- }
137
-
138
- function normalizeInstanceItem(item: ParsedExtensionItem): ExtensionInstanceConfig {
139
- const { type, ...config } = item;
140
- return {
141
- type,
142
- config,
143
- };
144
- }
145
-
146
- function normalizeInstances(parsedItems: Record<string, ParsedExtensionItem>): Record<string, ExtensionInstanceConfig> {
147
- const normalized: Record<string, ExtensionInstanceConfig> = {};
148
- for (const [id, item] of Object.entries(parsedItems)) {
149
- normalized[id] = normalizeInstanceItem(item);
150
- }
151
- return normalized;
152
- }
153
-
154
- function normalizeExtensions(parsed: ParsedGatewayConfig["extensions"]): ExtensionsConfig {
155
- return {
156
- allowList: parsed.allowList.map((item) => ({
157
- package: item.package,
158
- enabled: item.enabled,
159
- })),
160
- };
161
- }
162
-
163
- function normalizeProviders(parsed: ParsedGatewayConfig["providers"]): ProvidersConfig {
164
- return {
165
- default: parsed.default,
166
- items: normalizeInstances(parsed.items),
167
- };
168
- }
169
-
170
- function normalizeConnectors(parsed: ParsedGatewayConfig["connectors"]): ConnectorsConfig {
171
- return {
172
- items: normalizeInstances(parsed.items),
173
- };
174
- }
175
-
176
- function normalizeSandboxes(parsed: ParsedGatewayConfig["sandboxes"]): SandboxesConfig {
177
- return {
178
- ...(parsed.default ? { default: parsed.default } : {}),
179
- items: normalizeInstances(parsed.items),
180
- };
181
- }
182
-
183
- function normalizeRouteProfile(
184
- routeId: string,
185
- baseDir: string,
186
- profile: ParsedRouteItem,
187
- defaults: RouteDefaultConfig,
188
- ): RouteProfile {
189
- const resolvedProjectRoot = profile.projectRoot ?? defaults.projectRoot;
190
- if (!resolvedProjectRoot) {
191
- throw new Error(`routes.items['${routeId}'].projectRoot is required when routes.default.projectRoot is not set`);
192
- }
193
-
194
- const normalized: RouteProfile = {
195
- projectRoot: resolveMaybeAbsolute(baseDir, resolvedProjectRoot),
196
- tools: profile.tools ?? defaults.tools,
197
- mentions: profile.mentions ?? defaults.mentions,
198
- provider: profile.provider ?? defaults.provider,
199
- sandbox: profile.sandbox ?? defaults.sandbox,
200
- };
201
-
202
- if (profile.systemPromptFile) {
203
- normalized.systemPromptFile = resolveMaybeAbsolute(baseDir, profile.systemPromptFile);
204
- }
205
-
206
- return normalized;
207
- }
208
-
209
- function normalizeRoutes(parsed: ParsedGatewayConfig["routes"], baseDir: string, defaults: RouteDefaultConfig): RoutesConfig {
210
- const items: Record<string, RouteProfile> = {};
211
- for (const [routeId, profile] of Object.entries(parsed.items)) {
212
- items[routeId] = normalizeRouteProfile(routeId, baseDir, profile, defaults);
213
- }
214
-
215
- return {
216
- default: defaults,
217
- items,
218
- };
219
- }
220
-
221
- function normalizeBindings(parsed: ParsedGatewayConfig["bindings"]): GatewayConfig["bindings"] {
222
- const items: Record<string, BindingConfig> = {};
223
- for (const [bindingId, binding] of Object.entries(parsed.items)) {
224
- items[bindingId] = {
225
- connector: binding.connector,
226
- source: {
227
- type: binding.source.type,
228
- id: binding.source.id,
229
- },
230
- route: binding.route,
231
- };
232
- }
233
-
234
- return {
235
- ...(parsed.default ? { default: { route: parsed.default.route } } : {}),
236
- items,
237
- };
238
- }
239
-
240
- function validateConnectorConfigKeys(parsed: ParsedGatewayConfig["connectors"]): void {
241
- for (const [instanceId, item] of Object.entries(parsed.items)) {
242
- for (const [key, message] of Object.entries(FORBIDDEN_CONNECTOR_CONFIG_KEYS)) {
243
- if (Object.prototype.hasOwnProperty.call(item, key)) {
244
- throw new Error(`connectors.items['${instanceId}'] must not include '${key}'. ${message}`);
245
- }
246
- }
247
- }
248
- }
249
-
250
- function validateReferences(parsed: ParsedGatewayConfig, normalizedRoutes: RoutesConfig): void {
251
- if (!parsed.providers.items[parsed.providers.default]) {
252
- throw new Error(`providers.default '${parsed.providers.default}' does not exist in providers.items`);
253
- }
254
-
255
- const defaultSandbox = parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID;
256
- if (defaultSandbox !== BUILTIN_HOST_SANDBOX_ID && !parsed.sandboxes.items[defaultSandbox]) {
257
- throw new Error(`sandboxes.default '${defaultSandbox}' does not exist in sandboxes.items`);
258
- }
259
-
260
- const resolvedDefaults: RouteDefaultConfig = {
261
- provider: parsed.routes.default.provider ?? parsed.providers.default,
262
- sandbox: parsed.routes.default.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
263
- tools: parsed.routes.default.tools ?? "full",
264
- mentions: parsed.routes.default.mentions ?? "required",
265
- };
266
-
267
- if (!parsed.providers.items[resolvedDefaults.provider]) {
268
- throw new Error(`routes.default.provider references unknown provider '${resolvedDefaults.provider}'`);
269
- }
270
- if (resolvedDefaults.sandbox !== BUILTIN_HOST_SANDBOX_ID && !parsed.sandboxes.items[resolvedDefaults.sandbox]) {
271
- throw new Error(`routes.default.sandbox references unknown sandbox '${resolvedDefaults.sandbox}'`);
272
- }
273
-
274
- for (const [routeId, profile] of Object.entries(normalizedRoutes.items)) {
275
- if (!parsed.providers.items[profile.provider]) {
276
- throw new Error(`routes.items['${routeId}'].provider references unknown provider '${profile.provider}'`);
277
- }
278
- if (profile.sandbox !== BUILTIN_HOST_SANDBOX_ID && !parsed.sandboxes.items[profile.sandbox]) {
279
- throw new Error(`routes.items['${routeId}'].sandbox references unknown sandbox '${profile.sandbox}'`);
280
- }
281
- }
282
-
283
- const seenSources = new Map<string, string>();
284
- for (const [bindingId, binding] of Object.entries(parsed.bindings.items)) {
285
- if (!parsed.connectors.items[binding.connector]) {
286
- throw new Error(`bindings.items['${bindingId}'].connector references unknown connector '${binding.connector}'`);
287
- }
288
- if (!normalizedRoutes.items[binding.route]) {
289
- throw new Error(`bindings.items['${bindingId}'].route references unknown route '${binding.route}'`);
290
- }
291
-
292
- const bindingKey = `${binding.connector}:${binding.source.type}:${binding.source.id}`;
293
- const existingBindingId = seenSources.get(bindingKey);
294
- if (existingBindingId) {
295
- throw new Error(
296
- `bindings.items['${bindingId}'] duplicates source '${bindingKey}' already used by bindings.items['${existingBindingId}']`,
297
- );
298
- }
299
- seenSources.set(bindingKey, bindingId);
300
- }
301
-
302
- if (parsed.bindings.default && !normalizedRoutes.items[parsed.bindings.default.route]) {
303
- throw new Error(`bindings.default.route references unknown route '${parsed.bindings.default.route}'`);
304
- }
305
- }
306
-
307
- export async function loadGatewayConfig(configPath: string): Promise<GatewayConfig> {
308
- const absoluteConfigPath = resolve(configPath);
309
- const configBaseDir = resolveConfigBaseDir(absoluteConfigPath);
310
- const raw = await readFile(absoluteConfigPath, "utf-8");
311
- const parsed = gatewayConfigSchema.parse(JSON.parse(raw) as unknown);
312
- validateConnectorConfigKeys(parsed.connectors);
313
-
314
- const routeDefaults: RouteDefaultConfig = {
315
- ...(parsed.routes.default.projectRoot ? { projectRoot: resolveMaybeAbsolute(configBaseDir, parsed.routes.default.projectRoot) } : {}),
316
- provider: parsed.routes.default.provider ?? parsed.providers.default,
317
- sandbox: parsed.routes.default.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
318
- tools: parsed.routes.default.tools ?? "full",
319
- mentions: parsed.routes.default.mentions ?? "required",
320
- };
321
-
322
- const normalizedRoutes = normalizeRoutes(parsed.routes, configBaseDir, routeDefaults);
323
- validateReferences(parsed, normalizedRoutes);
324
-
325
- const rootDir = resolveMaybeAbsolute(configBaseDir, parsed.data.rootDir);
326
-
327
- return {
328
- extensions: normalizeExtensions(parsed.extensions),
329
- providers: normalizeProviders(parsed.providers),
330
- connectors: normalizeConnectors(parsed.connectors),
331
- sandboxes: normalizeSandboxes(parsed.sandboxes),
332
- routes: normalizedRoutes,
333
- bindings: normalizeBindings(parsed.bindings),
334
- data: {
335
- rootDir,
336
- sessionsDir: resolve(rootDir, "sessions"),
337
- attachmentsDir: resolve(rootDir, "attachments"),
338
- logsDir: resolve(rootDir, "logs"),
339
- stateDir: resolve(rootDir, "state"),
340
- dedupTtlMs: parsed.data.dedupTtlMs,
341
- },
342
- };
343
- }
344
-
345
- export class RouteResolver {
346
- constructor(private readonly routes: RoutesConfig) { }
347
-
348
- resolve(routeId: string): RouteResolution | null {
349
- const normalizedRouteId = routeId.trim();
350
- if (!normalizedRouteId) return null;
351
-
352
- const profile = this.routes.items[normalizedRouteId];
353
- if (!profile) return null;
354
-
355
- return { routeId: normalizedRouteId, profile };
356
- }
357
- }
358
-
359
- export class BindingResolver {
360
- private readonly bindingsBySource = new Map<string, BindingResolution>();
361
- private readonly defaultBinding: BindingResolution | null;
362
-
363
- constructor(bindings: GatewayConfig["bindings"]) {
364
- for (const [bindingId, binding] of Object.entries(bindings.items)) {
365
- this.bindingsBySource.set(this.buildKey(binding.connector, binding.source), {
366
- bindingId,
367
- config: binding,
368
- });
369
- }
370
-
371
- this.defaultBinding = bindings.default
372
- ? {
373
- bindingId: "__default__",
374
- config: {
375
- connector: "__default__",
376
- source: {
377
- type: "chat",
378
- id: "__direct_message__",
379
- },
380
- route: bindings.default.route,
381
- },
382
- }
383
- : null;
384
- }
385
-
386
- resolve(
387
- connectorId: string,
388
- source: BindingSource,
389
- options?: {
390
- isDirectMessage?: boolean;
391
- },
392
- ): BindingResolution | null {
393
- if (!connectorId.trim() || !source.id.trim()) {
394
- return options?.isDirectMessage ? this.defaultBinding : null;
395
- }
396
-
397
- return this.bindingsBySource.get(this.buildKey(connectorId, source))
398
- ?? (options?.isDirectMessage ? this.defaultBinding : null);
399
- }
400
-
401
- private buildKey(connectorId: string, source: BindingSource): string {
402
- return `${connectorId.trim()}:${source.type}:${source.id.trim()}`;
403
- }
404
- }
@@ -1,141 +0,0 @@
1
- import type { ConversationRuntime, GatewayLogger } from "./types.js";
2
-
3
- interface RuntimeEntry {
4
- runtime: ConversationRuntime | undefined;
5
- tail: Promise<void>;
6
- epoch: number;
7
- scheduledTasks: number;
8
- }
9
-
10
- export class RuntimeRegistry {
11
- private readonly entries = new Map<string, RuntimeEntry>();
12
-
13
- constructor(private readonly logger: GatewayLogger) {}
14
-
15
- async run(
16
- key: string,
17
- createFn: () => Promise<ConversationRuntime>,
18
- task: (runtime: ConversationRuntime) => Promise<void>,
19
- ): Promise<void> {
20
- const entry = this.getOrCreateEntry(key);
21
- const scheduledEpoch = entry.epoch;
22
- entry.scheduledTasks += 1;
23
- const run = entry.tail.then(async () => {
24
- if (scheduledEpoch !== entry.epoch) return;
25
-
26
- let runtime = entry.runtime;
27
- if (!runtime) {
28
- const created = await createFn();
29
- if (scheduledEpoch !== entry.epoch) {
30
- await this.closeRuntime(key, created, "Discarding runtime created for stale queued task");
31
- return;
32
- }
33
-
34
- entry.runtime = created;
35
- runtime = created;
36
- }
37
-
38
- await task(runtime);
39
- });
40
-
41
- const managedRun = run.finally(() => {
42
- entry.scheduledTasks = Math.max(0, entry.scheduledTasks - 1);
43
- });
44
-
45
- this.attachTail(key, entry, managedRun, "Queued task failed");
46
- await managedRun;
47
- }
48
-
49
- async abort(key: string): Promise<boolean> {
50
- const entry = this.entries.get(key);
51
- if (!entry?.runtime) return false;
52
- return this.abortRuntime(key, entry.runtime, "Failed to abort runtime");
53
- }
54
-
55
- async cancel(key: string): Promise<boolean> {
56
- const entry = this.entries.get(key);
57
- if (!entry) return false;
58
- if (entry.scheduledTasks === 0) return false;
59
-
60
- entry.epoch += 1;
61
- if (!entry.runtime) return true;
62
- return this.abortRuntime(key, entry.runtime, "Failed to cancel runtime");
63
- }
64
-
65
- async reset(key: string): Promise<boolean> {
66
- const entry = this.entries.get(key);
67
- if (!entry) return false;
68
-
69
- entry.epoch += 1;
70
- if (entry.runtime) {
71
- await this.abortRuntime(key, entry.runtime, "Failed to abort runtime during reset");
72
- }
73
-
74
- const close = entry.tail.then(async () => {
75
- const runtime = entry.runtime;
76
- entry.runtime = undefined;
77
- if (!runtime) return;
78
- await this.closeRuntime(key, runtime, "Failed to close runtime during reset");
79
- });
80
-
81
- this.attachTail(key, entry, close);
82
- await close;
83
- return true;
84
- }
85
-
86
- async closeAll(): Promise<void> {
87
- const keys = [...this.entries.keys()];
88
- await Promise.all(keys.map((key) => this.reset(key)));
89
- this.entries.clear();
90
- }
91
-
92
- private getOrCreateEntry(key: string): RuntimeEntry {
93
- const existing = this.entries.get(key);
94
- if (existing) return existing;
95
-
96
- const entry: RuntimeEntry = {
97
- runtime: undefined,
98
- tail: Promise.resolve(),
99
- epoch: 0,
100
- scheduledTasks: 0,
101
- };
102
- this.entries.set(key, entry);
103
- return entry;
104
- }
105
-
106
- private attachTail(
107
- key: string,
108
- entry: RuntimeEntry,
109
- run: Promise<void>,
110
- errorMessage = "Queued task failed",
111
- ): void {
112
- const nextTail = run.catch((error) => {
113
- this.logger.error({ err: error, conversationKey: key }, errorMessage);
114
- });
115
- entry.tail = nextTail;
116
- void nextTail.finally(() => {
117
- if (this.entries.get(key) !== entry) return;
118
- if (entry.runtime !== undefined) return;
119
- if (entry.tail !== nextTail) return;
120
- this.entries.delete(key);
121
- });
122
- }
123
-
124
- private async abortRuntime(key: string, runtime: ConversationRuntime, errorMessage: string): Promise<boolean> {
125
- try {
126
- await runtime.runtime.abort();
127
- return true;
128
- } catch (error) {
129
- this.logger.error({ err: error, conversationKey: key }, errorMessage);
130
- return false;
131
- }
132
- }
133
-
134
- private async closeRuntime(key: string, runtime: ConversationRuntime, errorMessage: string): Promise<void> {
135
- try {
136
- await runtime.close();
137
- } catch (error) {
138
- this.logger.error({ err: error, conversationKey: key }, errorMessage);
139
- }
140
- }
141
- }
@@ -1,20 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { parseControlCommand } from "../control-command.js";
4
-
5
- test("parseControlCommand recognizes cancel aliases", () => {
6
- assert.equal(parseControlCommand("stop"), "cancel");
7
- assert.equal(parseControlCommand(" /STOP "), "cancel");
8
- assert.equal(parseControlCommand("/cancel"), "cancel");
9
- });
10
-
11
- test("parseControlCommand recognizes new session aliases", () => {
12
- assert.equal(parseControlCommand("/new"), "new_session");
13
- assert.equal(parseControlCommand(" /reset "), "new_session");
14
- });
15
-
16
- test("parseControlCommand ignores regular messages", () => {
17
- assert.equal(parseControlCommand("please /new"), null);
18
- assert.equal(parseControlCommand(""), null);
19
- assert.equal(parseControlCommand("hello"), null);
20
- });
@@ -1,140 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { RuntimeRegistry } from "../runtime-registry.js";
4
- import type { ConversationRuntime, GatewayLogger } from "../types.js";
5
-
6
- function deferred<T = void>(): { promise: Promise<T>; resolve: (value: T) => void } {
7
- let resolve!: (value: T) => void;
8
- const promise = new Promise<T>((res) => {
9
- resolve = res;
10
- });
11
- return { promise, resolve };
12
- }
13
-
14
- function createLogger(): GatewayLogger {
15
- return {
16
- error() {},
17
- warn() {},
18
- info() {},
19
- debug() {},
20
- } as unknown as GatewayLogger;
21
- }
22
-
23
- function createConversationRuntime(id: string, abortCalls: string[], closeCalls: string[]): ConversationRuntime {
24
- return {
25
- key: id,
26
- routeId: "route.main",
27
- route: {
28
- projectRoot: "/tmp/project",
29
- tools: "full",
30
- mentions: "required",
31
- provider: "provider.main",
32
- sandbox: "host.builtin",
33
- },
34
- providerId: "provider.main",
35
- sandboxId: "host.builtin",
36
- runtime: {
37
- async prompt() {},
38
- subscribe() {
39
- return () => {};
40
- },
41
- async abort() {
42
- abortCalls.push(id);
43
- },
44
- dispose() {},
45
- },
46
- async close() {
47
- closeCalls.push(id);
48
- },
49
- };
50
- }
51
-
52
- test("cancel aborts the active run and drops queued turns", async () => {
53
- const registry = new RuntimeRegistry(createLogger());
54
- const abortCalls: string[] = [];
55
- const closeCalls: string[] = [];
56
- let runtimeCount = 0;
57
- const createRuntime = async (): Promise<ConversationRuntime> => {
58
- runtimeCount += 1;
59
- return createConversationRuntime(`runtime-${runtimeCount}`, abortCalls, closeCalls);
60
- };
61
-
62
- const firstStarted = deferred();
63
- const releaseFirst = deferred();
64
- const started: string[] = [];
65
-
66
- const firstTurn = registry.run("conversation", createRuntime, async (runtime) => {
67
- started.push(runtime.key);
68
- firstStarted.resolve(undefined);
69
- await releaseFirst.promise;
70
- });
71
-
72
- await firstStarted.promise;
73
-
74
- const secondTurn = registry.run("conversation", createRuntime, async (runtime) => {
75
- started.push(runtime.key);
76
- });
77
- const thirdTurn = registry.run("conversation", createRuntime, async (runtime) => {
78
- started.push(runtime.key);
79
- });
80
-
81
- assert.equal(await registry.cancel("conversation"), true);
82
- assert.deepEqual(abortCalls, ["runtime-1"]);
83
-
84
- releaseFirst.resolve(undefined);
85
- await Promise.all([firstTurn, secondTurn, thirdTurn]);
86
-
87
- assert.deepEqual(started, ["runtime-1"]);
88
- assert.deepEqual(closeCalls, []);
89
-
90
- await registry.run("conversation", createRuntime, async (runtime) => {
91
- started.push(runtime.key);
92
- });
93
-
94
- assert.deepEqual(started, ["runtime-1", "runtime-1"]);
95
- assert.equal(runtimeCount, 1);
96
- });
97
-
98
- test("reset closes the current runtime and recreates it on the next turn", async () => {
99
- const registry = new RuntimeRegistry(createLogger());
100
- const abortCalls: string[] = [];
101
- const closeCalls: string[] = [];
102
- let runtimeCount = 0;
103
- const createRuntime = async (): Promise<ConversationRuntime> => {
104
- runtimeCount += 1;
105
- return createConversationRuntime(`runtime-${runtimeCount}`, abortCalls, closeCalls);
106
- };
107
-
108
- const firstStarted = deferred();
109
- const releaseFirst = deferred();
110
- const started: string[] = [];
111
-
112
- const firstTurn = registry.run("conversation", createRuntime, async (runtime) => {
113
- started.push(runtime.key);
114
- firstStarted.resolve(undefined);
115
- await releaseFirst.promise;
116
- });
117
-
118
- await firstStarted.promise;
119
-
120
- const queuedTurn = registry.run("conversation", createRuntime, async (runtime) => {
121
- started.push(runtime.key);
122
- });
123
-
124
- const resetPromise = registry.reset("conversation");
125
- releaseFirst.resolve(undefined);
126
-
127
- assert.equal(await resetPromise, true);
128
- await Promise.all([firstTurn, queuedTurn]);
129
-
130
- assert.deepEqual(abortCalls, ["runtime-1"]);
131
- assert.deepEqual(closeCalls, ["runtime-1"]);
132
- assert.deepEqual(started, ["runtime-1"]);
133
-
134
- await registry.run("conversation", createRuntime, async (runtime) => {
135
- started.push(runtime.key);
136
- });
137
-
138
- assert.deepEqual(started, ["runtime-1", "runtime-2"]);
139
- assert.equal(runtimeCount, 2);
140
- });