@johpaz/hive-sdk 0.1.3 → 0.1.5

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 (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -20
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -17
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /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";