@johpaz/hive-sdk 0.1.4 → 0.1.6

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 (278) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +78 -23
  3. package/bun.lock +55 -29
  4. package/bunfig.toml +4 -2
  5. package/docs/API-AGENTS.md +78 -27
  6. package/docs/API-CONTEXT-COMPILER.md +31 -34
  7. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  8. package/docs/HIVE-HARNESS.md +1 -1
  9. package/docs/INDEX.md +4 -4
  10. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  11. package/package.json +17 -12
  12. package/packages/cli/package.json +2 -2
  13. package/packages/cli/src/commands/create-app.test.ts +36 -7
  14. package/packages/cli/src/commands/init.ts +3 -3
  15. package/packages/cli/src/commands/run.ts +1 -1
  16. package/packages/cli/src/commands/test.ts +37 -25
  17. package/packages/cli/src/commands/trace.ts +30 -28
  18. package/packages/cli/templates/hive-app/.env.example +10 -2
  19. package/packages/cli/templates/hive-app/README.md +103 -0
  20. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  21. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  22. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  23. package/packages/core/package.json +13 -12
  24. package/packages/core/src/agent/acceptance-checks.ts +172 -0
  25. package/packages/core/src/agent/agent-catalog.ts +348 -0
  26. package/packages/core/src/agent/agent-loop.ts +1373 -0
  27. package/packages/core/src/agent/capability-search.ts +186 -0
  28. package/packages/core/src/agent/catalog-selector.ts +103 -0
  29. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  30. package/packages/core/src/agent/context-compiler.ts +689 -0
  31. package/packages/core/src/agent/conversation-store.ts +381 -0
  32. package/packages/core/src/agent/curator.ts +276 -0
  33. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  34. package/packages/core/src/agent/goal-runner.ts +323 -0
  35. package/packages/core/src/agent/index.ts +17 -12
  36. package/packages/core/src/agent/llm-client.ts +266 -0
  37. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  38. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  39. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  40. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  41. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  42. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  43. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  44. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  45. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  47. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  48. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  49. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  50. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  51. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  52. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  54. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  55. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  56. package/packages/core/src/agent/playbook-selector.ts +119 -0
  57. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  58. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  59. package/packages/core/src/agent/providers/index.ts +35 -16
  60. package/packages/core/src/agent/reflector.ts +320 -0
  61. package/packages/core/src/agent/routing-intent.ts +22 -0
  62. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  63. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  64. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  65. package/packages/core/src/agent/skill-selector.ts +374 -0
  66. package/packages/core/src/agent/stuck-loop.ts +209 -0
  67. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  68. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  69. package/packages/core/src/api/createAgent.test.ts +139 -27
  70. package/packages/core/src/api/createAgent.ts +232 -44
  71. package/packages/core/src/artifacts/store.ts +162 -0
  72. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  73. package/packages/core/src/canvas/canvas.test.ts +8 -4
  74. package/packages/core/src/canvas/emitter.ts +131 -80
  75. package/packages/core/src/canvas/index.ts +1 -3
  76. package/packages/core/src/channels/base.ts +9 -1
  77. package/packages/core/src/channels/discord.ts +5 -4
  78. package/packages/core/src/channels/manager.ts +122 -30
  79. package/packages/core/src/channels/slack.ts +5 -4
  80. package/packages/core/src/channels/telegram.ts +36 -6
  81. package/packages/core/src/channels/webchat.ts +11 -10
  82. package/packages/core/src/channels/whatsapp.ts +23 -7
  83. package/packages/core/src/config/index.ts +13 -2
  84. package/packages/core/src/config/loader.ts +76 -29
  85. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  86. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  87. package/packages/core/src/events/agent-bus.ts +44 -68
  88. package/packages/core/src/events/channel-narration.ts +150 -0
  89. package/packages/core/src/events/narration.ts +82 -0
  90. package/packages/core/src/events/tool-narration.ts +62 -0
  91. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  92. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  93. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  94. package/packages/core/src/gateway/lane-queue.ts +173 -0
  95. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  96. package/packages/core/src/gateway/server.ts +1 -1
  97. package/packages/core/src/harness/index.ts +46 -27
  98. package/packages/core/src/index.ts +33 -27
  99. package/packages/core/src/mcp/hot-reload.ts +32 -23
  100. package/packages/core/src/mcp/index.ts +6 -3
  101. package/packages/core/src/mcp/singleton.ts +1 -4
  102. package/packages/core/src/mcp/tool-sync.ts +138 -0
  103. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  104. package/packages/core/src/memory/Scratchpad.ts +27 -34
  105. package/packages/core/src/multimodal/vision-service.ts +44 -38
  106. package/packages/core/src/resilience/retry.ts +95 -0
  107. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  108. package/packages/core/src/scheduler/index.ts +9 -7
  109. package/packages/core/src/scheduler/integration.ts +46 -26
  110. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  111. package/packages/core/src/scheduler/types.ts +7 -2
  112. package/packages/core/src/security/Pairing.ts +1 -1
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  115. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  116. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  117. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  118. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  119. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  120. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  121. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  122. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  123. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  124. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  125. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  128. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  129. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  130. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  131. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  132. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  133. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  134. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  135. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  137. package/packages/core/src/skills/skills.test.ts +52 -11
  138. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  139. package/packages/core/src/storage/bootstrap.ts +151 -0
  140. package/packages/core/src/storage/causal-events.ts +84 -0
  141. package/packages/core/src/storage/collections.ts +680 -0
  142. package/packages/core/src/storage/crypto.ts +205 -74
  143. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  144. package/packages/core/src/storage/hivedb.ts +61 -0
  145. package/packages/core/src/storage/index.ts +111 -18
  146. package/packages/core/src/storage/model-id.ts +53 -0
  147. package/packages/core/src/storage/onboarding.ts +540 -972
  148. package/packages/core/src/storage/reconcile.ts +238 -0
  149. package/packages/core/src/storage/seed.ts +572 -406
  150. package/packages/core/src/storage/usage.ts +285 -225
  151. package/packages/core/src/storage/user-email.ts +11 -0
  152. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  153. package/packages/core/src/swarm/EventBridge.ts +1 -1
  154. package/packages/core/src/swarm/index.ts +12 -9
  155. package/packages/core/src/tool-runtime/index.ts +146 -23
  156. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  157. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  158. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  159. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  160. package/packages/core/src/tools/agents/index.ts +784 -292
  161. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  162. package/packages/core/src/tools/api/api-request.ts +174 -0
  163. package/packages/core/src/tools/api/index.ts +16 -0
  164. package/packages/core/src/tools/cli/index.ts +4 -0
  165. package/packages/core/src/tools/core/index.ts +281 -112
  166. package/packages/core/src/tools/cron/index.ts +121 -124
  167. package/packages/core/src/tools/index.ts +63 -78
  168. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  169. package/packages/core/src/tools/types.ts +3 -1
  170. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  171. package/packages/core/src/tools/web/browser-backend.ts +129 -0
  172. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  173. package/packages/core/src/tools/web/browser-service.ts +80 -35
  174. package/packages/core/src/tools/web/browser-type.ts +3 -8
  175. package/packages/core/src/tools/web/index.ts +4 -4
  176. package/packages/core/src/tools/web/webview-backend.ts +412 -0
  177. package/packages/core/src/voice/index.ts +89 -63
  178. package/packages/core/src/workers/agent.worker.ts +2 -2
  179. package/packages/core/src/workers/workers.test.ts +3 -10
  180. package/scripts/bump-version.ts +248 -0
  181. package/scripts/generate-skill-bundle.ts +108 -0
  182. package/test/acceptance-checks.test.ts +403 -0
  183. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  184. package/test/browser-backend.test.ts +308 -0
  185. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  186. package/test/causal-events.test.ts +117 -0
  187. package/test/compaction.test.ts +105 -0
  188. package/test/context-compiler.test.ts +269 -0
  189. package/test/curator.test.ts +130 -0
  190. package/test/durable-queue.test.ts +114 -0
  191. package/test/harness-barrel.test.ts +64 -0
  192. package/test/hive-helpers.test.ts +130 -0
  193. package/test/hivedb-search.test.ts +189 -0
  194. package/test/internal-turns.test.ts +166 -0
  195. package/test/job-idempotency.test.ts +68 -0
  196. package/test/job-retry-backoff.test.ts +184 -0
  197. package/test/job-store.test.ts +381 -0
  198. package/test/llm-retry.test.ts +97 -0
  199. package/test/memory-perf.test.ts +774 -0
  200. package/test/minimal-loadout.test.ts +78 -0
  201. package/test/model-catalog.test.ts +105 -0
  202. package/test/preload.ts +12 -0
  203. package/test/reflector.test.ts +320 -0
  204. package/test/retention-cap.test.ts +91 -0
  205. package/test/retired-capabilities-pruned.test.ts +192 -0
  206. package/test/run-store.test.ts +355 -0
  207. package/test/scratchpad.test.ts +74 -0
  208. package/test/secrets-durability.test.ts +119 -0
  209. package/test/seed-model-reseed.test.ts +155 -0
  210. package/test/setup-agent-seed.test.ts +264 -0
  211. package/test/tool-inventory.test.ts +65 -0
  212. package/test/tool-runtime.test.ts +258 -0
  213. package/test/tool-selector-runtime-tools.test.ts +117 -0
  214. package/test/toon.test.ts +429 -0
  215. package/tsconfig.json +2 -0
  216. package/packages/core/src/ace/Curator.ts +0 -158
  217. package/packages/core/src/ace/Reflector.ts +0 -200
  218. package/packages/core/src/ace/index.ts +0 -4
  219. package/packages/core/src/agent/AgentRunner.ts +0 -711
  220. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  221. package/packages/core/src/agent/ContextGuard.ts +0 -91
  222. package/packages/core/src/agent/ConversationStore.ts +0 -254
  223. package/packages/core/src/agent/Hooks.ts +0 -166
  224. package/packages/core/src/agent/StuckLoop.ts +0 -133
  225. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  226. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  227. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  228. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  229. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  230. package/packages/core/src/agent/selectors/index.ts +0 -6
  231. package/packages/core/src/auth/auth.ts +0 -121
  232. package/packages/core/src/auth/index.ts +0 -1
  233. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  234. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  235. package/packages/core/src/harness/collections.ts +0 -98
  236. package/packages/core/src/harness/goal-verifier.ts +0 -141
  237. package/packages/core/src/harness/harness.test.ts +0 -236
  238. package/packages/core/src/harness/reconcile.ts +0 -149
  239. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  240. package/packages/core/src/multimodal/VisionService.ts +0 -293
  241. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  242. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  243. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  244. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  245. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  246. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  247. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  248. package/packages/core/src/scheduler/dag/index.ts +0 -26
  249. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  250. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  251. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  252. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  253. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  254. package/packages/core/src/storage/hiveSeed.ts +0 -308
  255. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  256. package/packages/core/src/storage/schema.ts +0 -689
  257. package/packages/core/src/storage/storage.test.ts +0 -37
  258. package/packages/core/src/swarm/AgentBus.ts +0 -460
  259. package/packages/core/src/swarm/EventBus.ts +0 -169
  260. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  261. package/packages/core/src/tools/bridge-events.ts +0 -26
  262. package/packages/core/src/tools/canvas/index.ts +0 -375
  263. package/packages/core/src/tools/codebridge/index.ts +0 -342
  264. package/packages/core/src/tools/meeting/index.ts +0 -353
  265. package/packages/core/src/tools/projects/index.ts +0 -37
  266. package/packages/core/src/tools/projects/project-create.ts +0 -94
  267. package/packages/core/src/tools/projects/project-done.ts +0 -66
  268. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  269. package/packages/core/src/tools/projects/project-list.ts +0 -96
  270. package/packages/core/src/tools/projects/project-update.ts +0 -72
  271. package/packages/core/src/tools/projects/task-create.ts +0 -68
  272. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  273. package/packages/core/src/tools/projects/task-update.ts +0 -93
  274. package/packages/core/src/tools/voice/index.ts +0 -104
  275. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  276. package/packages/core/src/tools/web/api-request.ts +0 -239
  277. package/test/setup-db.ts +0 -216
  278. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,241 @@
