@johpaz/hive-sdk 0.2.0 → 0.3.1

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 (88) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/README.md +11 -3
  3. package/package.json +10 -4
  4. package/packages/core/src/agent/agent-catalog.ts +81 -24
  5. package/packages/core/src/agent/agent-loop.ts +2 -2
  6. package/packages/core/src/agent/compaction.ts +20 -1
  7. package/packages/core/src/agent/context-compiler.ts +7 -4
  8. package/packages/core/src/agent/conversation-store.ts +136 -2
  9. package/packages/core/src/agent/curator.ts +12 -3
  10. package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
  11. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
  12. package/packages/core/src/agent/playbook-selector.ts +18 -3
  13. package/packages/core/src/agent/prompt-builder.ts +2 -2
  14. package/packages/core/src/agent/providers/index.ts +37 -2
  15. package/packages/core/src/agent/reflector.ts +32 -9
  16. package/packages/core/src/agent/skill-selector.ts +2 -2
  17. package/packages/core/src/agent/thread-store.ts +43 -0
  18. package/packages/core/src/agent/tool-selector.ts +2 -0
  19. package/packages/core/src/api/createAgent.ts +68 -2
  20. package/packages/core/src/artifacts/index.ts +15 -0
  21. package/packages/core/src/artifacts/store.ts +77 -2
  22. package/packages/core/src/canvas/index.ts +9 -0
  23. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  24. package/packages/core/src/events/index.ts +18 -0
  25. package/packages/core/src/events/tool-narration.ts +4 -0
  26. package/packages/core/src/gateway/channel-notify.ts +103 -6
  27. package/packages/core/src/gateway/durable-queue.ts +13 -1
  28. package/packages/core/src/gateway/index.ts +3 -0
  29. package/packages/core/src/gateway/job-store.ts +6 -0
  30. package/packages/core/src/harness/executors.ts +493 -0
  31. package/packages/core/src/harness/index.ts +12 -2
  32. package/packages/core/src/hooks/index.ts +203 -0
  33. package/packages/core/src/images/index.ts +161 -0
  34. package/packages/core/src/index.ts +1 -0
  35. package/packages/core/src/multimodal/vision-service.ts +45 -13
  36. package/packages/core/src/resilience/index.ts +13 -0
  37. package/packages/core/src/scheduler/CronScheduler.ts +48 -21
  38. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  39. package/packages/core/src/scheduler/cron/index.ts +10 -0
  40. package/packages/core/src/scheduler/cron/job.ts +339 -0
  41. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  42. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  43. package/packages/core/src/scheduler/index.ts +21 -3
  44. package/packages/core/src/scheduler/integration.ts +16 -5
  45. package/packages/core/src/scheduler/types.ts +3 -18
  46. package/packages/core/src/services/agents.ts +268 -0
  47. package/packages/core/src/services/cron.ts +257 -0
  48. package/packages/core/src/services/endpoints.ts +289 -0
  49. package/packages/core/src/services/ethics.ts +107 -0
  50. package/packages/core/src/services/images.ts +212 -0
  51. package/packages/core/src/services/index.ts +112 -0
  52. package/packages/core/src/services/mcp.ts +201 -0
  53. package/packages/core/src/services/memory.ts +133 -0
  54. package/packages/core/src/services/models.ts +179 -0
  55. package/packages/core/src/services/providers.ts +152 -0
  56. package/packages/core/src/services/setup.ts +222 -0
  57. package/packages/core/src/services/skills.ts +241 -0
  58. package/packages/core/src/services/swarms.ts +307 -0
  59. package/packages/core/src/services/tools.ts +106 -0
  60. package/packages/core/src/sessions/index.ts +5 -3
  61. package/packages/core/src/sessions/resolve.ts +108 -0
  62. package/packages/core/src/skills/SkillLoader.ts +8 -1
  63. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  64. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  65. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  66. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  67. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  68. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  69. package/packages/core/src/storage/bootstrap.ts +74 -5
  70. package/packages/core/src/storage/collections.ts +106 -1
  71. package/packages/core/src/storage/crypto.ts +24 -7
  72. package/packages/core/src/storage/hive.ts +9 -3
  73. package/packages/core/src/storage/index.ts +2 -1
  74. package/packages/core/src/storage/onboarding.ts +59 -43
  75. package/packages/core/src/storage/reconcile.ts +6 -1
  76. package/packages/core/src/storage/seed.ts +98 -14
  77. package/packages/core/src/swarm/types.ts +3 -18
  78. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  79. package/packages/core/src/tool-runtime/index.ts +129 -14
  80. package/packages/core/src/tools/agents/index.ts +18 -60
  81. package/packages/core/src/tools/cli/index.ts +55 -0
  82. package/packages/core/src/tools/core/index.ts +50 -2
  83. package/packages/core/src/tools/cron/index.ts +4 -4
  84. package/packages/core/src/tools/images/index.ts +130 -0
  85. package/packages/core/src/tools/index.ts +14 -1
  86. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  87. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  88. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Ejecutores del harness — qué corre la cola durable cuando toma un job.
