@lakitu/sdk 0.1.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 (111) hide show
  1. package/README.md +166 -0
  2. package/convex/_generated/api.d.ts +45 -0
  3. package/convex/_generated/api.js +23 -0
  4. package/convex/_generated/dataModel.d.ts +58 -0
  5. package/convex/_generated/server.d.ts +143 -0
  6. package/convex/_generated/server.js +93 -0
  7. package/convex/cloud/CLAUDE.md +238 -0
  8. package/convex/cloud/_generated/api.ts +84 -0
  9. package/convex/cloud/_generated/component.ts +861 -0
  10. package/convex/cloud/_generated/dataModel.ts +60 -0
  11. package/convex/cloud/_generated/server.ts +156 -0
  12. package/convex/cloud/convex.config.ts +16 -0
  13. package/convex/cloud/index.ts +29 -0
  14. package/convex/cloud/intentSchema/generate.ts +447 -0
  15. package/convex/cloud/intentSchema/index.ts +16 -0
  16. package/convex/cloud/intentSchema/types.ts +418 -0
  17. package/convex/cloud/ksaPolicy.ts +554 -0
  18. package/convex/cloud/mail.ts +92 -0
  19. package/convex/cloud/schema.ts +322 -0
  20. package/convex/cloud/utils/kanbanContext.ts +229 -0
  21. package/convex/cloud/workflows/agentBoard.ts +451 -0
  22. package/convex/cloud/workflows/agentPrompt.ts +272 -0
  23. package/convex/cloud/workflows/agentThread.ts +374 -0
  24. package/convex/cloud/workflows/compileSandbox.ts +146 -0
  25. package/convex/cloud/workflows/crudBoard.ts +217 -0
  26. package/convex/cloud/workflows/crudKSAs.ts +262 -0
  27. package/convex/cloud/workflows/crudLorobeads.ts +371 -0
  28. package/convex/cloud/workflows/crudSkills.ts +205 -0
  29. package/convex/cloud/workflows/crudThreads.ts +708 -0
  30. package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
  31. package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
  32. package/convex/sandbox/README.md +90 -0
  33. package/convex/sandbox/_generated/api.d.ts +2934 -0
  34. package/convex/sandbox/_generated/api.js +23 -0
  35. package/convex/sandbox/_generated/dataModel.d.ts +60 -0
  36. package/convex/sandbox/_generated/server.d.ts +143 -0
  37. package/convex/sandbox/_generated/server.js +93 -0
  38. package/convex/sandbox/actions/bash.ts +130 -0
  39. package/convex/sandbox/actions/browser.ts +282 -0
  40. package/convex/sandbox/actions/file.ts +336 -0
  41. package/convex/sandbox/actions/lsp.ts +325 -0
  42. package/convex/sandbox/actions/pdf.ts +119 -0
  43. package/convex/sandbox/agent/codeExecLoop.ts +535 -0
  44. package/convex/sandbox/agent/decisions.ts +284 -0
  45. package/convex/sandbox/agent/index.ts +515 -0
  46. package/convex/sandbox/agent/subagents.ts +651 -0
  47. package/convex/sandbox/brandResearch/index.ts +417 -0
  48. package/convex/sandbox/context/index.ts +7 -0
  49. package/convex/sandbox/context/session.ts +402 -0
  50. package/convex/sandbox/convex.config.ts +17 -0
  51. package/convex/sandbox/index.ts +51 -0
  52. package/convex/sandbox/nodeActions/codeExec.ts +130 -0
  53. package/convex/sandbox/planning/beads.ts +187 -0
  54. package/convex/sandbox/planning/index.ts +8 -0
  55. package/convex/sandbox/planning/sync.ts +194 -0
  56. package/convex/sandbox/prompts/codeExec.ts +852 -0
  57. package/convex/sandbox/prompts/modes.ts +231 -0
  58. package/convex/sandbox/prompts/system.ts +142 -0
  59. package/convex/sandbox/schema.ts +510 -0
  60. package/convex/sandbox/state/artifacts.ts +99 -0
  61. package/convex/sandbox/state/checkpoints.ts +341 -0
  62. package/convex/sandbox/state/files.ts +383 -0
  63. package/convex/sandbox/state/index.ts +10 -0
  64. package/convex/sandbox/state/verification.actions.ts +268 -0
  65. package/convex/sandbox/state/verification.ts +101 -0
  66. package/convex/sandbox/tsconfig.json +25 -0
  67. package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
  68. package/dist/cli/commands/build.d.ts +19 -0
  69. package/dist/cli/commands/build.d.ts.map +1 -0
  70. package/dist/cli/commands/build.js +223 -0
  71. package/dist/cli/commands/init.d.ts +16 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -0
  73. package/dist/cli/commands/init.js +148 -0
  74. package/dist/cli/commands/publish.d.ts +12 -0
  75. package/dist/cli/commands/publish.d.ts.map +1 -0
  76. package/dist/cli/commands/publish.js +33 -0
  77. package/dist/cli/index.d.ts +14 -0
  78. package/dist/cli/index.d.ts.map +1 -0
  79. package/dist/cli/index.js +40 -0
  80. package/dist/sdk/builders.d.ts +104 -0
  81. package/dist/sdk/builders.d.ts.map +1 -0
  82. package/dist/sdk/builders.js +214 -0
  83. package/dist/sdk/index.d.ts +29 -0
  84. package/dist/sdk/index.d.ts.map +1 -0
  85. package/dist/sdk/index.js +38 -0
  86. package/dist/sdk/types.d.ts +107 -0
  87. package/dist/sdk/types.d.ts.map +1 -0
  88. package/dist/sdk/types.js +6 -0
  89. package/ksa/README.md +263 -0
  90. package/ksa/_generated/REFERENCE.md +2954 -0
  91. package/ksa/_generated/registry.ts +257 -0
  92. package/ksa/_shared/configReader.ts +302 -0
  93. package/ksa/_shared/configSchemas.ts +649 -0
  94. package/ksa/_shared/gateway.ts +175 -0
  95. package/ksa/_shared/ksaBehaviors.ts +411 -0
  96. package/ksa/_shared/ksaProxy.ts +248 -0
  97. package/ksa/_shared/localDb.ts +302 -0
  98. package/ksa/index.ts +134 -0
  99. package/package.json +93 -0
  100. package/runtime/browser/agent-browser.ts +330 -0
  101. package/runtime/entrypoint.ts +194 -0
  102. package/runtime/lsp/manager.ts +366 -0
  103. package/runtime/pdf/pdf-generator.ts +50 -0
  104. package/runtime/pdf/renderer.ts +357 -0
  105. package/runtime/pdf/schema.ts +97 -0
  106. package/runtime/services/file-watcher.ts +191 -0
  107. package/template/build.ts +307 -0
  108. package/template/e2b/Dockerfile +69 -0
  109. package/template/e2b/e2b.toml +13 -0
  110. package/template/e2b/prebuild.sh +68 -0
  111. package/template/e2b/start.sh +14 -0