1
+ import { col, toIndexable, fromIndexable, updateDoc } from "../storage/hive";
2
+ import type {
3
+ AgentDoc,
4
+ McpServerDoc,
5
+ ModelDoc,
6
+ ProviderDoc,
7
+ SkillDoc,
8
+ AgentModelOverride,
9
+ } from "../storage/collections";
10
+ import { createAllTools } from "../tools";
11
+ import { loadConfig } from "../config/loader";
12
+ import type { MCPClientManager } from "../mcp/index.ts";
13
+ import { logger } from "../utils/logger";
14
+
15
+ const log = logger.child("delegation-runtime");
16
+ const MCP_IDLE_TTL_MS = 2 * 60_000;
17
+
18
+ export interface PrepareDelegationOptions {
19
+ workspace: string | null;
20
+ /** Fallback provider/model when the target row has none of its own (catalog agents are seeded without one — inherits the parent/coordinator's). */
21
+ parentProviderId?: string | null;
22
+ parentModelId?: string | null;
23
+ /** Model to avoid only during capability-based DB fallback resolution. Explicit agent/parent configuration always wins. */
24
+ executorModelId?: string | null;
25
+ mcpManager?: MCPClientManager | null;
26
+ }
27
+
28
+ export interface PreparedDelegation {
29
+ agent: AgentDoc;
30
+ toolNames: string[];
31
+ skillIds: string[];
32
+ mcpServerIds: string[];
33
+ providerId: string;
34
+ modelId: string;
35
+ release(): Promise<void>;
36
+ }
37
+
38
+ interface McpLease {
39
+ refs: number;
40
+ serverName: string;
41
+ timer?: ReturnType<typeof setTimeout>;
42
+ }
43
+
44
+ const mcpLeases = new Map<string, McpLease>();
45
+
46
+ function matchesPattern(name: string, pattern: string): boolean {
47
+ if (!pattern.includes("*")) return name === pattern;
48
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
49
+ return new RegExp(`^${escaped}$`).test(name);
50
+ }
51
+
52
+ export function expandToolAllowlist(patterns: string[], availableNames?: string[]): string[] {
53
+ const names = availableNames ?? createAllTools(loadConfig()).map((tool) => tool.name);
54
+ const selected = names.filter((name) => patterns.some((pattern) => matchesPattern(name, pattern)));
55
+ return [...new Set(selected)].sort();
56
+ }
57
+
58
+ function parseCapabilities(model: ModelDoc): string[] {
59
+ try {
60
+ return model.capabilities ? JSON.parse(model.capabilities) : [];
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Resolves the effective provider/model for a delegation. Persisted database
68
+ * configuration is authoritative: the agent's complete provider/model pair
69
+ * wins, otherwise the parent's complete pair is inherited. Capability metadata
70
+ * is consulted only when neither row supplies a complete pair, and candidates
71
+ * are selected exclusively from active database rows.
72
+ */
73
+ export async function resolveAgentModel(
74
+ modelOverride: AgentModelOverride | null,
75
+ rowProviderId: string | null,
76
+ rowModelId: string | null,
77
+ parentProviderId: string | null,
78
+ parentModelId: string | null,
79
+ executorModelId?: string | null,
80
+ ): Promise<{ providerId: string; modelId: string }> {
81
+ if (rowProviderId && rowModelId) {
82
+ return { providerId: rowProviderId, modelId: rowModelId };
83
+ }
84
+ if (parentProviderId && parentModelId) {
85
+ return { providerId: parentProviderId, modelId: parentModelId };
86
+ }
87
+
88
+ const fallback = {
89
+ providerId: rowProviderId ?? parentProviderId ?? "",
90
+ modelId: rowModelId ?? parentModelId ?? "",
91
+ };
92
+ if (!modelOverride) return fallback;
93
+
94
+ const models = (await (await col<ModelDoc>("models")).scan({})).map((entry) => entry.doc);
95
+ const providers = new Map(
96
+ (await (await col<ProviderDoc>("providers")).scan({}))
97
+ .map((entry) => entry.doc)
98
+ .filter((provider) => provider.enabled && provider.active)
99
+ .map((provider) => [provider.id, provider]),
100
+ );
101
+ const compatible = (model: ModelDoc) => {
102
+ if (!model.enabled || !providers.has(model.provider_id)) return false;
103
+ const caps = parseCapabilities(model);
104
+ if (!modelOverride.required_capabilities.every((cap) => caps.includes(cap))) return false;
105
+ if (modelOverride.prefer_different_family && executorModelId) {
106
+ const currentFamily = executorModelId.split(/[/:]/)[0];
107
+ const candidateFamily = model.id.split(/[/:]/)[0];
108
+ if (currentFamily === candidateFamily) return false;
109
+ }
110
+ return true;
111
+ };
112
+
113
+ const candidate = models.find(compatible);
114
+ return candidate ? { providerId: candidate.provider_id, modelId: candidate.id } : fallback;
115
+ }
116
+
117
+ export async function validateSkillIds(skillIds: string[]): Promise<string[]> {
118
+ const skills = await col<SkillDoc>("skills");
119
+ const valid: string[] = [];
120
+ for (const id of skillIds) {
121
+ const entry = await skills.get(id);
122
+ if (!entry?.doc.active) throw new Error(`Agent references missing or inactive skill: ${id}`);
123
+ valid.push(id);
124
+ }
125
+ return valid;
126
+ }
127
+
128
+ async function acquireMcpLease(
129
+ serverId: string,
130
+ manager: MCPClientManager,
131
+ ): Promise<{ serverId: string; serverName: string }> {
132
+ const entry = await (await col<McpServerDoc>("mcpServers")).get(serverId);
133
+ if (!entry?.doc.enabled) throw new Error(`MCP server is missing or disabled: ${serverId}`);
134
+ // Gateway initialization registers DB-backed MCP servers under their stable
135
+ // document id. The display name is only used in UI/tool labels.
136
+ const serverName = entry.doc.id;
137
+ const existing = mcpLeases.get(serverId);
138
+ if (existing) {
139
+ if (existing.timer) clearTimeout(existing.timer);
140
+ existing.timer = undefined;
141
+ existing.refs++;
142
+ return { serverId, serverName: existing.serverName };
143
+ }
144
+ await manager.connectServer(serverName);
145
+ mcpLeases.set(serverId, { refs: 1, serverName });
146
+ return { serverId, serverName };
147
+ }
148
+
149
+ async function releaseMcpLease(serverId: string, manager: MCPClientManager): Promise<void> {
150
+ const lease = mcpLeases.get(serverId);
151
+ if (!lease) return;
152
+ lease.refs = Math.max(0, lease.refs - 1);
153
+ if (lease.refs > 0) return;
154
+ lease.timer = setTimeout(() => {
155
+ const current = mcpLeases.get(serverId);
156
+ if (!current || current.refs > 0) return;
157
+ void manager.disconnectServer(current.serverName)
158
+ .catch((err) => log.warn(`[delegation-runtime] MCP disconnect failed: ${(err as Error).message}`))
159
+ .finally(() => mcpLeases.delete(serverId));
160
+ }, MCP_IDLE_TTL_MS);
161
+ lease.timer.unref?.();
162
+ }
163
+
164
+ /**
165
+ * Prepares a delegation target for execution: expands its tool allowlist
166
+ * (when it has one — catalog agents do, plain agent_create workers use their
167
+ * stored tools_json as-is), validates skills, resolves the effective
168
+ * provider/model, and acquires any requested MCP leases.
169
+ *
170
+ * This never creates a new AgentDoc — the row already exists (seeded from
171
+ * the catalog, or created via agent_create).
172
+ * It does persist the resolved workspace/model back onto that row (same as
173
+ * the old materialization did) so agent-loop.ts picks them up when it loads
174
+ * the agent — which reintroduces a narrow, accepted race for the rare case
175
+ * of two concurrent delegations to the *same* catalog agent with different
176
+ * workspaces (catalog agents are global rows, not one-per-workspace anymore).
177
+ * `updateDoc`'s OCC retry keeps this safe (last-write-wins), never corrupt.
178
+ */
179
+ export async function prepareDelegation(agentId: string, opts: PrepareDelegationOptions): Promise<PreparedDelegation> {
180
+ const agents = await col<AgentDoc>("agents");
181
+ const entry = await agents.get(agentId);
182
+ if (!entry?.doc.enabled) throw new Error(`Agent not found or disabled: ${agentId}`);
183
+ const agentDoc = entry.doc;
184
+
185
+ const toolNames = agentDoc.tool_allowlist_json
186
+ ? expandToolAllowlist(JSON.parse(agentDoc.tool_allowlist_json))
187
+ : (agentDoc.tools_json ? JSON.parse(agentDoc.tools_json) : []);
188
+
189
+ const skillIds = agentDoc.skills_json ? await validateSkillIds(JSON.parse(agentDoc.skills_json)) : [];
190
+
191
+ const requestedMcp: string[] = agentDoc.mcp_server_ids_json ? JSON.parse(agentDoc.mcp_server_ids_json) : [];
192
+ if (requestedMcp.length > 0 && !opts.mcpManager) throw new Error("MCP servers requested but MCP manager is unavailable");
193
+
194
+ const acquired: string[] = [];
195
+ try {
196
+ for (const serverId of requestedMcp) {
197
+ await acquireMcpLease(serverId, opts.mcpManager!);
198
+ acquired.push(serverId);
199
+ }
200
+ } catch (err) {
201
+ await Promise.all(acquired.map((id) => releaseMcpLease(id, opts.mcpManager!)));
202
+ throw err;
203
+ }
204
+
205
+ const modelOverride: AgentModelOverride | null = agentDoc.model_override_json
206
+ ? JSON.parse(agentDoc.model_override_json)
207
+ : null;
208
+ const resolved = await resolveAgentModel(
209
+ modelOverride,
210
+ fromIndexable(agentDoc.provider_id),
211
+ fromIndexable(agentDoc.model_id),
212
+ opts.parentProviderId ?? null,
213
+ opts.parentModelId ?? null,
214
+ opts.executorModelId ?? opts.parentModelId ?? null,
215
+ );
216
+
217
+ await updateDoc<AgentDoc>("agents", agentId, {
218
+ workspace: opts.workspace,
219
+ provider_id: toIndexable(resolved.providerId || null),
220
+ model_id: toIndexable(resolved.modelId || null),
221
+ active_mcp_json: JSON.stringify(requestedMcp),
222
+ updated_at: Date.now(),
223
+ }).catch((err) => log.warn(`[prepareDelegation] Failed to persist delegation context for ${agentId}: ${(err as Error).message}`));
224
+
225
+ let released = false;
226
+ return {
227
+ agent: agentDoc,
228
+ toolNames,
229
+ skillIds,
230
+ mcpServerIds: requestedMcp,
231
+ providerId: resolved.providerId,
232
+ modelId: resolved.modelId,
233
+ release: async () => {
234
+ if (released) return;
235
+ released = true;
236
+ if (opts.mcpManager) {
237
+ await Promise.all(requestedMcp.map((id) => releaseMcpLease(id, opts.mcpManager!)));
238
+ }
239
+ },
240
+ };
241
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * goal-runner — orchestrates a multi-turn agent run toward a verifiable goal.
3
+ *
4
+ * Flow:
5
+ * 1. Create an AgentRun (kind="goal") with goal + budget
6
+ * 2. Run agent turns until:
7
+ * - Goal is met (verified by goal_check_tool or LLM verifier)
8
+ * - Budget exhausted (iterations/tokens/turns)
9
+ * - Max goal attempts reached
10
+ * 3. Between turns: compact context, inject goal/reason/budget reminder
11
+ * 4. On completion: persist success/failure + notify channel
12
+ *
13
+ * The budget is HARD: iterations, tokens, and turns all count across the
14
+ * entire run (not per-turn). This prevents endless loops.
15
+ */
16
+
17
+ import { logger } from "../utils/logger";
18
+ import { callLLM, type LLMMessage } from "./llm-client";
19
+ import { createRun, type AcceptanceCriterion } from "./run-store";
20
+ import { clearOldToolResults } from "./compaction";
21
+ import { loadConfig } from "../config/loader";
22
+ import { getDurableQueue } from "../gateway/durable-queue.ts";
23
+ import { recordLLMUsage } from "./tracer";
24
+
25
+ export type { AcceptanceCriterion } from "./run-store";
26
+
27
+ export interface AcceptanceResult {
28
+ id: string;
29
+ description: string;
30
+ met: boolean;
31
+ evidence: string;
32
+ }
33
+
34
+ const log = logger.child("goal-runner");
35
+
36
+ const MAX_GOAL_ATTEMPTS = 5;
37
+
38
+ export interface GoalRunOptions {
39
+ agentId: string;
40
+ threadId: string;
41
+ userId: string;
42
+ channel: string | null;
43
+ goal: string;
44
+ goalCheckTool?: string | null;
45
+ maxIterationsPerTurn?: number;
46
+ maxTurns?: number;
47
+ maxTokens?: number;
48
+ maxAttempts?: number;
49
+ /** Whole-job acceptance criteria — when set, the goal is only "met" once every criterion is. */
50
+ acceptance?: AcceptanceCriterion[];
51
+ }
52
+
53
+ export interface GoalRunResult {
54
+ met: boolean;
55
+ reason: string;
56
+ turnsUsed: number;
57
+ iterationsUsed: number;
58
+ tokensUsed: number;
59
+ attempts: number;
60
+ finalContent: string;
61
+ }
62
+
63
+ /**
64
+ * Run a goal-based agent loop with verification between turns.
65
+ */
66
+ export async function runGoal(opts: GoalRunOptions): Promise<GoalRunResult> {
67
+ const maxAttempts = opts.maxAttempts ?? MAX_GOAL_ATTEMPTS;
68
+ const maxIterationsPerTurn = opts.maxIterationsPerTurn ?? 20;
69
+ const maxTurns = opts.maxTurns ?? 10;
70
+ const maxTokens = opts.maxTokens ?? 200_000;
71
+
72
+ log.info(`[runGoal] Starting goal="${opts.goal}" agent=${opts.agentId} maxTurns=${maxTurns} maxAttempts=${maxAttempts}`);
73
+
74
+ // Create the durable AgentRun
75
+ const run = await createRun({
76
+ thread_id: opts.threadId,
77
+ agent_id: opts.agentId,
78
+ user_id: opts.userId,
79
+ channel: opts.channel,
80
+ kind: "goal",
81
+ max_iterations: maxIterationsPerTurn * maxTurns,
82
+ max_turns: maxTurns,
83
+ max_tokens: maxTokens,
84
+ goal: opts.goal,
85
+ goal_check_tool: opts.goalCheckTool ?? null,
86
+ resume_policy: "resume",
87
+ acceptance: opts.acceptance,
88
+ });
89
+
90
+ // Enqueue a goal_run job in the durable queue
91
+ const queue = getDurableQueue();
92
+ const job = await queue.enqueue({
93
+ lane: `goal:${run.id}`,
94
+ type: "goal_run",
95
+ run_id: run.id,
96
+ payload: {
97
+ agentId: opts.agentId,
98
+ threadId: opts.threadId,
99
+ goal: opts.goal,
100
+ goal_check_tool: opts.goalCheckTool,
101
+ maxAttempts,
102
+ budget: {
103
+ maxIterations: maxIterationsPerTurn,
104
+ maxTurns,
105
+ maxTokens,
106
+ },
107
+ },
108
+ });
109
+
110
+ log.info(`[runGoal] Enqueued goal_run job ${job.id} for run ${run.id}`);
111
+
112
+ // Note: The actual execution happens asynchronously via the durable queue's
113
+ // goal_run executor. This function returns the initial state — the caller
114
+ // can poll the run status or subscribe to the channel for notifications.
115
+ return {
116
+ met: false,
117
+ reason: "Goal run enqueued — execution is asynchronous. Poll task_status or watch the channel for completion.",
118
+ turnsUsed: 0,
119
+ iterationsUsed: 0,
120
+ tokensUsed: 0,
121
+ attempts: 0,
122
+ finalContent: "",
123
+ };
124
+ }
125
+
126
+ /** Runs a deterministic goal_check_tool, no LLM involved. */
127
+ async function runDeterministicCheck(checkTool: string, goal: string): Promise<{ met: boolean; reason: string } | null> {
128
+ try {
129
+ const { executeToolBatch } = await import("../tool-runtime");
130
+ const { createAllTools } = await import("../tools/index");
131
+ const allTools = createAllTools(loadConfig());
132
+ const toolDef = allTools.find((t) => t.name === checkTool);
133
+ if (!toolDef) {
134
+ log.warn(`[verifyGoal] Check tool "${checkTool}" not found in the tool registry — falling back to LLM verifier`);
135
+ return null;
136
+ }
137
+ const toolResults = await executeToolBatch({
138
+ toolCalls: [{
139
+ id: "goal-check",
140
+ function: { name: checkTool, arguments: JSON.stringify({ goal }) },
141
+ }],
142
+ allTools,
143
+ toolConfig: {},
144
+ });
145
+ const result = toolResults[0];
146
+ if (result?.ok) return interpretCheckResult(result.result);
147
+ return { met: false, reason: `Check tool failed: ${result?.error?.message ?? "unknown"}` };
148
+ } catch (err) {
149
+ log.warn(`[verifyGoal] Check tool "${checkTool}" failed: ${(err as Error).message}`);
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Judges every criterion that has no deterministic checkTool with a SINGLE
156
+ * LLM call (not one call per criterion) — the model returns a verdict per
157
+ * criterion id in one structured response.
158
+ */
159
+ async function judgeCriteriaWithLLM(
160
+ criteria: AcceptanceCriterion[],
161
+ messages: LLMMessage[],
162
+ providerCfg: any,
163
+ ): Promise<AcceptanceResult[]> {
164
+ try {
165
+ const verificationMessages: LLMMessage[] = [
166
+ ...clearOldToolResults(messages),
167
+ {
168
+ role: "user",
169
+ content: `Evaluá si cada uno de los siguientes criterios de aceptación se cumplió, basándote en la conversación anterior.\n\nCriterios:\n${criteria.map((c) => `- ${c.id}: ${c.description}`).join("\n")}\n\nRespondé en JSON estricto, un resultado por criterio:\n{"results":[{"id":"...","met":true/false,"reason":"explicación breve"}]}`,
170
+ },
171
+ ];
172
+
173
+ const response = await callLLM({ ...providerCfg, messages: verificationMessages, tools: undefined });
174
+ if (providerCfg.provider && providerCfg.model && response.usage) {
175
+ recordLLMUsage({
176
+ provider: providerCfg.provider,
177
+ model: providerCfg.model,
178
+ inputTokens: response.usage.input_tokens ?? 0,
179
+ outputTokens: response.usage.output_tokens ?? 0,
180
+ });
181
+ }
182
+
183
+ // Without this the error text falls through to JSON.parse and the catch below
184
+ // reports a bogus "Unexpected token" instead of the actual provider failure.
185
+ if (response.stop_reason === "error") {
186
+ throw new Error(response.error?.message ?? response.content);
187
+ }
188
+
189
+ const content = response.content?.trim() || "";
190
+ const candidate = content.slice(content.indexOf("{"), content.lastIndexOf("}") + 1);
191
+ const parsed = JSON.parse(candidate) as { results?: Array<{ id: string; met: boolean; reason?: string }> };
192
+ const byId = new Map((parsed.results ?? []).map((r) => [r.id, r]));
193
+ return criteria.map((c) => {
194
+ const r = byId.get(c.id);
195
+ return { id: c.id, description: c.description, met: r?.met === true, evidence: r?.reason || "El modelo no evaluó este criterio" };
196
+ });
197
+ } catch (err) {
198
+ log.warn(`[verifyGoal] LLM verification failed: ${(err as Error).message}`);
199
+ return criteria.map((c) => ({ id: c.id, description: c.description, met: false, evidence: `Verification error: ${(err as Error).message}` }));
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Verify whether a goal has been met using either:
205
+ * - A deterministic tool (goal_check_tool) — executes the tool and checks the result
206
+ * - An LLM verifier — asks the model to return JSON {met, reason}
207
+ *
208
+ * When `acceptance` criteria are supplied, each with its own checkTool is
209
+ * checked deterministically (no LLM), and every remaining criterion is
210
+ * judged together in a single LLM call — never one call per criterion. The
211
+ * overall verdict is the conjunction of all of them; the top-level
212
+ * `goal`/`checkTool` are ignored in that case.
213
+ */
214
+ export async function verifyGoal(
215
+ goal: string,
216
+ checkTool: string | null | undefined,
217
+ messages: LLMMessage[],
218
+ providerCfg: any,
219
+ acceptance?: AcceptanceCriterion[] | null,
220
+ ): Promise<{ met: boolean; reason: string; acceptanceResults?: AcceptanceResult[] }> {
221
+ if (acceptance && acceptance.length > 0) {
222
+ const results: AcceptanceResult[] = [];
223
+ const needsLLMJudgment: AcceptanceCriterion[] = [];
224
+
225
+ for (const criterion of acceptance) {
226
+ const deterministic = criterion.checkTool ? await runDeterministicCheck(criterion.checkTool, criterion.description) : null;
227
+ if (deterministic) {
228
+ results.push({ id: criterion.id, description: criterion.description, met: deterministic.met, evidence: deterministic.reason });
229
+ } else {
230
+ needsLLMJudgment.push(criterion);
231
+ }
232
+ }
233
+
234
+ if (needsLLMJudgment.length > 0) {
235
+ results.push(...(await judgeCriteriaWithLLM(needsLLMJudgment, messages, providerCfg)));
236
+ }
237
+
238
+ const met = results.every((r) => r.met);
239
+ const reason = results.map((r) => `${r.met ? "✅" : "❌"} ${r.description}: ${r.evidence}`).join("\n");
240
+ return { met, reason, acceptanceResults: results };
241
+ }
242
+
243
+ // If we have a deterministic check tool, execute it
244
+ if (checkTool) {
245
+ const deterministic = await runDeterministicCheck(checkTool, goal);
246
+ if (deterministic) return deterministic;
247
+ // Falls through to the LLM verifier when the tool is missing or errored.
248
+ }
249
+
250
+ // LLM verifier: ask the model to evaluate whether the goal is met
251
+ try {
252
+ const verificationMessages: LLMMessage[] = [
253
+ ...clearOldToolResults(messages),
254
+ {
255
+ role: "user",
256
+ content: `Evaluá si el siguiente objetivo ha sido cumplido basándote en la conversación anterior.\n\nObjetivo: "${goal}"\n\nRespondé en JSON:\n{"met": true/false, "reason": "explicación breve"}`,
257
+ },
258
+ ];
259
+
260
+ const response = await callLLM({
261
+ ...providerCfg,
262
+ messages: verificationMessages,
263
+ tools: undefined,
264
+ });
265
+ if (providerCfg.provider && providerCfg.model && response.usage) {
266
+ recordLLMUsage({
267
+ provider: providerCfg.provider,
268
+ model: providerCfg.model,
269
+ inputTokens: response.usage.input_tokens ?? 0,
270
+ outputTokens: response.usage.output_tokens ?? 0,
271
+ });
272
+ }
273
+
274
+ if (response.stop_reason === "error") {
275
+ throw new Error(response.error?.message ?? response.content);
276
+ }
277
+
278
+ const content = response.content?.trim() || "";
279
+ // Extract JSON from the response
280
+ const jsonMatch = content.match(/\{[^}]*\}/);
281
+ if (jsonMatch) {
282
+ const parsed = JSON.parse(jsonMatch[0]);
283
+ return {
284
+ met: !!parsed.met,
285
+ reason: parsed.reason || "No reason provided",
286
+ };
287
+ }
288
+ return { met: false, reason: "Could not parse verification response" };
289
+ } catch (err) {
290
+ log.warn(`[verifyGoal] LLM verification failed: ${(err as Error).message}`);
291
+ return { met: false, reason: `Verification error: ${(err as Error).message}` };
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Interpret a check tool's result strictly: an object with a boolean `met`,
297
+ * a bare boolean, or a JSON string with `met` — anything else is not met.
298
+ */
299
+ export function interpretCheckResult(raw: unknown): { met: boolean; reason: string } {
300
+ if (typeof raw === "boolean") {
301
+ return { met: raw, reason: raw ? "Check tool returned true" : "Check tool returned false" };
302
+ }
303
+ if (raw && typeof raw === "object" && "met" in (raw as Record<string, unknown>)) {
304
+ const obj = raw as { met: unknown; reason?: unknown };
305
+ return { met: obj.met === true, reason: typeof obj.reason === "string" ? obj.reason : `Check tool met=${obj.met === true}` };
306
+ }
307
+ if (typeof raw === "string") {
308
+ const trimmed = raw.trim();
309
+ if (trimmed === "true" || trimmed === "false") {
310
+ return { met: trimmed === "true", reason: `Check tool returned "${trimmed}"` };
311
+ }
312
+ try {
313
+ const parsed = JSON.parse(trimmed);
314
+ if (parsed && typeof parsed === "object" && "met" in parsed) {
315
+ return { met: parsed.met === true, reason: typeof parsed.reason === "string" ? parsed.reason : `Check tool met=${parsed.met === true}` };
316
+ }
317
+ if (typeof parsed === "boolean") {
318
+ return { met: parsed, reason: `Check tool returned ${parsed}` };
319
+ }
320
+ } catch { /* not JSON */ }
321
+ }
322
+ return { met: false, reason: "Check tool result had no interpretable met/true signal" };
323
+ }
@@ -1,12 +1,17 @@
1
- export * from "./AgentRunner.ts";
2
- export * from "./Compaction.ts";
3
- export * from "./ContextCompiler.ts";
4
- export * from "./ContextGuard.ts";
5
- export * from "./ConversationStore.ts";
6
- export * from "./Hooks.ts";
7
- export * from "./NativeTools.ts";
8
- export * from "./PromptBuilder.ts";
9
- export * from "./Service.ts";
10
- export * from "./StuckLoop.ts";
11
- export * from "./providers/index.ts";
12
- export * from "./selectors/index.ts";
1
+ export * from "./acceptance-checks.ts";
2
+ export * from "./agent-catalog.ts";
3
+ export * from "./agent-loop.ts";
4
+ export * from "./capability-search.ts";
5
+ export * from "./catalog-selector.ts";
6
+ export * from "./context-compiler.ts";
7
+ export * from "./conversation-store.ts";
8
+ export * from "./delegation-runtime.ts";
9
+ export * from "./llm-client.ts";
10
+ export * from "./minimal-loadout.ts";
11
+ export * from "./playbook-selector.ts";
12
+ export * from "./prompt-builder.ts";
13
+ export * from "./proof-packet.ts";
14
+ export * from "./run-store.ts";
15
+ export * from "./service.ts";
16
+ export * from "./skill-selector.ts";
17
+ export * from "./tool-selector.ts";