3
+ *
4
+ * `harness/` da la cola, los leases y el checkpoint, pero no sabía ejecutar
5
+ * nada: registrar los ejecutores quedaba en manos de quien usara el SDK, y eso
6
+ * son varios cientos de líneas de cableado —reintentos, epoch, proof packets,
7
+ * criterios de aceptación, fan-in— antes de poder correr un solo enjambre
8
+ * durable. Estos dos vienen listos:
9
+ *
10
+ * - `worker_task`: ejecuta un worker delegado en contexto aislado, verifica
11
+ * sus criterios de aceptación, arma el proof packet y avisa por el bus.
12
+ * - `goal_run`: orquesta varios turnos contra un objetivo hasta que lo
13
+ * verifica o se acaba el presupuesto.
14
+ *
15
+ * `chat_turn` NO está acá a propósito: en hive depende de `webchat-turn.ts`,
16
+ * que es su servidor HTTP. Un turno de chat lo define la aplicación —qué es un
17
+ * "canal", cómo se transmite un token— así que ese se registra desde fuera con
18
+ * `registerExecutor("chat_turn", ...)`.
19
+ *
20
+ * Registrar es opt-in: `initHarnessExecutors()` no se llama sola, porque quién
21
+ * ejecuta qué es decisión de la app.
22
+ */
23
+
24
+ import { registerExecutor, getDurableQueue, type JobExecutor } from "../gateway/durable-queue.ts";
25
+ import { logger } from "../utils/logger.ts";
26
+ import { col, updateDoc } from "../storage/hive.ts";
27
+ import { isRetryableError } from "../resilience/retry.ts";
28
+ import type { JobDoc, TaskDoc, AgentRunDoc, AgentDoc } from "../storage/collections.ts";
29
+ import { runAgent, runAgentIsolatedDetailed } from "../agent/agent-loop.ts";
30
+ import {
31
+ createRun, completeRun, failRun, interruptRun, getRun, reclaimRun, bumpTurn,
32
+ startLeaseRenewal, stopLeaseRenewal, deserializeAcceptance, deserializeEpoch,
33
+ } from "../agent/run-store.ts";
34
+ import { sendToUserChannel } from "../gateway/channel-notify.ts";
35
+ import { verifyGoal } from "../agent/goal-runner.ts";
36
+ import { buildProofPacket } from "../agent/proof-packet.ts";
37
+ import { runAcceptanceChecks, recordAgentOutcome } from "../agent/acceptance-checks.ts";
38
+ import { prepareDelegation, type PreparedDelegation } from "../agent/delegation-runtime.ts";
39
+ import { agentBus } from "../events/agent-bus.ts";
40
+ import type { MCPClientManager } from "../mcp/index.ts";
41
+ import { publishNarration } from "../events/narration.ts";
42
+ import {
43
+ emitDelegationStarted,
44
+ emitDelegationFinished,
45
+ emitWorkEvent,
46
+ } from "../canvas/emitter.ts";
47
+
48
+ const log = logger.child("harness-executors");
49
+
50
+ let mcpManager: MCPClientManager | null = null;
51
+
52
+ /** Las tools MCP que verá un worker delegado. Sin esto corre sólo con las nativas. */
53
+ export function setHarnessExecutorMCPManager(m: MCPClientManager | null): void {
54
+ mcpManager = m;
55
+ }
56
+
57
+ const workerTaskExecutor: JobExecutor = async (job, signal) => {
58
+ const payload = JSON.parse(job.payload_json);
59
+ const workerId = payload.workerId as string;
60
+ const taskDescription = payload.taskDescription as string;
61
+ const taskName = payload.taskName as string;
62
+ const taskId = payload.taskId as string | undefined;
63
+ const acceptance = (payload.acceptance ?? null) as import("../agent/run-store").AcceptanceCriterion[] | null;
64
+ const runId = job.run_id;
65
+ const turnId = payload.turnId as string | undefined;
66
+ const parentAgentId = (payload.parentAgentId as string | undefined) ?? "";
67
+ const originThreadId = payload.originThreadId as string | undefined;
68
+ const originChannel = payload.originChannel as string | undefined;
69
+ const originUserId = payload.userId as string | undefined;
70
+ const originSessionId = payload.originSessionId as string | undefined;
71
+
72
+ log.info(`[worker_task] Job ${job.id} → worker=${workerId} task="${taskName}"`);
73
+
74
+ const agentsCol = await col<AgentDoc>("agents");
75
+ const workerEntry = await agentsCol.get(workerId);
76
+ if (!workerEntry) return { ok: false, error: `Worker not found: ${workerId}`, retryable: false };
77
+ if (!workerEntry.doc.enabled) return { ok: false, error: `Worker disabled: ${workerEntry.doc.name}`, retryable: false };
78
+
79
+ const workerName = workerEntry.doc.name;
80
+ const isCatalogAgent = workerEntry.doc.source === "catalog";
81
+ agentBus.notifyTaskStarted(workerId, workerName, 0, taskName, "");
82
+ const delegationRef = taskId ?? job.id;
83
+ emitDelegationStarted({ workerId, parentAgentId, taskRef: delegationRef, taskName });
84
+ if (turnId && originThreadId) {
85
+ await publishNarration({
86
+ turnId,
87
+ threadId: originThreadId,
88
+ channel: originChannel,
89
+ userId: originUserId,
90
+ sessionId: originSessionId,
91
+ agentId: workerId,
92
+ agentName: workerName,
93
+ kind: "worker_started",
94
+ status: "running",
95
+ label: `${workerName} inició “${taskName}”`,
96
+ dedupeKey: `worker_started:${taskId ?? job.id}`,
97
+ });
98
+ }
99
+
100
+ // Update TaskDoc → in_progress if we have a taskId
101
+ if (taskId) {
102
+ await updateDoc<TaskDoc>("tasks", taskId, {
103
+ status: "in_progress",
104
+ started_at: Date.now(),
105
+ job_id: job.id,
106
+ run_id: runId,
107
+ updated_at: Date.now(),
108
+ } as Partial<TaskDoc>).catch(() => {});
109
+ }
110
+
111
+ let prepared: PreparedDelegation | null = null;
112
+ try {
113
+ prepared = await prepareDelegation(workerId, {
114
+ workspace: payload.workspace ?? workerEntry.doc.workspace ?? null,
115
+ parentProviderId: payload.parentProviderId ?? null,
116
+ parentModelId: payload.parentModelId ?? null,
117
+ mcpManager,
118
+ });
119
+ // Resume from the checkpoint on a re-claimed job (payload can't know this)
120
+ const run = runId ? await getRun(runId) : null;
121
+ const resume = !!run?.state_json;
122
+ const threadId = run?.thread_id || `task-${Date.now()}-${workerId}`;
123
+
124
+ const execution = await runAgentIsolatedDetailed({
125
+ agentId: workerId,
126
+ taskDescription,
127
+ threadId,
128
+ mcpManager,
129
+ runId,
130
+ resume,
131
+ durable: !!runId,
132
+ signal,
133
+ turnId,
134
+ taskId,
135
+ userId: originUserId,
136
+ channel: originChannel,
137
+ sessionId: originSessionId,
138
+ });
139
+ const result = execution.content;
140
+
141
+ if (signal.aborted) {
142
+ agentBus.notifyTaskFailed(workerId, workerName, 0, taskName, "", "Aborted");
143
+ emitWorkEvent({
144
+ phase: "aborted",
145
+ taskRef: delegationRef,
146
+ taskName,
147
+ actorId: workerId,
148
+ targetId: parentAgentId || null,
149
+ detail: "Trabajo interrumpido",
150
+ });
151
+ if (taskId) {
152
+ await updateDoc<TaskDoc>("tasks", taskId, {
153
+ status: "pending",
154
+ updated_at: Date.now(),
155
+ } as Partial<TaskDoc>).catch(() => {});
156
+ }
157
+ return { ok: false, error: "Aborted", retryable: false };
158
+ }
159
+
160
+ const finishedRun = runId ? await getRun(runId) : null;
161
+ const resolvedAcceptance = acceptance ?? (finishedRun ? deserializeAcceptance(finishedRun) : null);
162
+ const evidence = [result, ...execution.toolEvidence];
163
+ const checks = await runAcceptanceChecks({
164
+ objective: taskDescription,
165
+ acceptance: resolvedAcceptance,
166
+ delivery: result,
167
+ evidence,
168
+ });
169
+
170
+ if (checks.status === "failed") {
171
+ await recordAgentOutcome(workerId, "harmful");
172
+ emitWorkEvent({
173
+ phase: "review_failed",
174
+ taskRef: delegationRef,
175
+ taskName,
176
+ actorId: parentAgentId || workerId,
177
+ targetId: workerId,
178
+ detail: checks.summary,
179
+ });
180
+ if (turnId && originThreadId) {
181
+ await publishNarration({
182
+ turnId,
183
+ threadId: originThreadId,
184
+ channel: originChannel,
185
+ userId: originUserId,
186
+ sessionId: originSessionId,
187
+ agentId: workerId,
188
+ agentName: workerName,
189
+ kind: "failed",
190
+ status: "error",
191
+ label: `La entrega de ${workerName} no pasó los checks automáticos`,
192
+ detail: checks.summary,
193
+ dedupeKey: `verification_failed:${taskId ?? job.id}`,
194
+ });
195
+ }
196
+ if (taskId) {
197
+ await updateDoc<TaskDoc>("tasks", taskId, {
198
+ status: "blocked",
199
+ error: checks.summary,
200
+ updated_at: Date.now(),
201
+ } as Partial<TaskDoc>).catch(() => {});
202
+ }
203
+ return {
204
+ ok: false,
205
+ error: checks.summary,
206
+ retryable: false,
207
+ };
208
+ }
209
+
210
+ await recordAgentOutcome(workerId, "helpful");
211
+ emitWorkEvent({
212
+ phase: "review_passed",
213
+ taskRef: delegationRef,
214
+ taskName,
215
+ actorId: parentAgentId || workerId,
216
+ targetId: workerId,
217
+ });
218
+ if (turnId && originThreadId) {
219
+ await publishNarration({
220
+ turnId,
221
+ threadId: originThreadId,
222
+ channel: originChannel,
223
+ userId: originUserId,
224
+ sessionId: originSessionId,
225
+ agentId: workerId,
226
+ agentName: workerName,
227
+ kind: "verified",
228
+ status: "done",
229
+ label: `${workerName} completó “${taskName}”`,
230
+ detail: checks.status === "passed" ? checks.summary : "El coordinador revisa el resultado.",
231
+ dedupeKey: `verified:${taskId ?? job.id}`,
232
+ });
233
+ }
234
+
235
+ agentBus.notifyTaskCompleted(workerId, workerName, 0, taskName, "", result);
236
+
237
+ // Update TaskDoc → completed
238
+ if (taskId) {
239
+ await updateDoc<TaskDoc>("tasks", taskId, {
240
+ status: "completed",
241
+ progress: 100,
242
+ result,
243
+ completed_at: Date.now(),
244
+ updated_at: Date.now(),
245
+ } as Partial<TaskDoc>).catch(() => {});
246
+ }
247
+
248
+ await buildProofPacket({
249
+ runId,
250
+ agentId: workerId,
251
+ intendedOutcome: taskDescription,
252
+ met: true,
253
+ checksRun: checks.results.length ? checks.results.map((r) => r.check) : ["none"],
254
+ evidence,
255
+ epoch: finishedRun ? deserializeEpoch(finishedRun) : null,
256
+ catalogAgentId: isCatalogAgent ? workerId : null,
257
+ });
258
+
259
+ emitWorkEvent({
260
+ phase: "completed",
261
+ taskRef: delegationRef,
262
+ taskName,
263
+ actorId: workerId,
264
+ targetId: parentAgentId || null,
265
+ });
266
+
267
+ return {
268
+ ok: true,
269
+ result: {
270
+ content: result,
271
+ evidence: execution.toolEvidence,
272
+ acceptance: resolvedAcceptance,
273
+ checks,
274
+ },
275
+ };
276
+ } catch (err) {
277
+ const errorMsg = (err as Error).message;
278
+ agentBus.notifyTaskFailed(workerId, workerName, 0, taskName, "", errorMsg);
279
+ emitWorkEvent({
280
+ phase: signal.aborted ? "aborted" : "failed",
281
+ taskRef: delegationRef,
282
+ taskName,
283
+ actorId: workerId,
284
+ targetId: parentAgentId || null,
285
+ detail: signal.aborted ? "Trabajo interrumpido" : errorMsg,
286
+ });
287
+ if (turnId && originThreadId) {
288
+ await publishNarration({
289
+ turnId,
290
+ threadId: originThreadId,
291
+ channel: originChannel,
292
+ userId: originUserId,
293
+ sessionId: originSessionId,
294
+ agentId: workerId,
295
+ agentName: workerName,
296
+ kind: "failed",
297
+ status: "error",
298
+ label: `${workerName} no pudo completar “${taskName}”`,
299
+ detail: errorMsg,
300
+ dedupeKey: `worker_failed:${taskId ?? job.id}`,
301
+ });
302
+ }
303
+
304
+ // Update TaskDoc → failed
305
+ if (taskId) {
306
+ await updateDoc<TaskDoc>("tasks", taskId, {
307
+ status: "failed",
308
+ error: errorMsg,
309
+ updated_at: Date.now(),
310
+ } as Partial<TaskDoc>).catch(() => {});
311
+ }
312
+
313
+ return { ok: false, error: errorMsg, retryable: isRetryableError(err) };
314
+ } finally {
315
+ emitDelegationFinished({ workerId, taskRef: delegationRef });
316
+ await prepared?.release();
317
+ }
318
+ };
319
+
320
+ // ─── goal_run executor ──────────────────────────────────────────────────────
321
+ // Multi-turn orchestration: run a turn → verify the goal → continue with the
322
+ // verifier's feedback (verifyGoal, goal-runner.ts) until the goal is met or
323
+ // the budget runs out. The goal
324
+ // AgentRun row (job.run_id) is the orchestrator record: goal_attempts,
325
+ // turns_used and tokens_used accumulate across turns and the budget is HARD.
326
+ // Turns are plain (non-durable) runs on the same thread — a mid-turn crash
327
+ // re-runs the current attempt; conversation history preserves prior progress.
328
+
329
+ const goalRunExecutor: JobExecutor = async (job, signal) => {
330
+ const payload = JSON.parse(job.payload_json);
331
+ const agentId = payload.agentId as string;
332
+ const threadId = payload.threadId as string;
333
+ const goal = payload.goal as string;
334
+ const checkTool = (payload.goal_check_tool as string | null) ?? null;
335
+ const budget = (payload.budget ?? {}) as { maxIterations?: number; maxTurns?: number; maxTokens?: number };
336
+ const maxIterationsPerTurn = budget.maxIterations ?? 20;
337
+ const maxTurns = budget.maxTurns ?? 10;
338
+ const maxTokens = budget.maxTokens ?? 200_000;
339
+ const maxAttempts = (payload.maxAttempts as number | undefined) ?? 5;
340
+ const goalRunId = job.run_id;
341
+
342
+ log.info(`[goal_run] Job ${job.id} → agent=${agentId} goal="${goal}" maxAttempts=${maxAttempts}`);
343
+
344
+ const goalRun = await getRun(goalRunId);
345
+ if (!goalRun) return { ok: false, error: `Goal run ${goalRunId} not found` };
346
+ const channel = goalRun.channel;
347
+ const notifyUserId = goalRun.user_id;
348
+
349
+ const notify = async (text: string) => {
350
+ if (channel && notifyUserId) {
351
+ await sendToUserChannel(channel, notifyUserId, text, { threadId }).catch(() => {});
352
+ }
353
+ };
354
+
355
+ const acceptance = deserializeAcceptance(goalRun);
356
+ const epoch = deserializeEpoch(goalRun);
357
+
358
+ await reclaimRun(goalRunId).catch(() => {});
359
+ startLeaseRenewal(goalRunId);
360
+
361
+ try {
362
+ let attempts = goalRun.goal_attempts ?? 0;
363
+ let lastContent = "";
364
+ let lastReason = "";
365
+
366
+ for (;;) {
367
+ if (signal.aborted) {
368
+ await interruptRun(goalRunId, "Goal run aborted").catch(() => {});
369
+ return { ok: false, error: "Aborted", retryable: false };
370
+ }
371
+
372
+ // HARD budget check against the accumulated goal run row
373
+ const current = await getRun(goalRunId);
374
+ const turnsUsed = current?.turns_used ?? 0;
375
+ const tokensUsed = current?.tokens_used ?? 0;
376
+ if (attempts >= maxAttempts || turnsUsed >= maxTurns || tokensUsed >= maxTokens) {
377
+ const summary = `intentos ${attempts}/${maxAttempts}, turnos ${turnsUsed}/${maxTurns}, tokens ${tokensUsed}/${maxTokens}`;
378
+ await failRun(goalRunId, `Goal not met — budget exhausted (${summary}). ${lastReason}`.trim());
379
+ await notify(`❌ Meta no cumplida: "${goal}". Presupuesto agotado (${summary}).${lastReason ? ` Última razón: ${lastReason}` : ""}`);
380
+ await buildProofPacket({
381
+ runId: goalRunId,
382
+ agentId,
383
+ intendedOutcome: goal,
384
+ met: false,
385
+ checksRun: acceptance ? acceptance.map((a) => a.checkTool ?? "llm_verifier") : [checkTool ?? "llm_verifier"],
386
+ evidence: [lastReason || "budget exhausted before verification succeeded"],
387
+ knownLimits: `Budget exhausted: ${summary}`,
388
+ epoch,
389
+ }).catch(() => {});
390
+ return { ok: false, error: `Goal budget exhausted (${summary})`, retryable: false };
391
+ }
392
+
393
+ const turnMessage = attempts === 0
394
+ ? `Meta: ${goal}\n\nTrabajá hasta cumplir esta meta. Explicá el resultado al terminar.`
395
+ : `La meta aún no se verificó como cumplida.\nMeta: "${goal}"\nRazón del verificador: ${lastReason}\nPresupuesto restante: ${maxAttempts - attempts} intento(s), ${maxTurns - turnsUsed} turno(s).\nContinuá trabajando para cumplirla.`;
396
+
397
+ // One turn (non-durable: the goal row carries the durable state)
398
+ let turnTokens = 0;
399
+ let turnContent = "";
400
+ for await (const chunk of runAgent({
401
+ agentId,
402
+ userMessage: turnMessage,
403
+ threadId,
404
+ signal,
405
+ mcpManager,
406
+ budget: { maxIterations: maxIterationsPerTurn },
407
+ })) {
408
+ const msgs = (chunk as any).agent?.messages;
409
+ if (msgs?.[0]?.content) turnContent = msgs[0].content;
410
+ const usage = (chunk as any).usage;
411
+ if (usage) turnTokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
412
+ }
413
+
414
+ attempts++;
415
+ lastContent = turnContent || lastContent;
416
+ await bumpTurn(goalRunId, turnTokens).catch(() => {});
417
+ await updateDoc<AgentRunDoc>("agentRuns", goalRunId, {
418
+ goal_attempts: attempts,
419
+ updated_at: Date.now(),
420
+ } as Partial<AgentRunDoc>).catch(() => {});
421
+
422
+ // Verify: deterministic tool when configured, a single LLM judgment
423
+ // otherwise (one call total, covering every acceptance criterion —
424
+ // see verifyGoal in goal-runner.ts).
425
+ const providerCfg = await resolveGoalProviderCfg(agentId);
426
+ const verdict = await verifyGoal(goal, checkTool, [
427
+ { role: "user", content: `Meta: ${goal}` },
428
+ { role: "assistant", content: turnContent || "(sin respuesta)" },
429
+ ], providerCfg, acceptance);
430
+
431
+ if (verdict.met) {
432
+ await completeRun(goalRunId, lastContent);
433
+ await notify(`✅ Meta cumplida: "${goal}". ${verdict.reason}`);
434
+ log.info(`[goal_run] Goal met after ${attempts} attempt(s): ${verdict.reason}`);
435
+ await buildProofPacket({
436
+ runId: goalRunId,
437
+ agentId,
438
+ intendedOutcome: goal,
439
+ met: true,
440
+ acceptanceResults: verdict.acceptanceResults,
441
+ checksRun: acceptance ? acceptance.map((a) => a.checkTool ?? "llm_verifier") : [checkTool ?? "llm_verifier"],
442
+ evidence: [verdict.reason, lastContent].filter(Boolean),
443
+ epoch,
444
+ catalogAgentId: null,
445
+ });
446
+ return { ok: true, result: { met: true, attempts, reason: verdict.reason, content: lastContent } };
447
+ }
448
+
449
+ lastReason = verdict.reason;
450
+ log.info(`[goal_run] Attempt ${attempts}/${maxAttempts} not met: ${verdict.reason}`);
451
+ }
452
+ } catch (err) {
453
+ await failRun(goalRunId, (err as Error).message).catch(() => {});
454
+ await notify(`❌ No pude completar la meta: "${goal}". Error: ${(err as Error).message}`);
455
+ return { ok: false, error: (err as Error).message, retryable: isRetryableError(err) };
456
+ } finally {
457
+ stopLeaseRenewal(goalRunId);
458
+ }
459
+ };
460
+
461
+ async function resolveGoalProviderCfg(agentId: string) {
462
+ const { fromIndexable } = await import("../storage/hive");
463
+ const { resolveProviderConfig, getDefaultLLM } = await import("../agent/llm-client");
464
+ const agentsCol = await col<{ provider_id?: string | null; model_id?: string | null }>("agents");
465
+ const entry = await agentsCol.get(agentId);
466
+ let providerId = entry ? fromIndexable(entry.doc.provider_id ?? null) : null;
467
+ let modelId = entry ? fromIndexable(entry.doc.model_id ?? null) : null;
468
+ if (!providerId || !modelId) {
469
+ const dflt = await getDefaultLLM();
470
+ providerId = providerId || dflt?.provider || "";
471
+ modelId = modelId || dflt?.model || "";
472
+ }
473
+ return resolveProviderConfig(providerId, modelId);
474
+ }
475
+
476
+ // ─── Register all executors ─────────────────────────────────────────────────
477
+
478
+
479
+ let initialized = false;
480
+
481
+ /**
482
+ * Registra `worker_task` y `goal_run` en la cola durable. Idempotente.
483
+ *
484
+ * No se llama sola: la app decide qué tipos de job ejecuta este proceso. Para
485
+ * un turno de chat, registrá el tuyo con `registerExecutor("chat_turn", ...)`.
486
+ */
487
+ export function initHarnessExecutors(): void {
488
+ if (initialized) return;
489
+ registerExecutor("worker_task", workerTaskExecutor);
490
+ registerExecutor("goal_run", goalRunExecutor);
491
+ initialized = true;
492
+ log.info("ejecutores del harness registrados: worker_task, goal_run");
493
+ }
@@ -11,12 +11,22 @@
11
11
  * dos estados posibles, así que ahora esto re-exporta la implementación única y el