@@ -0,0 +1,554 @@
1
+ /**
2
+ * KSA Policy Module
3
+ *
4
+ * Provides KSA (Knowledge, Skills, Abilities) registry and policy enforcement
5
+ * for the gateway and frontend.
6
+ *
7
+ * The KSA_REGISTRY is auto-generated from packages/lakitu/ksa/*.ts
8
+ * Run `bun generate:ksa` to regenerate.
9
+ *
10
+ * This file contains:
11
+ * - Re-exports of generated types and functions
12
+ * - Config schemas for UI configuration panels
13
+ * - Category labels and descriptions
14
+ * - Default KSA sets for different purposes
15
+ */
16
+
17
+ // ============================================================================
18
+ // Re-exports from Lakitu KSA Registry
19
+ // ============================================================================
20
+
21
+ // Types
22
+ export type {
23
+ KSACategory,
24
+ KSAGroup,
25
+ KSAInfo,
26
+ } from "../../ksa/_generated/registry";
27
+
28
+ // Registry and discovery functions
29
+ export {
30
+ KSA_REGISTRY,
31
+ CORE_KSAS,
32
+ getAllKSAs,
33
+ getKSA,
34
+ getKSAsByCategory,
35
+ getKSAsByNames,
36
+ searchKSAs,
37
+ } from "../../ksa/_generated/registry";
38
+
39
+ // Policy functions
40
+ export {
41
+ getServicePathsForKSAs,
42
+ isServicePathAllowed,
43
+ } from "../../ksa/_generated/registry";
44
+
45
+ /**
46
+ * Validate KSA names against the registry.
47
+ * Returns valid and invalid KSA names.
48
+ */
49
+ export function validateKSAs(ksaNames: string[]): { valid: string[]; invalid: string[] } {
50
+ const validNames = KSA_REGISTRY.map(k => k.name);
51
+ const valid: string[] = [];
52
+ const invalid: string[] = [];
53
+
54
+ for (const name of ksaNames) {
55
+ if (validNames.includes(name)) {
56
+ valid.push(name);
57
+ } else {
58
+ invalid.push(name);
59
+ }
60
+ }
61
+
62
+ return { valid, invalid };
63
+ }
64
+
65
+ // ============================================================================
66
+ // Config Field Types
67
+ // ============================================================================
68
+
69
+ export type ConfigFieldType =
70
+ | "select"
71
+ | "multiselect"
72
+ | "boolean"
73
+ | "number"
74
+ | "string"
75
+ | "textarea"
76
+ | "array";
77
+
78
+ export interface ConfigFieldOption {
79
+ value: string | number | boolean;
80
+ label: string;
81
+ }
82
+
83
+ export interface ConfigField {
84
+ type: ConfigFieldType;
85
+ label: string;
86
+ description?: string;
87
+ placeholder?: string;
88
+ options?: ConfigFieldOption[];
89
+ min?: number;
90
+ max?: number;
91
+ default: unknown;
92
+ }
93
+
94
+ // ============================================================================
95
+ // Universal Fields
96
+ // ============================================================================
97
+
98
+ const INSTRUCTIONS_FIELD: ConfigField = {
99
+ type: "textarea",
100
+ label: "Custom Instructions",
101
+ description:
102
+ "Instructions for the agent when using this skill (embedded in system prompt)",
103
+ placeholder:
104
+ 'e.g., "Focus on enterprise clients" or "Prioritize recent sources"',
105
+ default: "",
106
+ };
107
+
108
+ // ============================================================================
109
+ // Config Schemas
110
+ // ============================================================================
111
+
112
+ export const CONFIG_SCHEMAS: Record<string, Record<string, ConfigField>> = {
113
+ web: {
114
+ depth: {
115
+ type: "select",
116
+ label: "Research Depth",
117
+ description: "Number of results to fetch",
118
+ options: [
119
+ { value: "quick", label: "Quick (8 results)" },
120
+ { value: "thorough", label: "Thorough (15+ results)" },
121
+ ],
122
+ default: "quick",
123
+ },
124
+ searchType: {
125
+ type: "select",
126
+ label: "Search Type",
127
+ description: "Type of content to search",
128
+ options: [
129
+ { value: "all", label: "All Sources" },
130
+ { value: "web", label: "Web Only" },
131
+ { value: "news", label: "News Only" },
132
+ ],
133
+ default: "all",
134
+ },
135
+ fastMode: {
136
+ type: "boolean",
137
+ label: "Fast Mode",
138
+ description: "Prioritize speed over depth",
139
+ default: true,
140
+ },
141
+ includeSources: {
142
+ type: "array",
143
+ label: "Include Sources",
144
+ description: "Only search these domains (leave empty for all)",
145
+ placeholder: "e.g., techcrunch.com, reuters.com",
146
+ default: [],
147
+ },
148
+ excludeSources: {
149
+ type: "array",
150
+ label: "Exclude Sources",
151
+ description: "Never search these domains",
152
+ placeholder: "e.g., pinterest.com, reddit.com",
153
+ default: [],
154
+ },
155
+ instructions: INSTRUCTIONS_FIELD,
156
+ },
157
+ social: {
158
+ platforms: {
159
+ type: "multiselect",
160
+ label: "Platforms",
161
+ description: "Social platforms to search",
162
+ options: [
163
+ { value: "instagram", label: "Instagram" },
164
+ { value: "tiktok", label: "TikTok" },
165
+ { value: "youtube", label: "YouTube" },
166
+ { value: "twitter", label: "Twitter/X" },
167
+ { value: "linkedin", label: "LinkedIn" },
168
+ ],
169
+ default: ["instagram", "tiktok"],
170
+ },
171
+ contentTypes: {
172
+ type: "multiselect",
173
+ label: "Content Types",
174
+ description: "Types of content to fetch",
175
+ options: [
176
+ { value: "profiles", label: "Profiles" },
177
+ { value: "posts", label: "Posts" },
178
+ { value: "comments", label: "Comments" },
179
+ ],
180
+ default: ["profiles", "posts"],
181
+ },
182
+ postsLimit: {
183
+ type: "number",
184
+ label: "Posts Limit",
185
+ description: "Maximum posts per profile",
186
+ min: 1,
187
+ max: 50,
188
+ default: 10,
189
+ },
190
+ instructions: INSTRUCTIONS_FIELD,
191
+ },
192
+ companies: {
193
+ enrichmentLevel: {
194
+ type: "select",
195
+ label: "Enrichment Level",
196
+ description: "Depth of company data",
197
+ options: [
198
+ { value: "basic", label: "Basic (name, domain, logo)" },
199
+ { value: "detailed", label: "Detailed (+ description, industry)" },
200
+ { value: "full", label: "Full (+ employees, funding, tech)" },
201
+ ],
202
+ default: "basic",
203
+ },
204
+ includeTechStack: {
205
+ type: "boolean",
206
+ label: "Include Tech Stack",
207
+ description: "Fetch technology stack information",
208
+ default: false,
209
+ },
210
+ sources: {
211
+ type: "multiselect",
212
+ label: "Data Sources",
213
+ description: "Sources to query",
214
+ options: [
215
+ { value: "domain", label: "Domain Lookup" },
216
+ { value: "linkedin", label: "LinkedIn" },
217
+ { value: "crunchbase", label: "Crunchbase" },
218
+ ],
219
+ default: ["domain"],
220
+ },
221
+ instructions: INSTRUCTIONS_FIELD,
222
+ },
223
+ artifacts: {
224
+ validationRequired: {
225
+ type: "boolean",
226
+ label: "Require Artifact",
227
+ description: "Stage must produce at least one artifact",
228
+ default: false,
229
+ },
230
+ validationMinLength: {
231
+ type: "number",
232
+ label: "Minimum Length",
233
+ description: "Minimum content length (0 = no limit)",
234
+ min: 0,
235
+ max: 10000,
236
+ default: 0,
237
+ },
238
+ validationFormat: {
239
+ type: "select",
240
+ label: "Required Format",
241
+ description: "Artifact format requirement",
242
+ options: [
243
+ { value: "any", label: "Any Format" },
244
+ { value: "markdown", label: "Markdown" },
245
+ { value: "json", label: "JSON" },
246
+ { value: "html", label: "HTML" },
247
+ ],
248
+ default: "any",
249
+ },
250
+ autoSave: {
251
+ type: "boolean",
252
+ label: "Auto-Save",
253
+ description: "Automatically save artifacts on completion",
254
+ default: true,
255
+ },
256
+ instructions: INSTRUCTIONS_FIELD,
257
+ },
258
+ pdf: {
259
+ template: {
260
+ type: "select",
261
+ label: "Template",
262
+ description: "PDF template style",
263
+ options: [
264
+ { value: "report", label: "Report" },
265
+ { value: "presentation", label: "Presentation" },
266
+ { value: "minimal", label: "Minimal" },
267
+ ],
268
+ default: "report",
269
+ },
270
+ pageSize: {
271
+ type: "select",
272
+ label: "Page Size",
273
+ description: "Document page size",
274
+ options: [
275
+ { value: "letter", label: "Letter (8.5 x 11)" },
276
+ { value: "a4", label: "A4" },
277
+ ],
278
+ default: "letter",
279
+ },
280
+ includeTableOfContents: {
281
+ type: "boolean",
282
+ label: "Table of Contents",
283
+ description: "Include table of contents",
284
+ default: true,
285
+ },
286
+ instructions: INSTRUCTIONS_FIELD,
287
+ },
288
+ email: {
289
+ fromName: {
290
+ type: "string",
291
+ label: "From Name",
292
+ description: "Sender display name",
293
+ placeholder: "e.g., Marketing Team",
294
+ default: "Agent",
295
+ },
296
+ replyTo: {
297
+ type: "string",
298
+ label: "Reply-To",
299
+ description: "Reply-to email address",
300
+ placeholder: "e.g., replies@company.com",
301
+ default: "",
302
+ },
303
+ sandboxMode: {
304
+ type: "boolean",
305
+ label: "Sandbox Mode",
306
+ description: "Test mode - emails are logged but not sent",
307
+ default: true,
308
+ },
309
+ defaultTemplateId: {
310
+ type: "string",
311
+ label: "Default Template ID",
312
+ description: "SendGrid template ID",
313
+ placeholder: "e.g., d-abc123...",
314
+ default: "",
315
+ },
316
+ instructions: INSTRUCTIONS_FIELD,
317
+ },
318
+ news: {
319
+ sources: {
320
+ type: "multiselect",
321
+ label: "News Sources",
322
+ description: "Preferred news sources",
323
+ options: [
324
+ { value: "general", label: "General News" },
325
+ { value: "tech", label: "Tech News" },
326
+ { value: "business", label: "Business News" },
327
+ { value: "finance", label: "Finance News" },
328
+ ],
329
+ default: ["general"],
330
+ },
331
+ recency: {
332
+ type: "select",
333
+ label: "Recency",
334
+ description: "How recent should articles be",
335
+ options: [
336
+ { value: "day", label: "Last 24 hours" },
337
+ { value: "week", label: "Last week" },
338
+ { value: "month", label: "Last month" },
339
+ { value: "any", label: "Any time" },
340
+ ],
341
+ default: "week",
342
+ },
343
+ instructions: INSTRUCTIONS_FIELD,
344
+ },
345
+ browser: {
346
+ headless: {
347
+ type: "boolean",
348
+ label: "Headless Mode",
349
+ description: "Run browser without UI",
350
+ default: true,
351
+ },
352
+ timeout: {
353
+ type: "number",
354
+ label: "Page Timeout",
355
+ description: "Max seconds to wait for page load",
356
+ min: 5,
357
+ max: 120,
358
+ default: 30,
359
+ },
360
+ instructions: INSTRUCTIONS_FIELD,
361
+ },
362
+ };
363
+
364
+ export const CONFIG_DEFAULTS: Record<string, Record<string, unknown>> = {
365
+ web: {
366
+ depth: "quick",
367
+ searchType: "all",
368
+ fastMode: true,
369
+ includeSources: [],
370
+ excludeSources: [],
371
+ instructions: "",
372
+ },
373
+ social: {
374
+ platforms: ["instagram", "tiktok"],
375
+ contentTypes: ["profiles", "posts"],
376
+ postsLimit: 10,
377
+ instructions: "",
378
+ },
379
+ companies: {
380
+ enrichmentLevel: "basic",
381
+ includeTechStack: false,
382
+ sources: ["domain"],
383
+ instructions: "",
384
+ },
385
+ artifacts: {
386
+ validationRequired: false,
387
+ validationMinLength: 0,
388
+ validationFormat: "any",
389
+ autoSave: true,
390
+ instructions: "",
391
+ },
392
+ pdf: {
393
+ template: "report",
394
+ pageSize: "letter",
395
+ includeTableOfContents: true,
396
+ instructions: "",
397
+ },
398
+ email: {
399
+ fromName: "Agent",
400
+ replyTo: "",
401
+ sandboxMode: true,
402
+ defaultTemplateId: "",
403
+ instructions: "",
404
+ },
405
+ news: {
406
+ sources: ["general"],
407
+ recency: "week",
408
+ instructions: "",
409
+ },
410
+ browser: {
411
+ headless: true,
412
+ timeout: 30,
413
+ instructions: "",
414
+ },
415
+ };
416
+
417
+ // ============================================================================
418
+ // Category Labels
419
+ // ============================================================================
420
+
421
+ import type { KSACategory, KSAGroup } from "../../ksa/_generated/registry";
422
+
423
+ export const CATEGORY_LABELS: Record<KSACategory, string> = {
424
+ core: "Core (Always Available)",
425
+ skills: "Skills (Research & Data)",
426
+ deliverables: "Deliverables (Output Formats)",
427
+ };
428
+
429
+ export const CATEGORY_DESCRIPTIONS: Record<KSACategory, string> = {
430
+ core: "Fundamental operations available to every agent",
431
+ skills: "Research and data gathering capabilities",
432
+ deliverables: "Create non-standard output formats like PDFs and emails",
433
+ };
434
+
435
+ // ============================================================================
436
+ // Group Labels
437
+ // ============================================================================
438
+
439
+ export const GROUP_LABELS: Record<KSAGroup, string> = {
440
+ research: "Research",
441
+ };
442
+
443
+ export const GROUP_DESCRIPTIONS: Record<KSAGroup, string> = {
444
+ research: "Web search, news monitoring, company data, and browser automation",
445
+ };
446
+
447
+ export const GROUP_ICONS: Record<KSAGroup, string> = {
448
+ research: "mdi:magnify",
449
+ };
450
+
451
+ export const GROUP_ORDER: KSAGroup[] = ["research"];
452
+
453
+ // ============================================================================
454
+ // Default KSA Sets
455
+ // ============================================================================
456
+
457
+ import { KSA_REGISTRY, getKSA, CORE_KSAS } from "../../ksa/_generated/registry";
458
+
459
+ /** KSAs that are disabled by default (require explicit enablement) */
460
+ export const DEFAULT_DISABLED_KSAS = ["brandscan", "boards"];
461
+
462
+ /**
463
+ * Get core KSAs that are always available.
464
+ */
465
+ export function getCoreKSAs() {
466
+ return KSA_REGISTRY.filter((k) => k.category === "core");
467
+ }
468
+
469
+ /**
470
+ * Get KSAs by group (within skills category).
471
+ */
472
+ export function getKSAsByGroup(group: KSAGroup) {
473
+ return KSA_REGISTRY.filter((k) => k.group === group);
474
+ }
475
+
476
+ /**
477
+ * Get skills KSAs grouped by their group.
478
+ */
479
+ export function getSkillsByGroup(): Record<KSAGroup, typeof KSA_REGISTRY> {
480
+ const result: Record<KSAGroup, typeof KSA_REGISTRY> = {
481
+ research: [],
482
+ };
483
+
484
+ for (const ksa of KSA_REGISTRY) {
485
+ if (ksa.category === "skills" && ksa.group) {
486
+ result[ksa.group].push(ksa);
487
+ }
488
+ }
489
+
490
+ return result;
491
+ }
492
+
493
+ /**
494
+ * Get all available KSA names.
495
+ */
496
+ export function getKSANames(): string[] {
497
+ return KSA_REGISTRY.map((k) => k.name);
498
+ }
499
+
500
+ /**
501
+ * Get a default set of KSAs for common use cases.
502
+ */
503
+ export function getDefaultKSAs(
504
+ purpose: "research" | "content" | "automation" | "minimal" | "all" = "all"
505
+ ): string[] {
506
+ const core = CORE_KSAS;
507
+
508
+ switch (purpose) {
509
+ case "minimal":
510
+ return core;
511
+ case "research":
512
+ return [...core, "web", "news", "companies", "social"];
513
+ case "content":
514
+ return [...core, "web", "pdf", "email"];
515
+ case "automation":
516
+ return [...core, "web", "pdf", "email", "browser"];
517
+ case "all":
518
+ default:
519
+ return getKSANames().filter((k) => !DEFAULT_DISABLED_KSAS.includes(k));
520
+ }
521
+ }
522
+
523
+ // ============================================================================
524
+ // Config Functions
525
+ // ============================================================================
526
+
527
+ /**
528
+ * Get config schema for a KSA.
529
+ */
530
+ export function getConfigSchema(
531
+ ksaName: string
532
+ ): Record<string, ConfigField> | undefined {
533
+ return CONFIG_SCHEMAS[ksaName];
534
+ }
535
+
536
+ /**
537
+ * Get default config for a KSA.
538
+ */
539
+ export function getConfigDefaults(
540
+ ksaName: string
541
+ ): Record<string, unknown> | undefined {
542
+ return CONFIG_DEFAULTS[ksaName];
543
+ }
544
+
545
+ /**
546
+ * Merge user config with defaults.
547
+ */
548
+ export function mergeWithDefaults(
549
+ ksaName: string,
550
+ userConfig: Record<string, unknown>
551
+ ): Record<string, unknown> {
552
+ const defaults = CONFIG_DEFAULTS[ksaName] || {};
553
+ return { ...defaults, ...userConfig };
554
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Agent Mail - Simple inter-agent messaging
3
+ *
4
+ * Used for:
5
+ * - Beads notifications
6
+ * - Agent-to-agent communication
7
+ * - Async task handoffs
8
+ */
9
+
10
+ import { v } from "convex/values";
11
+ import { action, query, mutation, internalMutation, internalQuery } from "../_generated/server";
12
+ import { internal } from "../_generated/api";
13
+
14
+ /** Send a message to a recipient */
15
+ export const send = action({
16
+ args: {
17
+ recipientId: v.string(),
18
+ messageType: v.string(),
19
+ payload: v.any(),
20
+ ttlMs: v.optional(v.number()),
21
+ },
22
+ handler: async (ctx, args) => {
23
+ const id = await ctx.runMutation(internal.mail.sendInternal, {
24
+ senderId: "system",
25
+ recipientId: args.recipientId,
26
+ messageType: args.messageType,
27
+ payload: args.payload,
28
+ expiresAt: args.ttlMs ? Date.now() + args.ttlMs : undefined,
29
+ });
30
+ return { success: true, mailId: id };
31
+ },
32
+ });
33
+
34
+ /** Internal mutation to insert mail */
35
+ export const sendInternal = internalMutation({
36
+ args: {
37
+ senderId: v.string(),
38
+ recipientId: v.string(),
39
+ messageType: v.string(),
40
+ payload: v.any(),
41
+ expiresAt: v.optional(v.number()),
42
+ },
43
+ handler: async (ctx, args) => {
44
+ return await ctx.db.insert("agentMail", {
45
+ senderId: args.senderId,
46
+ recipientId: args.recipientId,
47
+ messageType: args.messageType,
48
+ payload: args.payload,
49
+ read: false,
50
+ expiresAt: args.expiresAt,
51
+ createdAt: Date.now(),
52
+ });
53
+ },
54
+ });
55
+
56
+ /** Get inbox messages for current recipient */
57
+ export const inbox = action({
58
+ args: {
59
+ recipientId: v.optional(v.string()),
60
+ limit: v.optional(v.number()),
61
+ },
62
+ handler: async (ctx, args) => {
63
+ const messages = await ctx.runQuery(internal.mail.listMessages, {
64
+ recipientId: args.recipientId || "anonymous",
65
+ limit: args.limit || 50,
66
+ });
67
+ return { messages };
68
+ },
69
+ });
70
+
71
+ /** List messages query */
72
+ export const listMessages = internalQuery({
73
+ args: {
74
+ recipientId: v.string(),
75
+ limit: v.number(),
76
+ },
77
+ handler: async (ctx, args) => {
78
+ return await ctx.db
79
+ .query("agentMail")
80
+ .withIndex("by_recipient", (q) => q.eq("recipientId", args.recipientId))
81
+ .order("desc")
82
+ .take(args.limit);
83
+ },
84
+ });
85
+
86
+ /** Mark message as read */
87
+ export const markRead = mutation({
88
+ args: { mailId: v.id("agentMail") },
89
+ handler: async (ctx, args) => {
90
+ await ctx.db.patch(args.mailId, { read: true });
91
+ },
92
+ });