@johpaz/hive-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +306 -0
- package/README.md +11 -3
- package/package.json +10 -4
- package/packages/core/src/agent/agent-catalog.ts +81 -24
- package/packages/core/src/agent/compaction.ts +20 -1
- package/packages/core/src/agent/context-compiler.ts +6 -3
- package/packages/core/src/agent/conversation-store.ts +136 -2
- package/packages/core/src/agent/curator.ts +12 -3
- package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
- package/packages/core/src/agent/playbook-selector.ts +18 -3
- package/packages/core/src/agent/prompt-builder.ts +2 -2
- package/packages/core/src/agent/providers/index.ts +36 -1
- package/packages/core/src/agent/reflector.ts +32 -9
- package/packages/core/src/agent/skill-selector.ts +2 -2
- package/packages/core/src/agent/thread-store.ts +43 -0
- package/packages/core/src/agent/tool-selector.ts +2 -0
- package/packages/core/src/api/createAgent.ts +66 -1
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +77 -2
- package/packages/core/src/canvas/index.ts +9 -0
- package/packages/core/src/ethics/EthicsGuard.ts +7 -1
- package/packages/core/src/events/index.ts +18 -0
- package/packages/core/src/events/tool-narration.ts +4 -0
- package/packages/core/src/gateway/channel-notify.ts +103 -6
- package/packages/core/src/gateway/durable-queue.ts +13 -1
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +6 -0
- package/packages/core/src/harness/executors.ts +493 -0
- package/packages/core/src/harness/index.ts +12 -2
- package/packages/core/src/hooks/index.ts +203 -0
- package/packages/core/src/images/index.ts +161 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/multimodal/vision-service.ts +45 -13
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/scheduler/CronScheduler.ts +48 -21
- package/packages/core/src/scheduler/cron/expression.ts +165 -0
- package/packages/core/src/scheduler/cron/index.ts +10 -0
- package/packages/core/src/scheduler/cron/job.ts +339 -0
- package/packages/core/src/scheduler/cron/next-run.ts +121 -0
- package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
- package/packages/core/src/scheduler/index.ts +21 -3
- package/packages/core/src/scheduler/integration.ts +16 -5
- package/packages/core/src/scheduler/types.ts +3 -18
- package/packages/core/src/services/agents.ts +268 -0
- package/packages/core/src/services/cron.ts +257 -0
- package/packages/core/src/services/endpoints.ts +289 -0
- package/packages/core/src/services/ethics.ts +107 -0
- package/packages/core/src/services/images.ts +212 -0
- package/packages/core/src/services/index.ts +112 -0
- package/packages/core/src/services/mcp.ts +201 -0
- package/packages/core/src/services/memory.ts +133 -0
- package/packages/core/src/services/models.ts +179 -0
- package/packages/core/src/services/providers.ts +152 -0
- package/packages/core/src/services/setup.ts +222 -0
- package/packages/core/src/services/skills.ts +241 -0
- package/packages/core/src/services/swarms.ts +307 -0
- package/packages/core/src/services/tools.ts +106 -0
- package/packages/core/src/sessions/index.ts +5 -3
- package/packages/core/src/sessions/resolve.ts +108 -0
- package/packages/core/src/skills/SkillLoader.ts +8 -1
- package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
- package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
- package/packages/core/src/skills/bundled-data.generated.ts +110 -12
- package/packages/core/src/storage/bootstrap.ts +74 -5
- package/packages/core/src/storage/collections.ts +106 -1
- package/packages/core/src/storage/crypto.ts +24 -7
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +59 -43
- package/packages/core/src/storage/reconcile.ts +6 -1
- package/packages/core/src/storage/seed.ts +89 -11
- package/packages/core/src/swarm/types.ts +3 -18
- package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
- package/packages/core/src/tool-runtime/index.ts +129 -14
- package/packages/core/src/tools/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +50 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/images/index.ts +130 -0
- package/packages/core/src/tools/index.ts +14 -1
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
- package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
- 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
|
-
*
|
|
15
|
-
*
|
|
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";
|