@hasna/loops 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/cli/index.js +1055 -23
- package/dist/daemon/index.js +975 -20
- package/dist/index.d.ts +3 -0
- package/dist/index.js +986 -19
- package/dist/lib/executor.d.ts +3 -0
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/goal/model-factory.d.ts +9 -0
- package/dist/lib/goal/prompts.d.ts +4 -0
- package/dist/lib/goal/runner.d.ts +3 -0
- package/dist/lib/goal/status.d.ts +9 -0
- package/dist/lib/goal/types.d.ts +120 -0
- package/dist/lib/store.d.ts +58 -1
- package/dist/lib/store.js +544 -5
- package/dist/lib/workflow-spec.d.ts +2 -1
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +982 -19
- package/dist/types.d.ts +10 -0
- package/package.json +5 -2
package/dist/daemon/index.js
CHANGED
|
@@ -236,6 +236,82 @@ function parseDuration(input) {
|
|
|
236
236
|
return Math.round(value * multiplier);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// src/lib/goal/types.ts
|
|
240
|
+
var GOAL_TERMINAL = ["budgetLimited", "complete", "cancelled"];
|
|
241
|
+
var GOAL_OBJECTIVE_MAX_CHARS = 4000;
|
|
242
|
+
|
|
243
|
+
// src/lib/goal/status.ts
|
|
244
|
+
function isTerminal(status) {
|
|
245
|
+
return GOAL_TERMINAL.includes(status);
|
|
246
|
+
}
|
|
247
|
+
function nodeBudgetExhausted(node) {
|
|
248
|
+
return node.tokenBudget !== undefined && node.tokensUsed >= node.tokenBudget;
|
|
249
|
+
}
|
|
250
|
+
function readyNodeKeys(plan) {
|
|
251
|
+
if (plan.status !== "active")
|
|
252
|
+
return [];
|
|
253
|
+
const byKey = new Map(plan.nodes.map((node) => [node.key, node]));
|
|
254
|
+
return plan.nodes.filter((node) => {
|
|
255
|
+
if (node.status !== "pending")
|
|
256
|
+
return false;
|
|
257
|
+
if (nodeBudgetExhausted(node))
|
|
258
|
+
return false;
|
|
259
|
+
return node.dependsOn.every((dependency) => byKey.get(dependency)?.status === "complete");
|
|
260
|
+
}).sort((a, b) => b.priority - a.priority || a.sequence - b.sequence || a.key.localeCompare(b.key)).map((node) => node.key);
|
|
261
|
+
}
|
|
262
|
+
function rollupSummary(nodes) {
|
|
263
|
+
const rollup = {
|
|
264
|
+
total: nodes.length,
|
|
265
|
+
pending: 0,
|
|
266
|
+
active: 0,
|
|
267
|
+
paused: 0,
|
|
268
|
+
blocked: 0,
|
|
269
|
+
usageLimited: 0,
|
|
270
|
+
budgetLimited: 0,
|
|
271
|
+
complete: 0,
|
|
272
|
+
cancelled: 0
|
|
273
|
+
};
|
|
274
|
+
for (const node of nodes) {
|
|
275
|
+
if (node.status in rollup) {
|
|
276
|
+
rollup[node.status] += 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return rollup;
|
|
280
|
+
}
|
|
281
|
+
function assertGoalTransition(from, to) {
|
|
282
|
+
if (isTerminal(from) && from !== to) {
|
|
283
|
+
throw new Error(`cannot transition terminal goal status ${from} to ${to}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function assertAcyclicNodes(nodes) {
|
|
287
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
288
|
+
const visiting = new Set;
|
|
289
|
+
const visited = new Set;
|
|
290
|
+
function visit(key) {
|
|
291
|
+
if (visited.has(key))
|
|
292
|
+
return;
|
|
293
|
+
if (visiting.has(key))
|
|
294
|
+
throw new Error(`goal dependency cycle includes node ${key}`);
|
|
295
|
+
const node = byKey.get(key);
|
|
296
|
+
if (!node)
|
|
297
|
+
throw new Error(`goal plan references missing node ${key}`);
|
|
298
|
+
visiting.add(key);
|
|
299
|
+
for (const dependency of node.dependsOn) {
|
|
300
|
+
if (!byKey.has(dependency))
|
|
301
|
+
throw new Error(`goal node ${key} depends on missing node ${dependency}`);
|
|
302
|
+
visit(dependency);
|
|
303
|
+
}
|
|
304
|
+
visiting.delete(key);
|
|
305
|
+
visited.add(key);
|
|
306
|
+
}
|
|
307
|
+
for (const node of nodes)
|
|
308
|
+
visit(node.key);
|
|
309
|
+
}
|
|
310
|
+
function updateReadyFlags(nodes, planStatus) {
|
|
311
|
+
const ready = new Set(readyNodeKeys({ status: planStatus, nodes }));
|
|
312
|
+
return nodes.map((node) => ({ ...node, ready: ready.has(node.key) }));
|
|
313
|
+
}
|
|
314
|
+
|
|
239
315
|
// src/lib/workflow-spec.ts
|
|
240
316
|
function assertObject(value, label) {
|
|
241
317
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -245,6 +321,35 @@ function assertString(value, label) {
|
|
|
245
321
|
if (typeof value !== "string" || value.trim() === "")
|
|
246
322
|
throw new Error(`${label} must be a non-empty string`);
|
|
247
323
|
}
|
|
324
|
+
function optionalPositiveInteger(value, label) {
|
|
325
|
+
if (value === undefined)
|
|
326
|
+
return;
|
|
327
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
328
|
+
throw new Error(`${label} must be a positive integer`);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function normalizeGoalSpec(value, label = "goal") {
|
|
332
|
+
if (value === undefined)
|
|
333
|
+
return;
|
|
334
|
+
assertObject(value, label);
|
|
335
|
+
assertString(value.objective, `${label}.objective`);
|
|
336
|
+
const objective = value.objective.trim();
|
|
337
|
+
if (objective.length > GOAL_OBJECTIVE_MAX_CHARS) {
|
|
338
|
+
throw new Error(`${label}.objective must be ${GOAL_OBJECTIVE_MAX_CHARS} characters or fewer`);
|
|
339
|
+
}
|
|
340
|
+
const autoExecute = value.autoExecute === undefined ? undefined : String(value.autoExecute);
|
|
341
|
+
if (autoExecute !== undefined && !["off", "readyOnly", "aiDirected"].includes(autoExecute)) {
|
|
342
|
+
throw new Error(`${label}.autoExecute must be off, readyOnly, or aiDirected`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
objective,
|
|
346
|
+
tokenBudget: optionalPositiveInteger(value.tokenBudget, `${label}.tokenBudget`),
|
|
347
|
+
maxTurns: optionalPositiveInteger(value.maxTurns, `${label}.maxTurns`),
|
|
348
|
+
maxTokens: optionalPositiveInteger(value.maxTokens, `${label}.maxTokens`),
|
|
349
|
+
model: typeof value.model === "string" && value.model.trim() ? value.model.trim() : undefined,
|
|
350
|
+
autoExecute
|
|
351
|
+
};
|
|
352
|
+
}
|
|
248
353
|
function validateTarget(value, label) {
|
|
249
354
|
assertObject(value, label);
|
|
250
355
|
if (value.type === "command") {
|
|
@@ -271,6 +376,7 @@ function validateTarget(value, label) {
|
|
|
271
376
|
}
|
|
272
377
|
function normalizeCreateWorkflowInput(input) {
|
|
273
378
|
assertString(input.name, "workflow.name");
|
|
379
|
+
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
274
380
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
275
381
|
throw new Error("workflow.steps must contain at least one step");
|
|
276
382
|
const seen = new Set;
|
|
@@ -283,6 +389,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
283
389
|
return {
|
|
284
390
|
...step,
|
|
285
391
|
id: step.id,
|
|
392
|
+
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
286
393
|
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
287
394
|
dependsOn: step.dependsOn ?? [],
|
|
288
395
|
continueOnFailure: step.continueOnFailure ?? false
|
|
@@ -297,7 +404,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
297
404
|
}
|
|
298
405
|
}
|
|
299
406
|
workflowExecutionOrder({ steps });
|
|
300
|
-
return { ...input, name: input.name.trim(), version: input.version ?? 1, steps };
|
|
407
|
+
return { ...input, name: input.name.trim(), goal, version: input.version ?? 1, steps };
|
|
301
408
|
}
|
|
302
409
|
function workflowExecutionOrder(workflow) {
|
|
303
410
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
@@ -333,6 +440,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
333
440
|
return normalizeCreateWorkflowInput({
|
|
334
441
|
name: rawName,
|
|
335
442
|
description: typeof value.description === "string" ? value.description : undefined,
|
|
443
|
+
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
336
444
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
337
445
|
steps: value.steps
|
|
338
446
|
});
|
|
@@ -347,6 +455,7 @@ function rowToLoop(row) {
|
|
|
347
455
|
status: row.status,
|
|
348
456
|
schedule: JSON.parse(row.schedule_json),
|
|
349
457
|
target: JSON.parse(row.target_json),
|
|
458
|
+
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
350
459
|
machine: row.machine_json ? JSON.parse(row.machine_json) : undefined,
|
|
351
460
|
nextRunAt: row.next_run_at ?? undefined,
|
|
352
461
|
retryScheduledFor: row.retry_scheduled_for ?? undefined,
|
|
@@ -379,6 +488,7 @@ function rowToRun(row) {
|
|
|
379
488
|
stdout: row.stdout ?? undefined,
|
|
380
489
|
stderr: row.stderr ?? undefined,
|
|
381
490
|
error: row.error ?? undefined,
|
|
491
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
382
492
|
createdAt: row.created_at,
|
|
383
493
|
updatedAt: row.updated_at
|
|
384
494
|
};
|
|
@@ -390,6 +500,7 @@ function rowToWorkflow(row) {
|
|
|
390
500
|
description: row.description ?? undefined,
|
|
391
501
|
version: row.version,
|
|
392
502
|
status: row.status,
|
|
503
|
+
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
393
504
|
steps: JSON.parse(row.steps_json),
|
|
394
505
|
createdAt: row.created_at,
|
|
395
506
|
updatedAt: row.updated_at
|
|
@@ -409,6 +520,7 @@ function rowToWorkflowRun(row) {
|
|
|
409
520
|
finishedAt: row.finished_at ?? undefined,
|
|
410
521
|
durationMs: row.duration_ms ?? undefined,
|
|
411
522
|
error: row.error ?? undefined,
|
|
523
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
412
524
|
createdAt: row.created_at,
|
|
413
525
|
updatedAt: row.updated_at
|
|
414
526
|
};
|
|
@@ -430,6 +542,68 @@ function rowToWorkflowStepRun(row) {
|
|
|
430
542
|
error: row.error ?? undefined,
|
|
431
543
|
accountProfile: row.account_profile ?? undefined,
|
|
432
544
|
accountTool: row.account_tool ?? undefined,
|
|
545
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
546
|
+
createdAt: row.created_at,
|
|
547
|
+
updatedAt: row.updated_at
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function rowToGoal(row) {
|
|
551
|
+
return {
|
|
552
|
+
goalId: row.id,
|
|
553
|
+
planId: row.plan_id,
|
|
554
|
+
objective: row.objective,
|
|
555
|
+
status: row.status,
|
|
556
|
+
tokenBudget: row.token_budget ?? undefined,
|
|
557
|
+
tokensUsed: row.tokens_used,
|
|
558
|
+
timeUsedSeconds: row.time_used_seconds,
|
|
559
|
+
autoExecute: row.auto_execute,
|
|
560
|
+
maxTokens: row.max_tokens ?? undefined,
|
|
561
|
+
sourceType: row.source_type ?? undefined,
|
|
562
|
+
sourceId: row.source_id ?? undefined,
|
|
563
|
+
loopId: row.loop_id ?? undefined,
|
|
564
|
+
loopRunId: row.loop_run_id ?? undefined,
|
|
565
|
+
workflowId: row.workflow_id ?? undefined,
|
|
566
|
+
workflowRunId: row.workflow_run_id ?? undefined,
|
|
567
|
+
workflowStepId: row.workflow_step_id ?? undefined,
|
|
568
|
+
createdAt: row.created_at,
|
|
569
|
+
updatedAt: row.updated_at
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function rowToGoalPlanNode(row) {
|
|
573
|
+
return {
|
|
574
|
+
nodeId: row.id,
|
|
575
|
+
planId: row.plan_id,
|
|
576
|
+
key: row.key,
|
|
577
|
+
sequence: row.sequence,
|
|
578
|
+
priority: row.priority,
|
|
579
|
+
objective: row.objective,
|
|
580
|
+
status: row.status,
|
|
581
|
+
ready: row.ready === 1,
|
|
582
|
+
tokenBudget: row.token_budget ?? undefined,
|
|
583
|
+
tokensUsed: row.tokens_used,
|
|
584
|
+
timeUsedSeconds: row.time_used_seconds,
|
|
585
|
+
dependsOn: JSON.parse(row.depends_on_json),
|
|
586
|
+
createdAt: row.created_at,
|
|
587
|
+
updatedAt: row.updated_at
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function rowToGoalRun(row) {
|
|
591
|
+
return {
|
|
592
|
+
runId: row.id,
|
|
593
|
+
goalId: row.goal_id,
|
|
594
|
+
planId: row.plan_id,
|
|
595
|
+
loopId: row.loop_id ?? undefined,
|
|
596
|
+
loopRunId: row.loop_run_id ?? undefined,
|
|
597
|
+
workflowId: row.workflow_id ?? undefined,
|
|
598
|
+
workflowRunId: row.workflow_run_id ?? undefined,
|
|
599
|
+
workflowStepId: row.workflow_step_id ?? undefined,
|
|
600
|
+
turn: row.turn,
|
|
601
|
+
phase: row.phase,
|
|
602
|
+
status: row.status,
|
|
603
|
+
nodeKey: row.node_key ?? undefined,
|
|
604
|
+
tokensUsed: row.tokens_used,
|
|
605
|
+
evidence: row.evidence_json ? JSON.parse(row.evidence_json) : undefined,
|
|
606
|
+
rawResponse: row.raw_response_json ? JSON.parse(row.raw_response_json) : undefined,
|
|
433
607
|
createdAt: row.created_at,
|
|
434
608
|
updatedAt: row.updated_at
|
|
435
609
|
};
|
|
@@ -490,6 +664,7 @@ class Store {
|
|
|
490
664
|
status TEXT NOT NULL,
|
|
491
665
|
schedule_json TEXT NOT NULL,
|
|
492
666
|
target_json TEXT NOT NULL,
|
|
667
|
+
goal_json TEXT,
|
|
493
668
|
machine_json TEXT,
|
|
494
669
|
next_run_at TEXT,
|
|
495
670
|
retry_scheduled_for TEXT,
|
|
@@ -523,6 +698,7 @@ class Store {
|
|
|
523
698
|
stdout TEXT,
|
|
524
699
|
stderr TEXT,
|
|
525
700
|
error TEXT,
|
|
701
|
+
goal_run_id TEXT,
|
|
526
702
|
created_at TEXT NOT NULL,
|
|
527
703
|
updated_at TEXT NOT NULL,
|
|
528
704
|
UNIQUE(loop_id, scheduled_for)
|
|
@@ -547,6 +723,7 @@ class Store {
|
|
|
547
723
|
description TEXT,
|
|
548
724
|
version INTEGER NOT NULL,
|
|
549
725
|
status TEXT NOT NULL,
|
|
726
|
+
goal_json TEXT,
|
|
550
727
|
steps_json TEXT NOT NULL,
|
|
551
728
|
created_at TEXT NOT NULL,
|
|
552
729
|
updated_at TEXT NOT NULL
|
|
@@ -567,6 +744,7 @@ class Store {
|
|
|
567
744
|
finished_at TEXT,
|
|
568
745
|
duration_ms INTEGER,
|
|
569
746
|
error TEXT,
|
|
747
|
+
goal_run_id TEXT,
|
|
570
748
|
created_at TEXT NOT NULL,
|
|
571
749
|
updated_at TEXT NOT NULL
|
|
572
750
|
);
|
|
@@ -593,6 +771,7 @@ class Store {
|
|
|
593
771
|
error TEXT,
|
|
594
772
|
account_profile TEXT,
|
|
595
773
|
account_tool TEXT,
|
|
774
|
+
goal_run_id TEXT,
|
|
596
775
|
created_at TEXT NOT NULL,
|
|
597
776
|
updated_at TEXT NOT NULL,
|
|
598
777
|
UNIQUE(workflow_run_id, step_id)
|
|
@@ -611,15 +790,100 @@ class Store {
|
|
|
611
790
|
UNIQUE(workflow_run_id, sequence)
|
|
612
791
|
);
|
|
613
792
|
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
793
|
+
|
|
794
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
795
|
+
id TEXT PRIMARY KEY,
|
|
796
|
+
plan_id TEXT NOT NULL,
|
|
797
|
+
objective TEXT NOT NULL,
|
|
798
|
+
status TEXT NOT NULL,
|
|
799
|
+
token_budget INTEGER,
|
|
800
|
+
tokens_used INTEGER NOT NULL,
|
|
801
|
+
time_used_seconds INTEGER NOT NULL,
|
|
802
|
+
auto_execute TEXT NOT NULL,
|
|
803
|
+
max_tokens INTEGER,
|
|
804
|
+
source_type TEXT,
|
|
805
|
+
source_id TEXT,
|
|
806
|
+
loop_id TEXT,
|
|
807
|
+
loop_run_id TEXT,
|
|
808
|
+
workflow_id TEXT,
|
|
809
|
+
workflow_run_id TEXT,
|
|
810
|
+
workflow_step_id TEXT,
|
|
811
|
+
created_at TEXT NOT NULL,
|
|
812
|
+
updated_at TEXT NOT NULL
|
|
813
|
+
);
|
|
814
|
+
CREATE INDEX IF NOT EXISTS idx_goals_status_updated ON goals(status, updated_at DESC);
|
|
815
|
+
CREATE INDEX IF NOT EXISTS idx_goals_loop_run ON goals(loop_run_id);
|
|
816
|
+
CREATE INDEX IF NOT EXISTS idx_goals_workflow_run ON goals(workflow_run_id);
|
|
817
|
+
CREATE INDEX IF NOT EXISTS idx_goals_source ON goals(source_type, source_id);
|
|
818
|
+
|
|
819
|
+
CREATE TABLE IF NOT EXISTS goal_plan_nodes (
|
|
820
|
+
id TEXT PRIMARY KEY,
|
|
821
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
822
|
+
plan_id TEXT NOT NULL,
|
|
823
|
+
key TEXT NOT NULL,
|
|
824
|
+
sequence INTEGER NOT NULL,
|
|
825
|
+
priority INTEGER NOT NULL,
|
|
826
|
+
objective TEXT NOT NULL,
|
|
827
|
+
status TEXT NOT NULL,
|
|
828
|
+
ready INTEGER NOT NULL,
|
|
829
|
+
token_budget INTEGER,
|
|
830
|
+
tokens_used INTEGER NOT NULL,
|
|
831
|
+
time_used_seconds INTEGER NOT NULL,
|
|
832
|
+
depends_on_json TEXT NOT NULL,
|
|
833
|
+
created_at TEXT NOT NULL,
|
|
834
|
+
updated_at TEXT NOT NULL,
|
|
835
|
+
UNIQUE(plan_id, key)
|
|
836
|
+
);
|
|
837
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_goal_sequence ON goal_plan_nodes(goal_id, sequence);
|
|
838
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_status ON goal_plan_nodes(status);
|
|
839
|
+
|
|
840
|
+
CREATE TABLE IF NOT EXISTS goal_runs (
|
|
841
|
+
id TEXT PRIMARY KEY,
|
|
842
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
843
|
+
plan_id TEXT NOT NULL,
|
|
844
|
+
loop_id TEXT,
|
|
845
|
+
loop_run_id TEXT,
|
|
846
|
+
workflow_id TEXT,
|
|
847
|
+
workflow_run_id TEXT,
|
|
848
|
+
workflow_step_id TEXT,
|
|
849
|
+
turn INTEGER NOT NULL,
|
|
850
|
+
phase TEXT NOT NULL,
|
|
851
|
+
status TEXT NOT NULL,
|
|
852
|
+
node_key TEXT,
|
|
853
|
+
tokens_used INTEGER NOT NULL,
|
|
854
|
+
evidence_json TEXT,
|
|
855
|
+
raw_response_json TEXT,
|
|
856
|
+
created_at TEXT NOT NULL,
|
|
857
|
+
updated_at TEXT NOT NULL
|
|
858
|
+
);
|
|
859
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_goal_created ON goal_runs(goal_id, created_at);
|
|
860
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_loop_run ON goal_runs(loop_run_id);
|
|
861
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
614
862
|
`);
|
|
615
863
|
try {
|
|
616
864
|
this.db.query("ALTER TABLE loops ADD COLUMN machine_json TEXT").run();
|
|
617
865
|
} catch {}
|
|
866
|
+
try {
|
|
867
|
+
this.db.query("ALTER TABLE loops ADD COLUMN goal_json TEXT").run();
|
|
868
|
+
} catch {}
|
|
869
|
+
try {
|
|
870
|
+
this.db.query("ALTER TABLE loop_runs ADD COLUMN goal_run_id TEXT").run();
|
|
871
|
+
} catch {}
|
|
872
|
+
try {
|
|
873
|
+
this.db.query("ALTER TABLE workflow_specs ADD COLUMN goal_json TEXT").run();
|
|
874
|
+
} catch {}
|
|
875
|
+
try {
|
|
876
|
+
this.db.query("ALTER TABLE workflow_runs ADD COLUMN goal_run_id TEXT").run();
|
|
877
|
+
} catch {}
|
|
618
878
|
try {
|
|
619
879
|
this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN pid INTEGER").run();
|
|
620
880
|
} catch {}
|
|
881
|
+
try {
|
|
882
|
+
this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN goal_run_id TEXT").run();
|
|
883
|
+
} catch {}
|
|
621
884
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
622
885
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
886
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
|
|
623
887
|
}
|
|
624
888
|
assertDaemonLeaseFence(opts = {}, now = nowIso()) {
|
|
625
889
|
if (!opts.daemonLeaseId)
|
|
@@ -637,6 +901,7 @@ class Store {
|
|
|
637
901
|
status: "active",
|
|
638
902
|
schedule: input.schedule,
|
|
639
903
|
target: input.target,
|
|
904
|
+
goal: input.goal,
|
|
640
905
|
machine: input.machine,
|
|
641
906
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
642
907
|
catchUp: input.catchUp ?? "latest",
|
|
@@ -650,8 +915,8 @@ class Store {
|
|
|
650
915
|
updatedAt: now
|
|
651
916
|
};
|
|
652
917
|
this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, machine_json, next_run_at, retry_scheduled_for,
|
|
653
|
-
catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
654
|
-
VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $catchUp, $catchUpLimit,
|
|
918
|
+
goal_json, catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
919
|
+
VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $goal, $catchUp, $catchUpLimit,
|
|
655
920
|
$overlap, $maxAttempts, $retryDelay, $leaseMs, $expiresAt, $created, $updated)`).run({
|
|
656
921
|
$id: loop.id,
|
|
657
922
|
$name: loop.name,
|
|
@@ -660,6 +925,7 @@ class Store {
|
|
|
660
925
|
$schedule: JSON.stringify(loop.schedule),
|
|
661
926
|
$target: JSON.stringify(loop.target),
|
|
662
927
|
$machine: loop.machine ? JSON.stringify(loop.machine) : null,
|
|
928
|
+
$goal: loop.goal ? JSON.stringify(loop.goal) : null,
|
|
663
929
|
$nextRun: loop.nextRunAt ?? null,
|
|
664
930
|
$catchUp: loop.catchUp,
|
|
665
931
|
$catchUpLimit: loop.catchUpLimit,
|
|
@@ -739,17 +1005,19 @@ class Store {
|
|
|
739
1005
|
description: normalized.description,
|
|
740
1006
|
version: normalized.version ?? 1,
|
|
741
1007
|
status: "active",
|
|
1008
|
+
goal: normalized.goal,
|
|
742
1009
|
steps: normalized.steps,
|
|
743
1010
|
createdAt: now,
|
|
744
1011
|
updatedAt: now
|
|
745
1012
|
};
|
|
746
|
-
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, steps_json, created_at, updated_at)
|
|
747
|
-
VALUES ($id, $name, $description, $version, $status, $steps, $created, $updated)`).run({
|
|
1013
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1014
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
748
1015
|
$id: workflow.id,
|
|
749
1016
|
$name: workflow.name,
|
|
750
1017
|
$description: workflow.description ?? null,
|
|
751
1018
|
$version: workflow.version,
|
|
752
1019
|
$status: workflow.status,
|
|
1020
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
753
1021
|
$steps: JSON.stringify(workflow.steps),
|
|
754
1022
|
$created: workflow.createdAt,
|
|
755
1023
|
$updated: workflow.updatedAt
|
|
@@ -783,6 +1051,277 @@ class Store {
|
|
|
783
1051
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
784
1052
|
return archived;
|
|
785
1053
|
}
|
|
1054
|
+
createGoal(input, opts = {}) {
|
|
1055
|
+
const now = nowIso();
|
|
1056
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1057
|
+
try {
|
|
1058
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1059
|
+
const id = genId();
|
|
1060
|
+
this.db.query(`INSERT INTO goals (id, plan_id, objective, status, token_budget, tokens_used, time_used_seconds, auto_execute,
|
|
1061
|
+
max_tokens, source_type, source_id, loop_id, loop_run_id, workflow_id, workflow_run_id, workflow_step_id,
|
|
1062
|
+
created_at, updated_at)
|
|
1063
|
+
VALUES ($id, $planId, $objective, 'active', $tokenBudget, 0, 0, $autoExecute, $maxTokens, $sourceType,
|
|
1064
|
+
$sourceId, $loopId, $loopRunId, $workflowId, $workflowRunId, $workflowStepId, $created, $updated)`).run({
|
|
1065
|
+
$id: id,
|
|
1066
|
+
$planId: id,
|
|
1067
|
+
$objective: input.objective,
|
|
1068
|
+
$tokenBudget: input.tokenBudget ?? null,
|
|
1069
|
+
$autoExecute: input.autoExecute ?? "readyOnly",
|
|
1070
|
+
$maxTokens: input.maxTokens ?? input.tokenBudget ?? null,
|
|
1071
|
+
$sourceType: input.sourceType ?? null,
|
|
1072
|
+
$sourceId: input.sourceId ?? null,
|
|
1073
|
+
$loopId: input.loopId ?? null,
|
|
1074
|
+
$loopRunId: input.loopRunId ?? null,
|
|
1075
|
+
$workflowId: input.workflowId ?? null,
|
|
1076
|
+
$workflowRunId: input.workflowRunId ?? null,
|
|
1077
|
+
$workflowStepId: input.workflowStepId ?? null,
|
|
1078
|
+
$created: now,
|
|
1079
|
+
$updated: now
|
|
1080
|
+
});
|
|
1081
|
+
this.db.exec("COMMIT");
|
|
1082
|
+
return this.requireGoal(id);
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
try {
|
|
1085
|
+
this.db.exec("ROLLBACK");
|
|
1086
|
+
} catch {}
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
getGoal(id) {
|
|
1091
|
+
const row = this.db.query("SELECT * FROM goals WHERE id = ?").get(id);
|
|
1092
|
+
return row ? rowToGoal(row) : undefined;
|
|
1093
|
+
}
|
|
1094
|
+
requireGoal(id) {
|
|
1095
|
+
const goal = this.getGoal(id);
|
|
1096
|
+
if (!goal)
|
|
1097
|
+
throw new Error(`goal not found: ${id}`);
|
|
1098
|
+
return goal;
|
|
1099
|
+
}
|
|
1100
|
+
findGoalByLoop(idOrName) {
|
|
1101
|
+
const loop = this.getLoop(idOrName) ?? this.findLoopByName(idOrName);
|
|
1102
|
+
if (!loop)
|
|
1103
|
+
return;
|
|
1104
|
+
const row = this.db.query("SELECT * FROM goals WHERE loop_id = ? ORDER BY created_at DESC LIMIT 1").get(loop.id);
|
|
1105
|
+
return row ? rowToGoal(row) : undefined;
|
|
1106
|
+
}
|
|
1107
|
+
findGoalByRunId(id) {
|
|
1108
|
+
const direct = this.getGoal(id);
|
|
1109
|
+
if (direct)
|
|
1110
|
+
return direct;
|
|
1111
|
+
const event = this.db.query("SELECT * FROM goal_runs WHERE id = ?").get(id);
|
|
1112
|
+
if (event)
|
|
1113
|
+
return this.getGoal(event.goal_id);
|
|
1114
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1115
|
+
WHERE loop_run_id = ? OR workflow_run_id = ? OR workflow_step_id = ?
|
|
1116
|
+
ORDER BY created_at DESC LIMIT 1`).get(id, id, id);
|
|
1117
|
+
return row ? rowToGoal(row) : undefined;
|
|
1118
|
+
}
|
|
1119
|
+
findGoalByContext(context) {
|
|
1120
|
+
if (context.loopRunId) {
|
|
1121
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1122
|
+
WHERE loop_run_id = ? AND (? IS NULL OR workflow_step_id = ?)
|
|
1123
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.loopRunId, context.workflowStepId ?? null, context.workflowStepId ?? null);
|
|
1124
|
+
if (row)
|
|
1125
|
+
return rowToGoal(row);
|
|
1126
|
+
}
|
|
1127
|
+
if (context.workflowRunId) {
|
|
1128
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1129
|
+
WHERE workflow_run_id = ? AND (? IS NULL OR workflow_step_id = ?)
|
|
1130
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.workflowRunId, context.workflowStepId ?? null, context.workflowStepId ?? null);
|
|
1131
|
+
if (row)
|
|
1132
|
+
return rowToGoal(row);
|
|
1133
|
+
}
|
|
1134
|
+
if (context.sourceType && context.sourceId) {
|
|
1135
|
+
const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
|
|
1136
|
+
if (row)
|
|
1137
|
+
return rowToGoal(row);
|
|
1138
|
+
}
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
listGoals(opts = {}) {
|
|
1142
|
+
const limit = opts.limit ?? 100;
|
|
1143
|
+
const rows = opts.status ? this.db.query("SELECT * FROM goals WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM goals ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1144
|
+
return rows.map(rowToGoal);
|
|
1145
|
+
}
|
|
1146
|
+
createGoalPlanNodes(goalId, nodes, opts = {}) {
|
|
1147
|
+
const goal = this.requireGoal(goalId);
|
|
1148
|
+
const now = nowIso();
|
|
1149
|
+
const materialized = nodes.map((node, sequence) => ({
|
|
1150
|
+
nodeId: genId(),
|
|
1151
|
+
planId: goal.planId,
|
|
1152
|
+
key: node.key,
|
|
1153
|
+
sequence,
|
|
1154
|
+
priority: node.priority ?? 0,
|
|
1155
|
+
objective: node.objective,
|
|
1156
|
+
status: "pending",
|
|
1157
|
+
ready: false,
|
|
1158
|
+
tokenBudget: node.tokenBudget,
|
|
1159
|
+
tokensUsed: 0,
|
|
1160
|
+
timeUsedSeconds: 0,
|
|
1161
|
+
dependsOn: node.dependsOn ?? [],
|
|
1162
|
+
createdAt: now,
|
|
1163
|
+
updatedAt: now
|
|
1164
|
+
}));
|
|
1165
|
+
const withReady = updateReadyFlags(materialized, "active");
|
|
1166
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1167
|
+
try {
|
|
1168
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1169
|
+
for (const node of withReady) {
|
|
1170
|
+
this.db.query(`INSERT OR IGNORE INTO goal_plan_nodes (id, goal_id, plan_id, key, sequence, priority, objective, status, ready,
|
|
1171
|
+
token_budget, tokens_used, time_used_seconds, depends_on_json, created_at, updated_at)
|
|
1172
|
+
VALUES ($id, $goalId, $planId, $key, $sequence, $priority, $objective, $status, $ready, $tokenBudget,
|
|
1173
|
+
$tokensUsed, $timeUsedSeconds, $dependsOn, $created, $updated)`).run({
|
|
1174
|
+
$id: node.nodeId,
|
|
1175
|
+
$goalId: goal.goalId,
|
|
1176
|
+
$planId: goal.planId,
|
|
1177
|
+
$key: node.key,
|
|
1178
|
+
$sequence: node.sequence,
|
|
1179
|
+
$priority: node.priority,
|
|
1180
|
+
$objective: node.objective,
|
|
1181
|
+
$status: node.status,
|
|
1182
|
+
$ready: node.ready ? 1 : 0,
|
|
1183
|
+
$tokenBudget: node.tokenBudget ?? null,
|
|
1184
|
+
$tokensUsed: node.tokensUsed,
|
|
1185
|
+
$timeUsedSeconds: node.timeUsedSeconds,
|
|
1186
|
+
$dependsOn: JSON.stringify(node.dependsOn),
|
|
1187
|
+
$created: node.createdAt,
|
|
1188
|
+
$updated: node.updatedAt
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
this.db.exec("COMMIT");
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
try {
|
|
1194
|
+
this.db.exec("ROLLBACK");
|
|
1195
|
+
} catch {}
|
|
1196
|
+
throw error;
|
|
1197
|
+
}
|
|
1198
|
+
return this.listGoalPlanNodes(goalId);
|
|
1199
|
+
}
|
|
1200
|
+
listGoalPlanNodes(goalIdOrPlanId) {
|
|
1201
|
+
const rows = this.db.query("SELECT * FROM goal_plan_nodes WHERE goal_id = ? OR plan_id = ? ORDER BY sequence ASC").all(goalIdOrPlanId, goalIdOrPlanId);
|
|
1202
|
+
return rows.map(rowToGoalPlanNode);
|
|
1203
|
+
}
|
|
1204
|
+
updateGoalStatus(goalId, status, opts = {}) {
|
|
1205
|
+
const current = this.requireGoal(goalId);
|
|
1206
|
+
assertGoalTransition(current.status, status);
|
|
1207
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1208
|
+
this.db.query(`UPDATE goals SET status=$status, updated_at=$updated
|
|
1209
|
+
WHERE id=$id
|
|
1210
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1211
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1212
|
+
))`).run({
|
|
1213
|
+
$id: goalId,
|
|
1214
|
+
$status: status,
|
|
1215
|
+
$updated: now,
|
|
1216
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1217
|
+
$now: now
|
|
1218
|
+
});
|
|
1219
|
+
return this.requireGoal(goalId);
|
|
1220
|
+
}
|
|
1221
|
+
addGoalUsage(goalId, tokens, timeUsedSeconds = 0, opts = {}) {
|
|
1222
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1223
|
+
this.db.query(`UPDATE goals
|
|
1224
|
+
SET tokens_used=tokens_used + $tokens, time_used_seconds=time_used_seconds + $seconds, updated_at=$updated
|
|
1225
|
+
WHERE id=$id
|
|
1226
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1227
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1228
|
+
))`).run({
|
|
1229
|
+
$id: goalId,
|
|
1230
|
+
$tokens: tokens,
|
|
1231
|
+
$seconds: timeUsedSeconds,
|
|
1232
|
+
$updated: now,
|
|
1233
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1234
|
+
$now: now
|
|
1235
|
+
});
|
|
1236
|
+
return this.requireGoal(goalId);
|
|
1237
|
+
}
|
|
1238
|
+
updateGoalPlanNode(goalId, key, patch, opts = {}) {
|
|
1239
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1240
|
+
this.db.query(`UPDATE goal_plan_nodes
|
|
1241
|
+
SET status=COALESCE($status, status),
|
|
1242
|
+
tokens_used=COALESCE($tokensUsed, tokens_used),
|
|
1243
|
+
time_used_seconds=COALESCE($timeUsedSeconds, time_used_seconds),
|
|
1244
|
+
ready=COALESCE($ready, ready),
|
|
1245
|
+
updated_at=$updated
|
|
1246
|
+
WHERE goal_id=$goalId AND key=$key
|
|
1247
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1248
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1249
|
+
))`).run({
|
|
1250
|
+
$goalId: goalId,
|
|
1251
|
+
$key: key,
|
|
1252
|
+
$status: patch.status ?? null,
|
|
1253
|
+
$tokensUsed: patch.tokensUsed ?? null,
|
|
1254
|
+
$timeUsedSeconds: patch.timeUsedSeconds ?? null,
|
|
1255
|
+
$ready: patch.ready === undefined ? null : patch.ready ? 1 : 0,
|
|
1256
|
+
$updated: now,
|
|
1257
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1258
|
+
$now: now
|
|
1259
|
+
});
|
|
1260
|
+
const node = this.listGoalPlanNodes(goalId).find((entry) => entry.key === key);
|
|
1261
|
+
if (!node)
|
|
1262
|
+
throw new Error(`goal node not found: ${goalId}/${key}`);
|
|
1263
|
+
return node;
|
|
1264
|
+
}
|
|
1265
|
+
recordGoalEvent(input, opts = {}) {
|
|
1266
|
+
const goal = this.requireGoal(input.goalId);
|
|
1267
|
+
const now = nowIso();
|
|
1268
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1269
|
+
try {
|
|
1270
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1271
|
+
const previous = this.db.query("SELECT MAX(turn) AS turn FROM goal_runs WHERE goal_id = ?").get(goal.goalId);
|
|
1272
|
+
const turn = input.turn ?? (previous?.turn ?? 0) + 1;
|
|
1273
|
+
const id = genId();
|
|
1274
|
+
this.db.query(`INSERT INTO goal_runs (id, goal_id, plan_id, loop_id, loop_run_id, workflow_id, workflow_run_id, workflow_step_id,
|
|
1275
|
+
turn, phase, status, node_key, tokens_used, evidence_json, raw_response_json, created_at, updated_at)
|
|
1276
|
+
VALUES ($id, $goalId, $planId, $loopId, $loopRunId, $workflowId, $workflowRunId, $workflowStepId,
|
|
1277
|
+
$turn, $phase, $status, $nodeKey, $tokensUsed, $evidence, $rawResponse, $created, $updated)`).run({
|
|
1278
|
+
$id: id,
|
|
1279
|
+
$goalId: goal.goalId,
|
|
1280
|
+
$planId: goal.planId,
|
|
1281
|
+
$loopId: goal.loopId ?? null,
|
|
1282
|
+
$loopRunId: goal.loopRunId ?? null,
|
|
1283
|
+
$workflowId: goal.workflowId ?? null,
|
|
1284
|
+
$workflowRunId: goal.workflowRunId ?? null,
|
|
1285
|
+
$workflowStepId: goal.workflowStepId ?? null,
|
|
1286
|
+
$turn: turn,
|
|
1287
|
+
$phase: input.phase,
|
|
1288
|
+
$status: input.status,
|
|
1289
|
+
$nodeKey: input.nodeKey ?? null,
|
|
1290
|
+
$tokensUsed: input.tokensUsed ?? 0,
|
|
1291
|
+
$evidence: input.evidence ? JSON.stringify(input.evidence) : null,
|
|
1292
|
+
$rawResponse: input.rawResponse === undefined ? null : JSON.stringify(input.rawResponse),
|
|
1293
|
+
$created: now,
|
|
1294
|
+
$updated: now
|
|
1295
|
+
});
|
|
1296
|
+
if (input.tokensUsed && input.tokensUsed > 0) {
|
|
1297
|
+
this.db.query("UPDATE goals SET tokens_used=tokens_used + ?, updated_at=? WHERE id=?").run(input.tokensUsed, now, goal.goalId);
|
|
1298
|
+
}
|
|
1299
|
+
this.db.exec("COMMIT");
|
|
1300
|
+
const event = this.db.query("SELECT * FROM goal_runs WHERE id = ?").get(id);
|
|
1301
|
+
if (!event)
|
|
1302
|
+
throw new Error(`goal run not found after record: ${id}`);
|
|
1303
|
+
return rowToGoalRun(event);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
try {
|
|
1306
|
+
this.db.exec("ROLLBACK");
|
|
1307
|
+
} catch {}
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
listGoalRuns(opts = {}) {
|
|
1312
|
+
const limit = opts.limit ?? 200;
|
|
1313
|
+
let rows;
|
|
1314
|
+
if (opts.goalId) {
|
|
1315
|
+
rows = this.db.query("SELECT * FROM goal_runs WHERE goal_id = ? ORDER BY created_at ASC LIMIT ?").all(opts.goalId, limit);
|
|
1316
|
+
} else if (opts.runId) {
|
|
1317
|
+
rows = this.db.query(`SELECT * FROM goal_runs
|
|
1318
|
+
WHERE id = ? OR loop_run_id = ? OR workflow_run_id = ?
|
|
1319
|
+
ORDER BY created_at ASC LIMIT ?`).all(opts.runId, opts.runId, opts.runId, limit);
|
|
1320
|
+
} else {
|
|
1321
|
+
rows = this.db.query("SELECT * FROM goal_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1322
|
+
}
|
|
1323
|
+
return rows.map(rowToGoalRun);
|
|
1324
|
+
}
|
|
786
1325
|
createWorkflowRun(input) {
|
|
787
1326
|
const now = nowIso();
|
|
788
1327
|
if (input.idempotencyKey) {
|
|
@@ -1519,6 +2058,7 @@ import { spawn as spawn2 } from "child_process";
|
|
|
1519
2058
|
|
|
1520
2059
|
// src/lib/executor.ts
|
|
1521
2060
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
2061
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1522
2062
|
import { once } from "events";
|
|
1523
2063
|
import { resolveMachineCommand } from "@hasna/machines/consumer";
|
|
1524
2064
|
|
|
@@ -1814,6 +2354,12 @@ function metadataEnv(metadata) {
|
|
|
1814
2354
|
env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
|
|
1815
2355
|
if (metadata.workflowStepId)
|
|
1816
2356
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
2357
|
+
if (metadata.goalId)
|
|
2358
|
+
env.LOOPS_GOAL_ID = metadata.goalId;
|
|
2359
|
+
if (metadata.goalObjective)
|
|
2360
|
+
env.LOOPS_GOAL_OBJECTIVE = metadata.goalObjective;
|
|
2361
|
+
if (metadata.goalNodeKey)
|
|
2362
|
+
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
1817
2363
|
return env;
|
|
1818
2364
|
}
|
|
1819
2365
|
function providerCommand(provider) {
|
|
@@ -1951,9 +2497,9 @@ function commandForShell(spec) {
|
|
|
1951
2497
|
return [spec.command, ...spec.args.map(shellQuote)].join(" ");
|
|
1952
2498
|
}
|
|
1953
2499
|
function hereDoc(value) {
|
|
1954
|
-
let delimiter2 = `__OPENLOOPS_STDIN_${
|
|
2500
|
+
let delimiter2 = `__OPENLOOPS_STDIN_${randomBytes2(8).toString("hex").toUpperCase()}__`;
|
|
1955
2501
|
while (value.split(/\r?\n/).includes(delimiter2)) {
|
|
1956
|
-
delimiter2 = `__OPENLOOPS_STDIN_${
|
|
2502
|
+
delimiter2 = `__OPENLOOPS_STDIN_${randomBytes2(8).toString("hex").toUpperCase()}__`;
|
|
1957
2503
|
}
|
|
1958
2504
|
return [`cat > "$__OPENLOOPS_STDIN" <<'${delimiter2}'`, value, delimiter2];
|
|
1959
2505
|
}
|
|
@@ -1984,7 +2530,7 @@ function remoteScript(spec, metadata) {
|
|
|
1984
2530
|
lines.push(...hereDoc(spec.stdin));
|
|
1985
2531
|
stdinRedirect = ' < "$__OPENLOOPS_STDIN"';
|
|
1986
2532
|
}
|
|
1987
|
-
const invocation = spec.shell ? `sh -
|
|
2533
|
+
const invocation = spec.shell ? `sh -c ${shellQuote(commandForShell(spec))}${stdinRedirect}` : `${[spec.command, ...spec.args].map(shellQuote).join(" ")}${stdinRedirect}`;
|
|
1988
2534
|
lines.push(invocation);
|
|
1989
2535
|
return `${lines.join(`
|
|
1990
2536
|
`)}
|
|
@@ -2277,6 +2823,346 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
2277
2823
|
}, { ...opts, machine: opts.machine ?? loop.machine });
|
|
2278
2824
|
}
|
|
2279
2825
|
|
|
2826
|
+
// src/lib/goal/runner.ts
|
|
2827
|
+
import { generateObject } from "ai";
|
|
2828
|
+
import { z } from "zod";
|
|
2829
|
+
|
|
2830
|
+
// src/lib/goal/model-factory.ts
|
|
2831
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
2832
|
+
var DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
2833
|
+
function resolveGoalModel(opts = {}) {
|
|
2834
|
+
const env = opts.env ?? process.env;
|
|
2835
|
+
const apiKey = opts.apiKey ?? env.OPENROUTER_API_KEY;
|
|
2836
|
+
if (!apiKey) {
|
|
2837
|
+
throw new Error("OPENROUTER_API_KEY is required to run goals with the OpenRouter AI SDK provider");
|
|
2838
|
+
}
|
|
2839
|
+
const provider = createOpenRouter({
|
|
2840
|
+
apiKey,
|
|
2841
|
+
baseURL: opts.baseURL ?? env.LOOPS_GOAL_BASE_URL ?? env.OPENROUTER_BASE_URL
|
|
2842
|
+
});
|
|
2843
|
+
return provider.chat(opts.model ?? env.LOOPS_GOAL_MODEL ?? DEFAULT_GOAL_MODEL);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/lib/goal/prompts.ts
|
|
2847
|
+
function planPrompt(spec) {
|
|
2848
|
+
const budget = spec.tokenBudget ? `Token budget: ${spec.tokenBudget}.` : "No explicit token budget.";
|
|
2849
|
+
return [
|
|
2850
|
+
"Create a flat DAG goal plan for this objective.",
|
|
2851
|
+
"Each node must have a stable short key, a concrete objective, optional dependsOn keys, and optional priority.",
|
|
2852
|
+
"Prefer the smallest plan that can prove the explicit requirements.",
|
|
2853
|
+
budget,
|
|
2854
|
+
`Objective: ${spec.objective}`
|
|
2855
|
+
].join(`
|
|
2856
|
+
`);
|
|
2857
|
+
}
|
|
2858
|
+
function achievementPrompt(goal, nodes, evidence) {
|
|
2859
|
+
return [
|
|
2860
|
+
"Run an adversarial achievement audit.",
|
|
2861
|
+
"Completion is unproven until every explicit requirement is verified against evidence.",
|
|
2862
|
+
"Return achieved=false if evidence is missing, ambiguous, or only asserts completion.",
|
|
2863
|
+
"adversarialReview must be non-empty and must describe the attempted falsification.",
|
|
2864
|
+
`Goal: ${goal.objective}`,
|
|
2865
|
+
`Nodes: ${nodes.map((node) => `${node.key}=${node.status}`).join(", ")}`,
|
|
2866
|
+
`Evidence:
|
|
2867
|
+
${evidence.join(`
|
|
2868
|
+
---
|
|
2869
|
+
`) || "(none)"}`
|
|
2870
|
+
].join(`
|
|
2871
|
+
`);
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// src/lib/goal/runner.ts
|
|
2875
|
+
var DEFAULT_MAX_TURNS = 10;
|
|
2876
|
+
var PlanNodeSchema = z.object({
|
|
2877
|
+
key: z.string().min(1).max(64).regex(/^[A-Za-z0-9_.-]+$/),
|
|
2878
|
+
objective: z.string().min(1),
|
|
2879
|
+
dependsOn: z.array(z.string().min(1)).optional().default([]),
|
|
2880
|
+
priority: z.number().int().optional().default(0),
|
|
2881
|
+
tokenBudget: z.number().int().positive().optional()
|
|
2882
|
+
});
|
|
2883
|
+
var PlanSchema = z.object({
|
|
2884
|
+
nodes: z.array(PlanNodeSchema).min(1)
|
|
2885
|
+
});
|
|
2886
|
+
var AchievementSchema = z.object({
|
|
2887
|
+
achieved: z.boolean(),
|
|
2888
|
+
status: z.enum(["active", "blocked", "budgetLimited", "complete", "cancelled"]).optional(),
|
|
2889
|
+
evidence: z.array(z.string()).optional().default([]),
|
|
2890
|
+
unmetRequirements: z.array(z.string()).optional().default([]),
|
|
2891
|
+
adversarialReview: z.string().min(1)
|
|
2892
|
+
});
|
|
2893
|
+
function normalizeGoalSpec2(spec) {
|
|
2894
|
+
const objective = spec.objective.trim();
|
|
2895
|
+
if (!objective)
|
|
2896
|
+
throw new Error("goal.objective must be a non-empty string");
|
|
2897
|
+
if (objective.length > GOAL_OBJECTIVE_MAX_CHARS) {
|
|
2898
|
+
throw new Error(`goal.objective must be ${GOAL_OBJECTIVE_MAX_CHARS} characters or fewer`);
|
|
2899
|
+
}
|
|
2900
|
+
return { ...spec, objective, autoExecute: spec.autoExecute ?? "readyOnly" };
|
|
2901
|
+
}
|
|
2902
|
+
function usageTotal(value) {
|
|
2903
|
+
const usage = value;
|
|
2904
|
+
const input = typeof usage.inputTokens === "number" ? usage.inputTokens : usage.inputTokens?.total ?? 0;
|
|
2905
|
+
const output = typeof usage.outputTokens === "number" ? usage.outputTokens : usage.outputTokens?.total ?? 0;
|
|
2906
|
+
return usage.totalTokens ?? input + output;
|
|
2907
|
+
}
|
|
2908
|
+
function resultFromGoal(goal, status, stdout, error, startedAt = goal.createdAt) {
|
|
2909
|
+
const finishedAt = nowIso();
|
|
2910
|
+
return {
|
|
2911
|
+
status,
|
|
2912
|
+
exitCode: status === "succeeded" ? 0 : 1,
|
|
2913
|
+
stdout,
|
|
2914
|
+
stderr: "",
|
|
2915
|
+
error,
|
|
2916
|
+
startedAt,
|
|
2917
|
+
finishedAt,
|
|
2918
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
2919
|
+
goalId: goal.goalId
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
function budgetExhausted(goal) {
|
|
2923
|
+
return goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget;
|
|
2924
|
+
}
|
|
2925
|
+
function sameBlockerKey(values) {
|
|
2926
|
+
return values.map((value) => value.trim()).filter(Boolean).join(`
|
|
2927
|
+
`) || "goal completion remains unproven";
|
|
2928
|
+
}
|
|
2929
|
+
function metadataFor(goal, node, context) {
|
|
2930
|
+
return {
|
|
2931
|
+
loopId: context?.loopId,
|
|
2932
|
+
loopName: context?.loopName,
|
|
2933
|
+
runId: context?.loopRunId,
|
|
2934
|
+
scheduledFor: context?.scheduledFor,
|
|
2935
|
+
workflowId: context?.workflowId,
|
|
2936
|
+
workflowName: context?.workflowName,
|
|
2937
|
+
workflowRunId: context?.workflowRunId,
|
|
2938
|
+
workflowStepId: context?.workflowStepId,
|
|
2939
|
+
goalId: goal.goalId,
|
|
2940
|
+
goalObjective: goal.objective,
|
|
2941
|
+
goalNodeKey: node.key
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
async function executeUnderlyingTarget(target, goal, node, opts) {
|
|
2945
|
+
const metadata = metadataFor(goal, node, opts.context);
|
|
2946
|
+
if (opts.executeNode)
|
|
2947
|
+
return opts.executeNode(node, metadata);
|
|
2948
|
+
if (!target)
|
|
2949
|
+
throw new Error("runGoal requires either target or executeNode");
|
|
2950
|
+
return executeTarget(target, metadata, {
|
|
2951
|
+
env: opts.env,
|
|
2952
|
+
daemonLeaseId: opts.daemonLeaseId,
|
|
2953
|
+
beforePersist: opts.beforePersist,
|
|
2954
|
+
signal: opts.signal
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
async function planGoal(store, goal, spec, model, opts) {
|
|
2958
|
+
const existing = store.listGoalPlanNodes(goal.goalId);
|
|
2959
|
+
if (existing.length > 0)
|
|
2960
|
+
return existing;
|
|
2961
|
+
const planned = await generateObject({
|
|
2962
|
+
model,
|
|
2963
|
+
schema: PlanSchema,
|
|
2964
|
+
temperature: 0,
|
|
2965
|
+
prompt: planPrompt(spec),
|
|
2966
|
+
abortSignal: opts.signal
|
|
2967
|
+
});
|
|
2968
|
+
const tokens = usageTotal(planned.usage);
|
|
2969
|
+
const rawNodes = planned.object.nodes.map((node, index) => ({
|
|
2970
|
+
key: node.key,
|
|
2971
|
+
objective: node.objective,
|
|
2972
|
+
dependsOn: node.dependsOn ?? [],
|
|
2973
|
+
priority: node.priority ?? 0,
|
|
2974
|
+
tokenBudget: node.tokenBudget,
|
|
2975
|
+
sequence: index
|
|
2976
|
+
}));
|
|
2977
|
+
assertAcyclicNodes(rawNodes.map((node) => ({ key: node.key, dependsOn: node.dependsOn })));
|
|
2978
|
+
store.recordGoalEvent({
|
|
2979
|
+
goalId: goal.goalId,
|
|
2980
|
+
turn: 0,
|
|
2981
|
+
phase: "plan",
|
|
2982
|
+
status: "active",
|
|
2983
|
+
tokensUsed: tokens,
|
|
2984
|
+
evidence: { nodeCount: rawNodes.length },
|
|
2985
|
+
rawResponse: planned.object
|
|
2986
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
2987
|
+
return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
|
|
2988
|
+
}
|
|
2989
|
+
function stdoutFor(goal, nodes, evidence, validation) {
|
|
2990
|
+
return JSON.stringify({
|
|
2991
|
+
goal,
|
|
2992
|
+
rollup: rollupSummary(nodes),
|
|
2993
|
+
nodes,
|
|
2994
|
+
evidence,
|
|
2995
|
+
validation
|
|
2996
|
+
}, null, 2);
|
|
2997
|
+
}
|
|
2998
|
+
async function runGoal(store, input, opts = {}) {
|
|
2999
|
+
const spec = normalizeGoalSpec2(input);
|
|
3000
|
+
const model = opts.model ?? resolveGoalModel({ model: spec.model, env: opts.env });
|
|
3001
|
+
const startedAt = nowIso();
|
|
3002
|
+
const existing = store.findGoalByContext({
|
|
3003
|
+
loopRunId: opts.context?.loopRunId,
|
|
3004
|
+
workflowRunId: opts.context?.workflowRunId,
|
|
3005
|
+
workflowStepId: opts.context?.workflowStepId,
|
|
3006
|
+
sourceType: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : "manual",
|
|
3007
|
+
sourceId: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : spec.objective
|
|
3008
|
+
});
|
|
3009
|
+
let goal = existing ?? store.createGoal({
|
|
3010
|
+
objective: spec.objective,
|
|
3011
|
+
tokenBudget: spec.tokenBudget,
|
|
3012
|
+
autoExecute: spec.autoExecute,
|
|
3013
|
+
maxTokens: spec.maxTokens ?? spec.tokenBudget,
|
|
3014
|
+
sourceType: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : "manual",
|
|
3015
|
+
sourceId: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : spec.objective,
|
|
3016
|
+
loopId: opts.context?.loopId,
|
|
3017
|
+
loopRunId: opts.context?.loopRunId,
|
|
3018
|
+
workflowId: opts.context?.workflowId,
|
|
3019
|
+
workflowRunId: opts.context?.workflowRunId,
|
|
3020
|
+
workflowStepId: opts.context?.workflowStepId
|
|
3021
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3022
|
+
let nodes = await planGoal(store, goal, spec, model, opts);
|
|
3023
|
+
goal = store.requireGoal(goal.goalId);
|
|
3024
|
+
const evidence = [];
|
|
3025
|
+
let validation;
|
|
3026
|
+
let lastBlocker = "";
|
|
3027
|
+
let repeatedBlockerCount = 0;
|
|
3028
|
+
if (budgetExhausted(goal)) {
|
|
3029
|
+
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3030
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
|
|
3031
|
+
}
|
|
3032
|
+
for (let turn = 1;turn <= (spec.maxTurns ?? DEFAULT_MAX_TURNS); turn++) {
|
|
3033
|
+
if (opts.signal?.aborted) {
|
|
3034
|
+
goal = store.updateGoalStatus(goal.goalId, "cancelled", { daemonLeaseId: opts.daemonLeaseId });
|
|
3035
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
|
|
3036
|
+
}
|
|
3037
|
+
goal = store.requireGoal(goal.goalId);
|
|
3038
|
+
nodes = store.listGoalPlanNodes(goal.goalId);
|
|
3039
|
+
if (budgetExhausted(goal)) {
|
|
3040
|
+
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3041
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
|
|
3042
|
+
}
|
|
3043
|
+
const readyKeys = readyNodeKeys({
|
|
3044
|
+
status: goal.status === "active" ? "active" : goal.status === "budgetLimited" ? "budgetLimited" : "blocked",
|
|
3045
|
+
nodes
|
|
3046
|
+
});
|
|
3047
|
+
if (readyKeys.length > 0) {
|
|
3048
|
+
for (const key of readyKeys) {
|
|
3049
|
+
const node = store.listGoalPlanNodes(goal.goalId).find((entry) => entry.key === key);
|
|
3050
|
+
if (!node || node.status !== "pending")
|
|
3051
|
+
continue;
|
|
3052
|
+
opts.beforePersist?.();
|
|
3053
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { status: "active", ready: false }, { daemonLeaseId: opts.daemonLeaseId });
|
|
3054
|
+
const result = await executeUnderlyingTarget(opts.target, goal, node, opts);
|
|
3055
|
+
store.recordGoalEvent({
|
|
3056
|
+
goalId: goal.goalId,
|
|
3057
|
+
turn,
|
|
3058
|
+
phase: "execute",
|
|
3059
|
+
status: result.status === "succeeded" ? "complete" : "active",
|
|
3060
|
+
nodeKey: node.key,
|
|
3061
|
+
evidence: {
|
|
3062
|
+
status: result.status,
|
|
3063
|
+
exitCode: result.exitCode,
|
|
3064
|
+
stdout: result.stdout,
|
|
3065
|
+
stderr: result.stderr,
|
|
3066
|
+
error: result.error
|
|
3067
|
+
}
|
|
3068
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3069
|
+
if (result.status === "succeeded") {
|
|
3070
|
+
evidence.push(`node ${node.key} succeeded
|
|
3071
|
+
stdout:
|
|
3072
|
+
${result.stdout}
|
|
3073
|
+
stderr:
|
|
3074
|
+
${result.stderr}`);
|
|
3075
|
+
store.updateGoalPlanNode(goal.goalId, node.key, {
|
|
3076
|
+
status: "complete",
|
|
3077
|
+
timeUsedSeconds: Math.round(result.durationMs / 1000)
|
|
3078
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
const blocker2 = `node ${node.key} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
3082
|
+
if (blocker2 === lastBlocker)
|
|
3083
|
+
repeatedBlockerCount += 1;
|
|
3084
|
+
else {
|
|
3085
|
+
lastBlocker = blocker2;
|
|
3086
|
+
repeatedBlockerCount = 1;
|
|
3087
|
+
}
|
|
3088
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { status: repeatedBlockerCount >= 3 ? "blocked" : "pending" }, {
|
|
3089
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
3090
|
+
});
|
|
3091
|
+
if (repeatedBlockerCount >= 3) {
|
|
3092
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3093
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence), blocker2, startedAt);
|
|
3094
|
+
}
|
|
3095
|
+
break;
|
|
3096
|
+
}
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
if (nodes.every((node) => node.status === "complete")) {
|
|
3100
|
+
const judged = await generateObject({
|
|
3101
|
+
model,
|
|
3102
|
+
schema: AchievementSchema,
|
|
3103
|
+
temperature: 0,
|
|
3104
|
+
prompt: achievementPrompt(goal, nodes, evidence),
|
|
3105
|
+
abortSignal: opts.signal
|
|
3106
|
+
});
|
|
3107
|
+
const tokens = usageTotal(judged.usage);
|
|
3108
|
+
validation = judged.object;
|
|
3109
|
+
const achieved = judged.object.achieved && judged.object.adversarialReview.trim().length > 0;
|
|
3110
|
+
const unmet = achieved ? [] : judged.object.unmetRequirements.length > 0 ? judged.object.unmetRequirements : ["adversarial review did not prove completion"];
|
|
3111
|
+
store.recordGoalEvent({
|
|
3112
|
+
goalId: goal.goalId,
|
|
3113
|
+
turn,
|
|
3114
|
+
phase: "validate",
|
|
3115
|
+
status: achieved ? "complete" : "blocked",
|
|
3116
|
+
tokensUsed: tokens,
|
|
3117
|
+
evidence: {
|
|
3118
|
+
achieved,
|
|
3119
|
+
evidence: judged.object.evidence,
|
|
3120
|
+
unmetRequirements: unmet,
|
|
3121
|
+
adversarialReview: judged.object.adversarialReview
|
|
3122
|
+
},
|
|
3123
|
+
rawResponse: judged.object
|
|
3124
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3125
|
+
goal = store.requireGoal(goal.goalId);
|
|
3126
|
+
if (achieved) {
|
|
3127
|
+
goal = store.updateGoalStatus(goal.goalId, "complete", { daemonLeaseId: opts.daemonLeaseId });
|
|
3128
|
+
return resultFromGoal(goal, "succeeded", stdoutFor(goal, nodes, evidence, validation), undefined, startedAt);
|
|
3129
|
+
}
|
|
3130
|
+
const blocker2 = sameBlockerKey(unmet);
|
|
3131
|
+
if (blocker2 === lastBlocker)
|
|
3132
|
+
repeatedBlockerCount += 1;
|
|
3133
|
+
else {
|
|
3134
|
+
lastBlocker = blocker2;
|
|
3135
|
+
repeatedBlockerCount = 1;
|
|
3136
|
+
}
|
|
3137
|
+
if (repeatedBlockerCount >= 3) {
|
|
3138
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3139
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker2, startedAt);
|
|
3140
|
+
}
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
const blocker = "no ready goal nodes and goal plan is incomplete";
|
|
3144
|
+
if (blocker === lastBlocker)
|
|
3145
|
+
repeatedBlockerCount += 1;
|
|
3146
|
+
else {
|
|
3147
|
+
lastBlocker = blocker;
|
|
3148
|
+
repeatedBlockerCount = 1;
|
|
3149
|
+
}
|
|
3150
|
+
store.recordGoalEvent({
|
|
3151
|
+
goalId: goal.goalId,
|
|
3152
|
+
turn,
|
|
3153
|
+
phase: "status",
|
|
3154
|
+
status: repeatedBlockerCount >= 3 ? "blocked" : "active",
|
|
3155
|
+
evidence: { blocker }
|
|
3156
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3157
|
+
if (repeatedBlockerCount >= 3) {
|
|
3158
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3159
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3163
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation), "goal max turns exhausted", startedAt);
|
|
3164
|
+
}
|
|
3165
|
+
|
|
2280
3166
|
// src/lib/workflow-runner.ts
|
|
2281
3167
|
function targetWithStepAccount(step) {
|
|
2282
3168
|
const account = step.account ?? step.target.account;
|
|
@@ -2299,6 +3185,24 @@ function workflowResult(workflowRun, status, startedAt, finishedAt, stdout, erro
|
|
|
2299
3185
|
};
|
|
2300
3186
|
}
|
|
2301
3187
|
async function executeWorkflow(store, workflow, opts = {}) {
|
|
3188
|
+
if (workflow.goal) {
|
|
3189
|
+
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
3190
|
+
return runGoal(store, workflow.goal, {
|
|
3191
|
+
...opts,
|
|
3192
|
+
context: {
|
|
3193
|
+
loopId: opts.loop?.id,
|
|
3194
|
+
loopName: opts.loop?.name,
|
|
3195
|
+
loopRunId: opts.loopRun?.id,
|
|
3196
|
+
scheduledFor: opts.loopRun?.scheduledFor ?? opts.scheduledFor,
|
|
3197
|
+
workflowId: workflow.id,
|
|
3198
|
+
workflowName: workflow.name
|
|
3199
|
+
},
|
|
3200
|
+
executeNode: async (node) => executeWorkflow(store, workflowWithoutGoal, {
|
|
3201
|
+
...opts,
|
|
3202
|
+
idempotencyKey: `${opts.idempotencyKey ?? workflow.id}:goal:${node.key}`
|
|
3203
|
+
})
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
2302
3206
|
const run = store.createWorkflowRun({
|
|
2303
3207
|
workflow,
|
|
2304
3208
|
loop: opts.loop,
|
|
@@ -2374,16 +3278,34 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
2374
3278
|
}, opts.cancelPollMs ?? 500);
|
|
2375
3279
|
cancelTimer.unref();
|
|
2376
3280
|
try {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
3281
|
+
if (step.goal) {
|
|
3282
|
+
result = await runGoal(store, step.goal, {
|
|
3283
|
+
...opts,
|
|
3284
|
+
target: targetWithStepAccount(step),
|
|
3285
|
+
signal: controller.signal,
|
|
3286
|
+
context: {
|
|
3287
|
+
loopId: opts.loop?.id,
|
|
3288
|
+
loopName: opts.loop?.name,
|
|
3289
|
+
loopRunId: opts.loopRun?.id,
|
|
3290
|
+
scheduledFor: opts.loopRun?.scheduledFor ?? opts.scheduledFor,
|
|
3291
|
+
workflowId: workflow.id,
|
|
3292
|
+
workflowName: workflow.name,
|
|
3293
|
+
workflowRunId: run.id,
|
|
3294
|
+
workflowStepId: step.id
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
} else {
|
|
3298
|
+
result = await executeTarget(targetWithStepAccount(step), metadata, {
|
|
3299
|
+
...opts,
|
|
3300
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
3301
|
+
signal: controller.signal,
|
|
3302
|
+
onSpawn: (pid) => {
|
|
3303
|
+
opts.beforePersist?.();
|
|
3304
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
3305
|
+
opts.onSpawn?.(pid);
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
2387
3309
|
} catch (error) {
|
|
2388
3310
|
const finishedAt2 = nowIso();
|
|
2389
3311
|
result = {
|
|
@@ -2461,9 +3383,42 @@ function preflightWorkflow(workflow, opts = {}) {
|
|
|
2461
3383
|
}, opts));
|
|
2462
3384
|
}
|
|
2463
3385
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
2464
|
-
if (loop.target.type !== "workflow")
|
|
3386
|
+
if (loop.target.type !== "workflow") {
|
|
3387
|
+
if (loop.goal) {
|
|
3388
|
+
return runGoal(store, loop.goal, {
|
|
3389
|
+
...opts,
|
|
3390
|
+
target: loop.target,
|
|
3391
|
+
context: {
|
|
3392
|
+
loopId: loop.id,
|
|
3393
|
+
loopName: loop.name,
|
|
3394
|
+
loopRunId: run.id,
|
|
3395
|
+
scheduledFor: run.scheduledFor
|
|
3396
|
+
}
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
2465
3399
|
return executeLoop(loop, run, opts);
|
|
3400
|
+
}
|
|
2466
3401
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
3402
|
+
if (loop.goal) {
|
|
3403
|
+
return runGoal(store, loop.goal, {
|
|
3404
|
+
...opts,
|
|
3405
|
+
context: {
|
|
3406
|
+
loopId: loop.id,
|
|
3407
|
+
loopName: loop.name,
|
|
3408
|
+
loopRunId: run.id,
|
|
3409
|
+
scheduledFor: run.scheduledFor,
|
|
3410
|
+
workflowId: workflow.id,
|
|
3411
|
+
workflowName: workflow.name
|
|
3412
|
+
},
|
|
3413
|
+
executeNode: async (node) => executeWorkflow(store, workflow, {
|
|
3414
|
+
...opts,
|
|
3415
|
+
loop,
|
|
3416
|
+
loopRun: run,
|
|
3417
|
+
scheduledFor: run.scheduledFor,
|
|
3418
|
+
idempotencyKey: `${loop.id}:${run.scheduledFor}:attempt:${run.attempt}:goal:${node.key}`
|
|
3419
|
+
})
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
2467
3422
|
const controller = loop.target.timeoutMs ? new AbortController : undefined;
|
|
2468
3423
|
let workflowTimedOut = false;
|
|
2469
3424
|
const externalAbort = () => controller?.abort();
|
|
@@ -3061,7 +4016,7 @@ function enableStartup(result) {
|
|
|
3061
4016
|
|
|
3062
4017
|
// src/daemon/index.ts
|
|
3063
4018
|
var program = new Command;
|
|
3064
|
-
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.
|
|
4019
|
+
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.6");
|
|
3065
4020
|
program.command("run").option("--interval-ms <ms>", "tick interval", (value) => Number(value)).action(async (opts) => runDaemon({ intervalMs: opts.intervalMs }));
|
|
3066
4021
|
program.command("start").action(async () => {
|
|
3067
4022
|
const result = await startDaemon({ cliEntry: process.argv[1] ?? "loops-daemon", args: ["run"] });
|