12
12
  * subpath `@johpaz/hive-sdk/harness` sigue funcionando igual.
13
13
  *
14
- * El módulo no se cablea solo dentro del agent loop: la app decide qué significa
15
- * "durable" para sus tipos de job y registra ejecutores con `registerExecutor()`.
14
+ * La app decide qué significa "durable" para sus tipos de job. Para los dos
15
+ * casos que necesita cualquier enjambre —ejecutar un worker delegado y
16
+ * perseguir un objetivo— hay ejecutores listos: `initHarnessExecutors()`. Para
17
+ * el resto, `registerExecutor()`.
16
18
  */
17
19
 
18
20
  import { ensureHiveDb } from "../storage/bootstrap.ts";
19
21
 
22
+ // ─── Ejecutores listos para usar ─────────────────────────────────────────────
23
+ // La cola sabe encolar, reintentar y recuperar; estos son los que saben hacer
24
+ // el trabajo. Ver executors.ts para por qué `chat_turn` no está entre ellos.
25
+ export {
26
+ initHarnessExecutors,
27
+ setHarnessExecutorMCPManager,
28
+ } from "./executors.ts";
29
+
20
30
  // ─── Cola y almacén de jobs ──────────────────────────────────────────────────
21
31
  export * from "../gateway/job-store.ts";
22
32
  export * from "../gateway/durable-queue.ts";