@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/index.js
CHANGED
|
@@ -234,6 +234,82 @@ function parseDuration(input) {
|
|
|
234
234
|
return Math.round(value * multiplier);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// src/lib/goal/types.ts
|
|
238
|
+
var GOAL_TERMINAL = ["budgetLimited", "complete", "cancelled"];
|
|
239
|
+
var GOAL_OBJECTIVE_MAX_CHARS = 4000;
|
|
240
|
+
|
|
241
|
+
// src/lib/goal/status.ts
|
|
242
|
+
function isTerminal(status) {
|
|
243
|
+
return GOAL_TERMINAL.includes(status);
|
|
244
|
+
}
|
|
245
|
+
function nodeBudgetExhausted(node) {
|
|
246
|
+
return node.tokenBudget !== undefined && node.tokensUsed >= node.tokenBudget;
|
|
247
|
+
}
|
|
248
|
+
function readyNodeKeys(plan) {
|
|
249
|
+
if (plan.status !== "active")
|
|
250
|
+
return [];
|
|
251
|
+
const byKey = new Map(plan.nodes.map((node) => [node.key, node]));
|
|
252
|
+
return plan.nodes.filter((node) => {
|
|
253
|
+
if (node.status !== "pending")
|
|
254
|
+
return false;
|
|
255
|
+
if (nodeBudgetExhausted(node))
|
|
256
|
+
return false;
|
|
257
|
+
return node.dependsOn.every((dependency) => byKey.get(dependency)?.status === "complete");
|
|
258
|
+
}).sort((a, b) => b.priority - a.priority || a.sequence - b.sequence || a.key.localeCompare(b.key)).map((node) => node.key);
|
|
259
|
+
}
|
|
260
|
+
function rollupSummary(nodes) {
|
|
261
|
+
const rollup = {
|
|
262
|
+
total: nodes.length,
|
|
263
|
+
pending: 0,
|
|
264
|
+
active: 0,
|
|
265
|
+
paused: 0,
|
|
266
|
+
blocked: 0,
|
|
267
|
+
usageLimited: 0,
|
|
268
|
+
budgetLimited: 0,
|
|
269
|
+
complete: 0,
|
|
270
|
+
cancelled: 0
|
|
271
|
+
};
|
|
272
|
+
for (const node of nodes) {
|
|
273
|
+
if (node.status in rollup) {
|
|
274
|
+
rollup[node.status] += 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return rollup;
|
|
278
|
+
}
|
|
279
|
+
function assertGoalTransition(from, to) {
|
|
280
|
+
if (isTerminal(from) && from !== to) {
|
|
281
|
+
throw new Error(`cannot transition terminal goal status ${from} to ${to}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function assertAcyclicNodes(nodes) {
|
|
285
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
286
|
+
const visiting = new Set;
|
|
287
|
+
const visited = new Set;
|
|
288
|
+
function visit(key) {
|
|
289
|
+
if (visited.has(key))
|
|
290
|
+
return;
|
|
291
|
+
if (visiting.has(key))
|
|
292
|
+
throw new Error(`goal dependency cycle includes node ${key}`);
|
|
293
|
+
const node = byKey.get(key);
|
|
294
|
+
if (!node)
|
|
295
|
+
throw new Error(`goal plan references missing node ${key}`);
|
|
296
|
+
visiting.add(key);
|
|
297
|
+
for (const dependency of node.dependsOn) {
|
|
298
|
+
if (!byKey.has(dependency))
|
|
299
|
+
throw new Error(`goal node ${key} depends on missing node ${dependency}`);
|
|
300
|
+
visit(dependency);
|
|
301
|
+
}
|
|
302
|
+
visiting.delete(key);
|
|
303
|
+
visited.add(key);
|
|
304
|
+
}
|
|
305
|
+
for (const node of nodes)
|
|
306
|
+
visit(node.key);
|
|
307
|
+
}
|
|
308
|
+
function updateReadyFlags(nodes, planStatus) {
|
|
309
|
+
const ready = new Set(readyNodeKeys({ status: planStatus, nodes }));
|
|
310
|
+
return nodes.map((node) => ({ ...node, ready: ready.has(node.key) }));
|
|
311
|
+
}
|
|
312
|
+
|
|
237
313
|
// src/lib/workflow-spec.ts
|
|
238
314
|
function assertObject(value, label) {
|
|
239
315
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -243,6 +319,35 @@ function assertString(value, label) {
|
|
|
243
319
|
if (typeof value !== "string" || value.trim() === "")
|
|
244
320
|
throw new Error(`${label} must be a non-empty string`);
|
|
245
321
|
}
|
|
322
|
+
function optionalPositiveInteger(value, label) {
|
|
323
|
+
if (value === undefined)
|
|
324
|
+
return;
|
|
325
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
326
|
+
throw new Error(`${label} must be a positive integer`);
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
function normalizeGoalSpec(value, label = "goal") {
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return;
|
|
332
|
+
assertObject(value, label);
|
|
333
|
+
assertString(value.objective, `${label}.objective`);
|
|
334
|
+
const objective = value.objective.trim();
|
|
335
|
+
if (objective.length > GOAL_OBJECTIVE_MAX_CHARS) {
|
|
336
|
+
throw new Error(`${label}.objective must be ${GOAL_OBJECTIVE_MAX_CHARS} characters or fewer`);
|
|
337
|
+
}
|
|
338
|
+
const autoExecute = value.autoExecute === undefined ? undefined : String(value.autoExecute);
|
|
339
|
+
if (autoExecute !== undefined && !["off", "readyOnly", "aiDirected"].includes(autoExecute)) {
|
|
340
|
+
throw new Error(`${label}.autoExecute must be off, readyOnly, or aiDirected`);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
objective,
|
|
344
|
+
tokenBudget: optionalPositiveInteger(value.tokenBudget, `${label}.tokenBudget`),
|
|
345
|
+
maxTurns: optionalPositiveInteger(value.maxTurns, `${label}.maxTurns`),
|
|
346
|
+
maxTokens: optionalPositiveInteger(value.maxTokens, `${label}.maxTokens`),
|
|
347
|
+
model: typeof value.model === "string" && value.model.trim() ? value.model.trim() : undefined,
|
|
348
|
+
autoExecute
|
|
349
|
+
};
|
|
350
|
+
}
|
|
246
351
|
function validateTarget(value, label) {
|
|
247
352
|
assertObject(value, label);
|
|
248
353
|
if (value.type === "command") {
|
|
@@ -269,6 +374,7 @@ function validateTarget(value, label) {
|
|
|
269
374
|
}
|
|
270
375
|
function normalizeCreateWorkflowInput(input) {
|
|
271
376
|
assertString(input.name, "workflow.name");
|
|
377
|
+
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
272
378
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
273
379
|
throw new Error("workflow.steps must contain at least one step");
|
|
274
380
|
const seen = new Set;
|
|
@@ -281,6 +387,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
281
387
|
return {
|
|
282
388
|
...step,
|
|
283
389
|
id: step.id,
|
|
390
|
+
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
284
391
|
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
285
392
|
dependsOn: step.dependsOn ?? [],
|
|
286
393
|
continueOnFailure: step.continueOnFailure ?? false
|
|
@@ -295,7 +402,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
295
402
|
}
|
|
296
403
|
}
|
|
297
404
|
workflowExecutionOrder({ steps });
|
|
298
|
-
return { ...input, name: input.name.trim(), version: input.version ?? 1, steps };
|
|
405
|
+
return { ...input, name: input.name.trim(), goal, version: input.version ?? 1, steps };
|
|
299
406
|
}
|
|
300
407
|
function workflowExecutionOrder(workflow) {
|
|
301
408
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
@@ -331,6 +438,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
331
438
|
return normalizeCreateWorkflowInput({
|
|
332
439
|
name: rawName,
|
|
333
440
|
description: typeof value.description === "string" ? value.description : undefined,
|
|
441
|
+
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
334
442
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
335
443
|
steps: value.steps
|
|
336
444
|
});
|
|
@@ -345,6 +453,7 @@ function rowToLoop(row) {
|
|
|
345
453
|
status: row.status,
|
|
346
454
|
schedule: JSON.parse(row.schedule_json),
|
|
347
455
|
target: JSON.parse(row.target_json),
|
|
456
|
+
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
348
457
|
machine: row.machine_json ? JSON.parse(row.machine_json) : undefined,
|
|
349
458
|
nextRunAt: row.next_run_at ?? undefined,
|
|
350
459
|
retryScheduledFor: row.retry_scheduled_for ?? undefined,
|
|
@@ -377,6 +486,7 @@ function rowToRun(row) {
|
|
|
377
486
|
stdout: row.stdout ?? undefined,
|
|
378
487
|
stderr: row.stderr ?? undefined,
|
|
379
488
|
error: row.error ?? undefined,
|
|
489
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
380
490
|
createdAt: row.created_at,
|
|
381
491
|
updatedAt: row.updated_at
|
|
382
492
|
};
|
|
@@ -388,6 +498,7 @@ function rowToWorkflow(row) {
|
|
|
388
498
|
description: row.description ?? undefined,
|
|
389
499
|
version: row.version,
|
|
390
500
|
status: row.status,
|
|
501
|
+
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
391
502
|
steps: JSON.parse(row.steps_json),
|
|
392
503
|
createdAt: row.created_at,
|
|
393
504
|
updatedAt: row.updated_at
|
|
@@ -407,6 +518,7 @@ function rowToWorkflowRun(row) {
|
|
|
407
518
|
finishedAt: row.finished_at ?? undefined,
|
|
408
519
|
durationMs: row.duration_ms ?? undefined,
|
|
409
520
|
error: row.error ?? undefined,
|
|
521
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
410
522
|
createdAt: row.created_at,
|
|
411
523
|
updatedAt: row.updated_at
|
|
412
524
|
};
|
|
@@ -428,6 +540,68 @@ function rowToWorkflowStepRun(row) {
|
|
|
428
540
|
error: row.error ?? undefined,
|
|
429
541
|
accountProfile: row.account_profile ?? undefined,
|
|
430
542
|
accountTool: row.account_tool ?? undefined,
|
|
543
|
+
goalRunId: row.goal_run_id ?? undefined,
|
|
544
|
+
createdAt: row.created_at,
|
|
545
|
+
updatedAt: row.updated_at
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function rowToGoal(row) {
|
|
549
|
+
return {
|
|
550
|
+
goalId: row.id,
|
|
551
|
+
planId: row.plan_id,
|
|
552
|
+
objective: row.objective,
|
|
553
|
+
status: row.status,
|
|
554
|
+
tokenBudget: row.token_budget ?? undefined,
|
|
555
|
+
tokensUsed: row.tokens_used,
|
|
556
|
+
timeUsedSeconds: row.time_used_seconds,
|
|
557
|
+
autoExecute: row.auto_execute,
|
|
558
|
+
maxTokens: row.max_tokens ?? undefined,
|
|
559
|
+
sourceType: row.source_type ?? undefined,
|
|
560
|
+
sourceId: row.source_id ?? undefined,
|
|
561
|
+
loopId: row.loop_id ?? undefined,
|
|
562
|
+
loopRunId: row.loop_run_id ?? undefined,
|
|
563
|
+
workflowId: row.workflow_id ?? undefined,
|
|
564
|
+
workflowRunId: row.workflow_run_id ?? undefined,
|
|
565
|
+
workflowStepId: row.workflow_step_id ?? undefined,
|
|
566
|
+
createdAt: row.created_at,
|
|
567
|
+
updatedAt: row.updated_at
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function rowToGoalPlanNode(row) {
|
|
571
|
+
return {
|
|
572
|
+
nodeId: row.id,
|
|
573
|
+
planId: row.plan_id,
|
|
574
|
+
key: row.key,
|
|
575
|
+
sequence: row.sequence,
|
|
576
|
+
priority: row.priority,
|
|
577
|
+
objective: row.objective,
|
|
578
|
+
status: row.status,
|
|
579
|
+
ready: row.ready === 1,
|
|
580
|
+
tokenBudget: row.token_budget ?? undefined,
|
|
581
|
+
tokensUsed: row.tokens_used,
|
|
582
|
+
timeUsedSeconds: row.time_used_seconds,
|
|
583
|
+
dependsOn: JSON.parse(row.depends_on_json),
|
|
584
|
+
createdAt: row.created_at,
|
|
585
|
+
updatedAt: row.updated_at
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function rowToGoalRun(row) {
|
|
589
|
+
return {
|
|
590
|
+
runId: row.id,
|
|
591
|
+
goalId: row.goal_id,
|
|
592
|
+
planId: row.plan_id,
|
|
593
|
+
loopId: row.loop_id ?? undefined,
|
|
594
|
+
loopRunId: row.loop_run_id ?? undefined,
|
|
595
|
+
workflowId: row.workflow_id ?? undefined,
|
|
596
|
+
workflowRunId: row.workflow_run_id ?? undefined,
|
|
597
|
+
workflowStepId: row.workflow_step_id ?? undefined,
|
|
598
|
+
turn: row.turn,
|
|
599
|
+
phase: row.phase,
|
|
600
|
+
status: row.status,
|
|
601
|
+
nodeKey: row.node_key ?? undefined,
|
|
602
|
+
tokensUsed: row.tokens_used,
|
|
603
|
+
evidence: row.evidence_json ? JSON.parse(row.evidence_json) : undefined,
|
|
604
|
+
rawResponse: row.raw_response_json ? JSON.parse(row.raw_response_json) : undefined,
|
|
431
605
|
createdAt: row.created_at,
|
|
432
606
|
updatedAt: row.updated_at
|
|
433
607
|
};
|
|
@@ -488,6 +662,7 @@ class Store {
|
|
|
488
662
|
status TEXT NOT NULL,
|
|
489
663
|
schedule_json TEXT NOT NULL,
|
|
490
664
|
target_json TEXT NOT NULL,
|
|
665
|
+
goal_json TEXT,
|
|
491
666
|
machine_json TEXT,
|
|
492
667
|
next_run_at TEXT,
|
|
493
668
|
retry_scheduled_for TEXT,
|
|
@@ -521,6 +696,7 @@ class Store {
|
|
|
521
696
|
stdout TEXT,
|
|
522
697
|
stderr TEXT,
|
|
523
698
|
error TEXT,
|
|
699
|
+
goal_run_id TEXT,
|
|
524
700
|
created_at TEXT NOT NULL,
|
|
525
701
|
updated_at TEXT NOT NULL,
|
|
526
702
|
UNIQUE(loop_id, scheduled_for)
|
|
@@ -545,6 +721,7 @@ class Store {
|
|
|
545
721
|
description TEXT,
|
|
546
722
|
version INTEGER NOT NULL,
|
|
547
723
|
status TEXT NOT NULL,
|
|
724
|
+
goal_json TEXT,
|
|
548
725
|
steps_json TEXT NOT NULL,
|
|
549
726
|
created_at TEXT NOT NULL,
|
|
550
727
|
updated_at TEXT NOT NULL
|
|
@@ -565,6 +742,7 @@ class Store {
|
|
|
565
742
|
finished_at TEXT,
|
|
566
743
|
duration_ms INTEGER,
|
|
567
744
|
error TEXT,
|
|
745
|
+
goal_run_id TEXT,
|
|
568
746
|
created_at TEXT NOT NULL,
|
|
569
747
|
updated_at TEXT NOT NULL
|
|
570
748
|
);
|
|
@@ -591,6 +769,7 @@ class Store {
|
|
|
591
769
|
error TEXT,
|
|
592
770
|
account_profile TEXT,
|
|
593
771
|
account_tool TEXT,
|
|
772
|
+
goal_run_id TEXT,
|
|
594
773
|
created_at TEXT NOT NULL,
|
|
595
774
|
updated_at TEXT NOT NULL,
|
|
596
775
|
UNIQUE(workflow_run_id, step_id)
|
|
@@ -609,15 +788,100 @@ class Store {
|
|
|
609
788
|
UNIQUE(workflow_run_id, sequence)
|
|
610
789
|
);
|
|
611
790
|
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
791
|
+
|
|
792
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
793
|
+
id TEXT PRIMARY KEY,
|
|
794
|
+
plan_id TEXT NOT NULL,
|
|
795
|
+
objective TEXT NOT NULL,
|
|
796
|
+
status TEXT NOT NULL,
|
|
797
|
+
token_budget INTEGER,
|
|
798
|
+
tokens_used INTEGER NOT NULL,
|
|
799
|
+
time_used_seconds INTEGER NOT NULL,
|
|
800
|
+
auto_execute TEXT NOT NULL,
|
|
801
|
+
max_tokens INTEGER,
|
|
802
|
+
source_type TEXT,
|
|
803
|
+
source_id TEXT,
|
|
804
|
+
loop_id TEXT,
|
|
805
|
+
loop_run_id TEXT,
|
|
806
|
+
workflow_id TEXT,
|
|
807
|
+
workflow_run_id TEXT,
|
|
808
|
+
workflow_step_id TEXT,
|
|
809
|
+
created_at TEXT NOT NULL,
|
|
810
|
+
updated_at TEXT NOT NULL
|
|
811
|
+
);
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_goals_status_updated ON goals(status, updated_at DESC);
|
|
813
|
+
CREATE INDEX IF NOT EXISTS idx_goals_loop_run ON goals(loop_run_id);
|
|
814
|
+
CREATE INDEX IF NOT EXISTS idx_goals_workflow_run ON goals(workflow_run_id);
|
|
815
|
+
CREATE INDEX IF NOT EXISTS idx_goals_source ON goals(source_type, source_id);
|
|
816
|
+
|
|
817
|
+
CREATE TABLE IF NOT EXISTS goal_plan_nodes (
|
|
818
|
+
id TEXT PRIMARY KEY,
|
|
819
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
820
|
+
plan_id TEXT NOT NULL,
|
|
821
|
+
key TEXT NOT NULL,
|
|
822
|
+
sequence INTEGER NOT NULL,
|
|
823
|
+
priority INTEGER NOT NULL,
|
|
824
|
+
objective TEXT NOT NULL,
|
|
825
|
+
status TEXT NOT NULL,
|
|
826
|
+
ready INTEGER NOT NULL,
|
|
827
|
+
token_budget INTEGER,
|
|
828
|
+
tokens_used INTEGER NOT NULL,
|
|
829
|
+
time_used_seconds INTEGER NOT NULL,
|
|
830
|
+
depends_on_json TEXT NOT NULL,
|
|
831
|
+
created_at TEXT NOT NULL,
|
|
832
|
+
updated_at TEXT NOT NULL,
|
|
833
|
+
UNIQUE(plan_id, key)
|
|
834
|
+
);
|
|
835
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_goal_sequence ON goal_plan_nodes(goal_id, sequence);
|
|
836
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_status ON goal_plan_nodes(status);
|
|
837
|
+
|
|
838
|
+
CREATE TABLE IF NOT EXISTS goal_runs (
|
|
839
|
+
id TEXT PRIMARY KEY,
|
|
840
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
841
|
+
plan_id TEXT NOT NULL,
|
|
842
|
+
loop_id TEXT,
|
|
843
|
+
loop_run_id TEXT,
|
|
844
|
+
workflow_id TEXT,
|
|
845
|
+
workflow_run_id TEXT,
|
|
846
|
+
workflow_step_id TEXT,
|
|
847
|
+
turn INTEGER NOT NULL,
|
|
848
|
+
phase TEXT NOT NULL,
|
|
849
|
+
status TEXT NOT NULL,
|
|
850
|
+
node_key TEXT,
|
|
851
|
+
tokens_used INTEGER NOT NULL,
|
|
852
|
+
evidence_json TEXT,
|
|
853
|
+
raw_response_json TEXT,
|
|
854
|
+
created_at TEXT NOT NULL,
|
|
855
|
+
updated_at TEXT NOT NULL
|
|
856
|
+
);
|
|
857
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_goal_created ON goal_runs(goal_id, created_at);
|
|
858
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_loop_run ON goal_runs(loop_run_id);
|
|
859
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
612
860
|
`);
|
|
613
861
|
try {
|
|
614
862
|
this.db.query("ALTER TABLE loops ADD COLUMN machine_json TEXT").run();
|
|
615
863
|
} catch {}
|
|
864
|
+
try {
|
|
865
|
+
this.db.query("ALTER TABLE loops ADD COLUMN goal_json TEXT").run();
|
|
866
|
+
} catch {}
|
|
867
|
+
try {
|
|
868
|
+
this.db.query("ALTER TABLE loop_runs ADD COLUMN goal_run_id TEXT").run();
|
|
869
|
+
} catch {}
|
|
870
|
+
try {
|
|
871
|
+
this.db.query("ALTER TABLE workflow_specs ADD COLUMN goal_json TEXT").run();
|
|
872
|
+
} catch {}
|
|
873
|
+
try {
|
|
874
|
+
this.db.query("ALTER TABLE workflow_runs ADD COLUMN goal_run_id TEXT").run();
|
|
875
|
+
} catch {}
|
|
616
876
|
try {
|
|
617
877
|
this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN pid INTEGER").run();
|
|
618
878
|
} catch {}
|
|
879
|
+
try {
|
|
880
|
+
this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN goal_run_id TEXT").run();
|
|
881
|
+
} catch {}
|
|
619
882
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
620
883
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
884
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
|
|
621
885
|
}
|
|
622
886
|
assertDaemonLeaseFence(opts = {}, now = nowIso()) {
|
|
623
887
|
if (!opts.daemonLeaseId)
|
|
@@ -635,6 +899,7 @@ class Store {
|
|
|
635
899
|
status: "active",
|
|
636
900
|
schedule: input.schedule,
|
|
637
901
|
target: input.target,
|
|
902
|
+
goal: input.goal,
|
|
638
903
|
machine: input.machine,
|
|
639
904
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
640
905
|
catchUp: input.catchUp ?? "latest",
|
|
@@ -648,8 +913,8 @@ class Store {
|
|
|
648
913
|
updatedAt: now
|
|
649
914
|
};
|
|
650
915
|
this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, machine_json, next_run_at, retry_scheduled_for,
|
|
651
|
-
catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
652
|
-
VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $catchUp, $catchUpLimit,
|
|
916
|
+
goal_json, catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
917
|
+
VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $goal, $catchUp, $catchUpLimit,
|
|
653
918
|
$overlap, $maxAttempts, $retryDelay, $leaseMs, $expiresAt, $created, $updated)`).run({
|
|
654
919
|
$id: loop.id,
|
|
655
920
|
$name: loop.name,
|
|
@@ -658,6 +923,7 @@ class Store {
|
|
|
658
923
|
$schedule: JSON.stringify(loop.schedule),
|
|
659
924
|
$target: JSON.stringify(loop.target),
|
|
660
925
|
$machine: loop.machine ? JSON.stringify(loop.machine) : null,
|
|
926
|
+
$goal: loop.goal ? JSON.stringify(loop.goal) : null,
|
|
661
927
|
$nextRun: loop.nextRunAt ?? null,
|
|
662
928
|
$catchUp: loop.catchUp,
|
|
663
929
|
$catchUpLimit: loop.catchUpLimit,
|
|
@@ -737,17 +1003,19 @@ class Store {
|
|
|
737
1003
|
description: normalized.description,
|
|
738
1004
|
version: normalized.version ?? 1,
|
|
739
1005
|
status: "active",
|
|
1006
|
+
goal: normalized.goal,
|
|
740
1007
|
steps: normalized.steps,
|
|
741
1008
|
createdAt: now,
|
|
742
1009
|
updatedAt: now
|
|
743
1010
|
};
|
|
744
|
-
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, steps_json, created_at, updated_at)
|
|
745
|
-
VALUES ($id, $name, $description, $version, $status, $steps, $created, $updated)`).run({
|
|
1011
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1012
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
746
1013
|
$id: workflow.id,
|
|
747
1014
|
$name: workflow.name,
|
|
748
1015
|
$description: workflow.description ?? null,
|
|
749
1016
|
$version: workflow.version,
|
|
750
1017
|
$status: workflow.status,
|
|
1018
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
751
1019
|
$steps: JSON.stringify(workflow.steps),
|
|
752
1020
|
$created: workflow.createdAt,
|
|
753
1021
|
$updated: workflow.updatedAt
|
|
@@ -781,6 +1049,277 @@ class Store {
|
|
|
781
1049
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
782
1050
|
return archived;
|
|
783
1051
|
}
|
|
1052
|
+
createGoal(input, opts = {}) {
|
|
1053
|
+
const now = nowIso();
|
|
1054
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1055
|
+
try {
|
|
1056
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1057
|
+
const id = genId();
|
|
1058
|
+
this.db.query(`INSERT INTO goals (id, plan_id, objective, status, token_budget, tokens_used, time_used_seconds, auto_execute,
|
|
1059
|
+
max_tokens, source_type, source_id, loop_id, loop_run_id, workflow_id, workflow_run_id, workflow_step_id,
|
|
1060
|
+
created_at, updated_at)
|
|
1061
|
+
VALUES ($id, $planId, $objective, 'active', $tokenBudget, 0, 0, $autoExecute, $maxTokens, $sourceType,
|
|
1062
|
+
$sourceId, $loopId, $loopRunId, $workflowId, $workflowRunId, $workflowStepId, $created, $updated)`).run({
|
|
1063
|
+
$id: id,
|
|
1064
|
+
$planId: id,
|
|
1065
|
+
$objective: input.objective,
|
|
1066
|
+
$tokenBudget: input.tokenBudget ?? null,
|
|
1067
|
+
$autoExecute: input.autoExecute ?? "readyOnly",
|
|
1068
|
+
$maxTokens: input.maxTokens ?? input.tokenBudget ?? null,
|
|
1069
|
+
$sourceType: input.sourceType ?? null,
|
|
1070
|
+
$sourceId: input.sourceId ?? null,
|
|
1071
|
+
$loopId: input.loopId ?? null,
|
|
1072
|
+
$loopRunId: input.loopRunId ?? null,
|
|
1073
|
+
$workflowId: input.workflowId ?? null,
|
|
1074
|
+
$workflowRunId: input.workflowRunId ?? null,
|
|
1075
|
+
$workflowStepId: input.workflowStepId ?? null,
|
|
1076
|
+
$created: now,
|
|
1077
|
+
$updated: now
|
|
1078
|
+
});
|
|
1079
|
+
this.db.exec("COMMIT");
|
|
1080
|
+
return this.requireGoal(id);
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
try {
|
|
1083
|
+
this.db.exec("ROLLBACK");
|
|
1084
|
+
} catch {}
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
getGoal(id) {
|
|
1089
|
+
const row = this.db.query("SELECT * FROM goals WHERE id = ?").get(id);
|
|
1090
|
+
return row ? rowToGoal(row) : undefined;
|
|
1091
|
+
}
|
|
1092
|
+
requireGoal(id) {
|
|
1093
|
+
const goal = this.getGoal(id);
|
|
1094
|
+
if (!goal)
|
|
1095
|
+
throw new Error(`goal not found: ${id}`);
|
|
1096
|
+
return goal;
|
|
1097
|
+
}
|
|
1098
|
+
findGoalByLoop(idOrName) {
|
|
1099
|
+
const loop = this.getLoop(idOrName) ?? this.findLoopByName(idOrName);
|
|
1100
|
+
if (!loop)
|
|
1101
|
+
return;
|
|
1102
|
+
const row = this.db.query("SELECT * FROM goals WHERE loop_id = ? ORDER BY created_at DESC LIMIT 1").get(loop.id);
|
|
1103
|
+
return row ? rowToGoal(row) : undefined;
|
|
1104
|
+
}
|
|
1105
|
+
findGoalByRunId(id) {
|
|
1106
|
+
const direct = this.getGoal(id);
|
|
1107
|
+
if (direct)
|
|
1108
|
+
return direct;
|
|
1109
|
+
const event = this.db.query("SELECT * FROM goal_runs WHERE id = ?").get(id);
|
|
1110
|
+
if (event)
|
|
1111
|
+
return this.getGoal(event.goal_id);
|
|
1112
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1113
|
+
WHERE loop_run_id = ? OR workflow_run_id = ? OR workflow_step_id = ?
|
|
1114
|
+
ORDER BY created_at DESC LIMIT 1`).get(id, id, id);
|
|
1115
|
+
return row ? rowToGoal(row) : undefined;
|
|
1116
|
+
}
|
|
1117
|
+
findGoalByContext(context) {
|
|
1118
|
+
if (context.loopRunId) {
|
|
1119
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1120
|
+
WHERE loop_run_id = ? AND (? IS NULL OR workflow_step_id = ?)
|
|
1121
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.loopRunId, context.workflowStepId ?? null, context.workflowStepId ?? null);
|
|
1122
|
+
if (row)
|
|
1123
|
+
return rowToGoal(row);
|
|
1124
|
+
}
|
|
1125
|
+
if (context.workflowRunId) {
|
|
1126
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
1127
|
+
WHERE workflow_run_id = ? AND (? IS NULL OR workflow_step_id = ?)
|
|
1128
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.workflowRunId, context.workflowStepId ?? null, context.workflowStepId ?? null);
|
|
1129
|
+
if (row)
|
|
1130
|
+
return rowToGoal(row);
|
|
1131
|
+
}
|
|
1132
|
+
if (context.sourceType && context.sourceId) {
|
|
1133
|
+
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);
|
|
1134
|
+
if (row)
|
|
1135
|
+
return rowToGoal(row);
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
listGoals(opts = {}) {
|
|
1140
|
+
const limit = opts.limit ?? 100;
|
|
1141
|
+
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);
|
|
1142
|
+
return rows.map(rowToGoal);
|
|
1143
|
+
}
|
|
1144
|
+
createGoalPlanNodes(goalId, nodes, opts = {}) {
|
|
1145
|
+
const goal = this.requireGoal(goalId);
|
|
1146
|
+
const now = nowIso();
|
|
1147
|
+
const materialized = nodes.map((node, sequence) => ({
|
|
1148
|
+
nodeId: genId(),
|
|
1149
|
+
planId: goal.planId,
|
|
1150
|
+
key: node.key,
|
|
1151
|
+
sequence,
|
|
1152
|
+
priority: node.priority ?? 0,
|
|
1153
|
+
objective: node.objective,
|
|
1154
|
+
status: "pending",
|
|
1155
|
+
ready: false,
|
|
1156
|
+
tokenBudget: node.tokenBudget,
|
|
1157
|
+
tokensUsed: 0,
|
|
1158
|
+
timeUsedSeconds: 0,
|
|
1159
|
+
dependsOn: node.dependsOn ?? [],
|
|
1160
|
+
createdAt: now,
|
|
1161
|
+
updatedAt: now
|
|
1162
|
+
}));
|
|
1163
|
+
const withReady = updateReadyFlags(materialized, "active");
|
|
1164
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1165
|
+
try {
|
|
1166
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1167
|
+
for (const node of withReady) {
|
|
1168
|
+
this.db.query(`INSERT OR IGNORE INTO goal_plan_nodes (id, goal_id, plan_id, key, sequence, priority, objective, status, ready,
|
|
1169
|
+
token_budget, tokens_used, time_used_seconds, depends_on_json, created_at, updated_at)
|
|
1170
|
+
VALUES ($id, $goalId, $planId, $key, $sequence, $priority, $objective, $status, $ready, $tokenBudget,
|
|
1171
|
+
$tokensUsed, $timeUsedSeconds, $dependsOn, $created, $updated)`).run({
|
|
1172
|
+
$id: node.nodeId,
|
|
1173
|
+
$goalId: goal.goalId,
|
|
1174
|
+
$planId: goal.planId,
|
|
1175
|
+
$key: node.key,
|
|
1176
|
+
$sequence: node.sequence,
|
|
1177
|
+
$priority: node.priority,
|
|
1178
|
+
$objective: node.objective,
|
|
1179
|
+
$status: node.status,
|
|
1180
|
+
$ready: node.ready ? 1 : 0,
|
|
1181
|
+
$tokenBudget: node.tokenBudget ?? null,
|
|
1182
|
+
$tokensUsed: node.tokensUsed,
|
|
1183
|
+
$timeUsedSeconds: node.timeUsedSeconds,
|
|
1184
|
+
$dependsOn: JSON.stringify(node.dependsOn),
|
|
1185
|
+
$created: node.createdAt,
|
|
1186
|
+
$updated: node.updatedAt
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
this.db.exec("COMMIT");
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
try {
|
|
1192
|
+
this.db.exec("ROLLBACK");
|
|
1193
|
+
} catch {}
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1196
|
+
return this.listGoalPlanNodes(goalId);
|
|
1197
|
+
}
|
|
1198
|
+
listGoalPlanNodes(goalIdOrPlanId) {
|
|
1199
|
+
const rows = this.db.query("SELECT * FROM goal_plan_nodes WHERE goal_id = ? OR plan_id = ? ORDER BY sequence ASC").all(goalIdOrPlanId, goalIdOrPlanId);
|
|
1200
|
+
return rows.map(rowToGoalPlanNode);
|
|
1201
|
+
}
|
|
1202
|
+
updateGoalStatus(goalId, status, opts = {}) {
|
|
1203
|
+
const current = this.requireGoal(goalId);
|
|
1204
|
+
assertGoalTransition(current.status, status);
|
|
1205
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1206
|
+
this.db.query(`UPDATE goals SET status=$status, updated_at=$updated
|
|
1207
|
+
WHERE id=$id
|
|
1208
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1209
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1210
|
+
))`).run({
|
|
1211
|
+
$id: goalId,
|
|
1212
|
+
$status: status,
|
|
1213
|
+
$updated: now,
|
|
1214
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1215
|
+
$now: now
|
|
1216
|
+
});
|
|
1217
|
+
return this.requireGoal(goalId);
|
|
1218
|
+
}
|
|
1219
|
+
addGoalUsage(goalId, tokens, timeUsedSeconds = 0, opts = {}) {
|
|
1220
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1221
|
+
this.db.query(`UPDATE goals
|
|
1222
|
+
SET tokens_used=tokens_used + $tokens, time_used_seconds=time_used_seconds + $seconds, updated_at=$updated
|
|
1223
|
+
WHERE id=$id
|
|
1224
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1225
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1226
|
+
))`).run({
|
|
1227
|
+
$id: goalId,
|
|
1228
|
+
$tokens: tokens,
|
|
1229
|
+
$seconds: timeUsedSeconds,
|
|
1230
|
+
$updated: now,
|
|
1231
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1232
|
+
$now: now
|
|
1233
|
+
});
|
|
1234
|
+
return this.requireGoal(goalId);
|
|
1235
|
+
}
|
|
1236
|
+
updateGoalPlanNode(goalId, key, patch, opts = {}) {
|
|
1237
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1238
|
+
this.db.query(`UPDATE goal_plan_nodes
|
|
1239
|
+
SET status=COALESCE($status, status),
|
|
1240
|
+
tokens_used=COALESCE($tokensUsed, tokens_used),
|
|
1241
|
+
time_used_seconds=COALESCE($timeUsedSeconds, time_used_seconds),
|
|
1242
|
+
ready=COALESCE($ready, ready),
|
|
1243
|
+
updated_at=$updated
|
|
1244
|
+
WHERE goal_id=$goalId AND key=$key
|
|
1245
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1246
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1247
|
+
))`).run({
|
|
1248
|
+
$goalId: goalId,
|
|
1249
|
+
$key: key,
|
|
1250
|
+
$status: patch.status ?? null,
|
|
1251
|
+
$tokensUsed: patch.tokensUsed ?? null,
|
|
1252
|
+
$timeUsedSeconds: patch.timeUsedSeconds ?? null,
|
|
1253
|
+
$ready: patch.ready === undefined ? null : patch.ready ? 1 : 0,
|
|
1254
|
+
$updated: now,
|
|
1255
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1256
|
+
$now: now
|
|
1257
|
+
});
|
|
1258
|
+
const node = this.listGoalPlanNodes(goalId).find((entry) => entry.key === key);
|
|
1259
|
+
if (!node)
|
|
1260
|
+
throw new Error(`goal node not found: ${goalId}/${key}`);
|
|
1261
|
+
return node;
|
|
1262
|
+
}
|
|
1263
|
+
recordGoalEvent(input, opts = {}) {
|
|
1264
|
+
const goal = this.requireGoal(input.goalId);
|
|
1265
|
+
const now = nowIso();
|
|
1266
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1267
|
+
try {
|
|
1268
|
+
this.assertDaemonLeaseFence(opts, now);
|
|
1269
|
+
const previous = this.db.query("SELECT MAX(turn) AS turn FROM goal_runs WHERE goal_id = ?").get(goal.goalId);
|
|
1270
|
+
const turn = input.turn ?? (previous?.turn ?? 0) + 1;
|
|
1271
|
+
const id = genId();
|
|
1272
|
+
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,
|
|
1273
|
+
turn, phase, status, node_key, tokens_used, evidence_json, raw_response_json, created_at, updated_at)
|
|
1274
|
+
VALUES ($id, $goalId, $planId, $loopId, $loopRunId, $workflowId, $workflowRunId, $workflowStepId,
|
|
1275
|
+
$turn, $phase, $status, $nodeKey, $tokensUsed, $evidence, $rawResponse, $created, $updated)`).run({
|
|
1276
|
+
$id: id,
|
|
1277
|
+
$goalId: goal.goalId,
|
|
1278
|
+
$planId: goal.planId,
|
|
1279
|
+
$loopId: goal.loopId ?? null,
|
|
1280
|
+
$loopRunId: goal.loopRunId ?? null,
|
|
1281
|
+
$workflowId: goal.workflowId ?? null,
|
|
1282
|
+
$workflowRunId: goal.workflowRunId ?? null,
|
|
1283
|
+
$workflowStepId: goal.workflowStepId ?? null,
|
|
1284
|
+
$turn: turn,
|
|
1285
|
+
$phase: input.phase,
|
|
1286
|
+
$status: input.status,
|
|
1287
|
+
$nodeKey: input.nodeKey ?? null,
|
|
1288
|
+
$tokensUsed: input.tokensUsed ?? 0,
|
|
1289
|
+
$evidence: input.evidence ? JSON.stringify(input.evidence) : null,
|
|
1290
|
+
$rawResponse: input.rawResponse === undefined ? null : JSON.stringify(input.rawResponse),
|
|
1291
|
+
$created: now,
|
|
1292
|
+
$updated: now
|
|
1293
|
+
});
|
|
1294
|
+
if (input.tokensUsed && input.tokensUsed > 0) {
|
|
1295
|
+
this.db.query("UPDATE goals SET tokens_used=tokens_used + ?, updated_at=? WHERE id=?").run(input.tokensUsed, now, goal.goalId);
|
|
1296
|
+
}
|
|
1297
|
+
this.db.exec("COMMIT");
|
|
1298
|
+
const event = this.db.query("SELECT * FROM goal_runs WHERE id = ?").get(id);
|
|
1299
|
+
if (!event)
|
|
1300
|
+
throw new Error(`goal run not found after record: ${id}`);
|
|
1301
|
+
return rowToGoalRun(event);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
try {
|
|
1304
|
+
this.db.exec("ROLLBACK");
|
|
1305
|
+
} catch {}
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
listGoalRuns(opts = {}) {
|
|
1310
|
+
const limit = opts.limit ?? 200;
|
|
1311
|
+
let rows;
|
|
1312
|
+
if (opts.goalId) {
|
|
1313
|
+
rows = this.db.query("SELECT * FROM goal_runs WHERE goal_id = ? ORDER BY created_at ASC LIMIT ?").all(opts.goalId, limit);
|
|
1314
|
+
} else if (opts.runId) {
|
|
1315
|
+
rows = this.db.query(`SELECT * FROM goal_runs
|
|
1316
|
+
WHERE id = ? OR loop_run_id = ? OR workflow_run_id = ?
|
|
1317
|
+
ORDER BY created_at ASC LIMIT ?`).all(opts.runId, opts.runId, opts.runId, limit);
|
|
1318
|
+
} else {
|
|
1319
|
+
rows = this.db.query("SELECT * FROM goal_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1320
|
+
}
|
|
1321
|
+
return rows.map(rowToGoalRun);
|
|
1322
|
+
}
|
|
784
1323
|
createWorkflowRun(input) {
|
|
785
1324
|
const now = nowIso();
|
|
786
1325
|
if (input.idempotencyKey) {
|
|
@@ -1509,6 +2048,7 @@ class Store {
|
|
|
1509
2048
|
|
|
1510
2049
|
// src/lib/executor.ts
|
|
1511
2050
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
2051
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1512
2052
|
import { once } from "events";
|
|
1513
2053
|
import { resolveMachineCommand } from "@hasna/machines/consumer";
|
|
1514
2054
|
|
|
@@ -1804,6 +2344,12 @@ function metadataEnv(metadata) {
|
|
|
1804
2344
|
env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
|
|
1805
2345
|
if (metadata.workflowStepId)
|
|
1806
2346
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
2347
|
+
if (metadata.goalId)
|
|
2348
|
+
env.LOOPS_GOAL_ID = metadata.goalId;
|
|
2349
|
+
if (metadata.goalObjective)
|
|
2350
|
+
env.LOOPS_GOAL_OBJECTIVE = metadata.goalObjective;
|
|
2351
|
+
if (metadata.goalNodeKey)
|
|
2352
|
+
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
1807
2353
|
return env;
|
|
1808
2354
|
}
|
|
1809
2355
|
function providerCommand(provider) {
|
|
@@ -1941,9 +2487,9 @@ function commandForShell(spec) {
|
|
|
1941
2487
|
return [spec.command, ...spec.args.map(shellQuote)].join(" ");
|
|
1942
2488
|
}
|
|
1943
2489
|
function hereDoc(value) {
|
|
1944
|
-
let delimiter2 = `__OPENLOOPS_STDIN_${
|
|
2490
|
+
let delimiter2 = `__OPENLOOPS_STDIN_${randomBytes2(8).toString("hex").toUpperCase()}__`;
|
|
1945
2491
|
while (value.split(/\r?\n/).includes(delimiter2)) {
|
|
1946
|
-
delimiter2 = `__OPENLOOPS_STDIN_${
|
|
2492
|
+
delimiter2 = `__OPENLOOPS_STDIN_${randomBytes2(8).toString("hex").toUpperCase()}__`;
|
|
1947
2493
|
}
|
|
1948
2494
|
return [`cat > "$__OPENLOOPS_STDIN" <<'${delimiter2}'`, value, delimiter2];
|
|
1949
2495
|
}
|
|
@@ -1974,7 +2520,7 @@ function remoteScript(spec, metadata) {
|
|
|
1974
2520
|
lines.push(...hereDoc(spec.stdin));
|
|
1975
2521
|
stdinRedirect = ' < "$__OPENLOOPS_STDIN"';
|
|
1976
2522
|
}
|
|
1977
|
-
const invocation = spec.shell ? `sh -
|
|
2523
|
+
const invocation = spec.shell ? `sh -c ${shellQuote(commandForShell(spec))}${stdinRedirect}` : `${[spec.command, ...spec.args].map(shellQuote).join(" ")}${stdinRedirect}`;
|
|
1978
2524
|
lines.push(invocation);
|
|
1979
2525
|
return `${lines.join(`
|
|
1980
2526
|
`)}
|
|
@@ -2267,6 +2813,346 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
2267
2813
|
}, { ...opts, machine: opts.machine ?? loop.machine });
|
|
2268
2814
|
}
|
|
2269
2815
|
|
|
2816
|
+
// src/lib/goal/runner.ts
|
|
2817
|
+
import { generateObject } from "ai";
|
|
2818
|
+
import { z } from "zod";
|
|
2819
|
+
|
|
2820
|
+
// src/lib/goal/model-factory.ts
|
|
2821
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
2822
|
+
var DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
2823
|
+
function resolveGoalModel(opts = {}) {
|
|
2824
|
+
const env = opts.env ?? process.env;
|
|
2825
|
+
const apiKey = opts.apiKey ?? env.OPENROUTER_API_KEY;
|
|
2826
|
+
if (!apiKey) {
|
|
2827
|
+
throw new Error("OPENROUTER_API_KEY is required to run goals with the OpenRouter AI SDK provider");
|
|
2828
|
+
}
|
|
2829
|
+
const provider = createOpenRouter({
|
|
2830
|
+
apiKey,
|
|
2831
|
+
baseURL: opts.baseURL ?? env.LOOPS_GOAL_BASE_URL ?? env.OPENROUTER_BASE_URL
|
|
2832
|
+
});
|
|
2833
|
+
return provider.chat(opts.model ?? env.LOOPS_GOAL_MODEL ?? DEFAULT_GOAL_MODEL);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/lib/goal/prompts.ts
|
|
2837
|
+
function planPrompt(spec) {
|
|
2838
|
+
const budget = spec.tokenBudget ? `Token budget: ${spec.tokenBudget}.` : "No explicit token budget.";
|
|
2839
|
+
return [
|
|
2840
|
+
"Create a flat DAG goal plan for this objective.",
|
|
2841
|
+
"Each node must have a stable short key, a concrete objective, optional dependsOn keys, and optional priority.",
|
|
2842
|
+
"Prefer the smallest plan that can prove the explicit requirements.",
|
|
2843
|
+
budget,
|
|
2844
|
+
`Objective: ${spec.objective}`
|
|
2845
|
+
].join(`
|
|
2846
|
+
`);
|
|
2847
|
+
}
|
|
2848
|
+
function achievementPrompt(goal, nodes, evidence) {
|
|
2849
|
+
return [
|
|
2850
|
+
"Run an adversarial achievement audit.",
|
|
2851
|
+
"Completion is unproven until every explicit requirement is verified against evidence.",
|
|
2852
|
+
"Return achieved=false if evidence is missing, ambiguous, or only asserts completion.",
|
|
2853
|
+
"adversarialReview must be non-empty and must describe the attempted falsification.",
|
|
2854
|
+
`Goal: ${goal.objective}`,
|
|
2855
|
+
`Nodes: ${nodes.map((node) => `${node.key}=${node.status}`).join(", ")}`,
|
|
2856
|
+
`Evidence:
|
|
2857
|
+
${evidence.join(`
|
|
2858
|
+
---
|
|
2859
|
+
`) || "(none)"}`
|
|
2860
|
+
].join(`
|
|
2861
|
+
`);
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// src/lib/goal/runner.ts
|
|
2865
|
+
var DEFAULT_MAX_TURNS = 10;
|
|
2866
|
+
var PlanNodeSchema = z.object({
|
|
2867
|
+
key: z.string().min(1).max(64).regex(/^[A-Za-z0-9_.-]+$/),
|
|
2868
|
+
objective: z.string().min(1),
|
|
2869
|
+
dependsOn: z.array(z.string().min(1)).optional().default([]),
|
|
2870
|
+
priority: z.number().int().optional().default(0),
|
|
2871
|
+
tokenBudget: z.number().int().positive().optional()
|
|
2872
|
+
});
|
|
2873
|
+
var PlanSchema = z.object({
|
|
2874
|
+
nodes: z.array(PlanNodeSchema).min(1)
|
|
2875
|
+
});
|
|
2876
|
+
var AchievementSchema = z.object({
|
|
2877
|
+
achieved: z.boolean(),
|
|
2878
|
+
status: z.enum(["active", "blocked", "budgetLimited", "complete", "cancelled"]).optional(),
|
|
2879
|
+
evidence: z.array(z.string()).optional().default([]),
|
|
2880
|
+
unmetRequirements: z.array(z.string()).optional().default([]),
|
|
2881
|
+
adversarialReview: z.string().min(1)
|
|
2882
|
+
});
|
|
2883
|
+
function normalizeGoalSpec2(spec) {
|
|
2884
|
+
const objective = spec.objective.trim();
|
|
2885
|
+
if (!objective)
|
|
2886
|
+
throw new Error("goal.objective must be a non-empty string");
|
|
2887
|
+
if (objective.length > GOAL_OBJECTIVE_MAX_CHARS) {
|
|
2888
|
+
throw new Error(`goal.objective must be ${GOAL_OBJECTIVE_MAX_CHARS} characters or fewer`);
|
|
2889
|
+
}
|
|
2890
|
+
return { ...spec, objective, autoExecute: spec.autoExecute ?? "readyOnly" };
|
|
2891
|
+
}
|
|
2892
|
+
function usageTotal(value) {
|
|
2893
|
+
const usage = value;
|
|
2894
|
+
const input = typeof usage.inputTokens === "number" ? usage.inputTokens : usage.inputTokens?.total ?? 0;
|
|
2895
|
+
const output = typeof usage.outputTokens === "number" ? usage.outputTokens : usage.outputTokens?.total ?? 0;
|
|
2896
|
+
return usage.totalTokens ?? input + output;
|
|
2897
|
+
}
|
|
2898
|
+
function resultFromGoal(goal, status, stdout, error, startedAt = goal.createdAt) {
|
|
2899
|
+
const finishedAt = nowIso();
|
|
2900
|
+
return {
|
|
2901
|
+
status,
|
|
2902
|
+
exitCode: status === "succeeded" ? 0 : 1,
|
|
2903
|
+
stdout,
|
|
2904
|
+
stderr: "",
|
|
2905
|
+
error,
|
|
2906
|
+
startedAt,
|
|
2907
|
+
finishedAt,
|
|
2908
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
2909
|
+
goalId: goal.goalId
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function budgetExhausted(goal) {
|
|
2913
|
+
return goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget;
|
|
2914
|
+
}
|
|
2915
|
+
function sameBlockerKey(values) {
|
|
2916
|
+
return values.map((value) => value.trim()).filter(Boolean).join(`
|
|
2917
|
+
`) || "goal completion remains unproven";
|
|
2918
|
+
}
|
|
2919
|
+
function metadataFor(goal, node, context) {
|
|
2920
|
+
return {
|
|
2921
|
+
loopId: context?.loopId,
|
|
2922
|
+
loopName: context?.loopName,
|
|
2923
|
+
runId: context?.loopRunId,
|
|
2924
|
+
scheduledFor: context?.scheduledFor,
|
|
2925
|
+
workflowId: context?.workflowId,
|
|
2926
|
+
workflowName: context?.workflowName,
|
|
2927
|
+
workflowRunId: context?.workflowRunId,
|
|
2928
|
+
workflowStepId: context?.workflowStepId,
|
|
2929
|
+
goalId: goal.goalId,
|
|
2930
|
+
goalObjective: goal.objective,
|
|
2931
|
+
goalNodeKey: node.key
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
async function executeUnderlyingTarget(target, goal, node, opts) {
|
|
2935
|
+
const metadata = metadataFor(goal, node, opts.context);
|
|
2936
|
+
if (opts.executeNode)
|
|
2937
|
+
return opts.executeNode(node, metadata);
|
|
2938
|
+
if (!target)
|
|
2939
|
+
throw new Error("runGoal requires either target or executeNode");
|
|
2940
|
+
return executeTarget(target, metadata, {
|
|
2941
|
+
env: opts.env,
|
|
2942
|
+
daemonLeaseId: opts.daemonLeaseId,
|
|
2943
|
+
beforePersist: opts.beforePersist,
|
|
2944
|
+
signal: opts.signal
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
async function planGoal(store, goal, spec, model, opts) {
|
|
2948
|
+
const existing = store.listGoalPlanNodes(goal.goalId);
|
|
2949
|
+
if (existing.length > 0)
|
|
2950
|
+
return existing;
|
|
2951
|
+
const planned = await generateObject({
|
|
2952
|
+
model,
|
|
2953
|
+
schema: PlanSchema,
|
|
2954
|
+
temperature: 0,
|
|
2955
|
+
prompt: planPrompt(spec),
|
|
2956
|
+
abortSignal: opts.signal
|
|
2957
|
+
});
|
|
2958
|
+
const tokens = usageTotal(planned.usage);
|
|
2959
|
+
const rawNodes = planned.object.nodes.map((node, index) => ({
|
|
2960
|
+
key: node.key,
|
|
2961
|
+
objective: node.objective,
|
|
2962
|
+
dependsOn: node.dependsOn ?? [],
|
|
2963
|
+
priority: node.priority ?? 0,
|
|
2964
|
+
tokenBudget: node.tokenBudget,
|
|
2965
|
+
sequence: index
|
|
2966
|
+
}));
|
|
2967
|
+
assertAcyclicNodes(rawNodes.map((node) => ({ key: node.key, dependsOn: node.dependsOn })));
|
|
2968
|
+
store.recordGoalEvent({
|
|
2969
|
+
goalId: goal.goalId,
|
|
2970
|
+
turn: 0,
|
|
2971
|
+
phase: "plan",
|
|
2972
|
+
status: "active",
|
|
2973
|
+
tokensUsed: tokens,
|
|
2974
|
+
evidence: { nodeCount: rawNodes.length },
|
|
2975
|
+
rawResponse: planned.object
|
|
2976
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
2977
|
+
return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
|
|
2978
|
+
}
|
|
2979
|
+
function stdoutFor(goal, nodes, evidence, validation) {
|
|
2980
|
+
return JSON.stringify({
|
|
2981
|
+
goal,
|
|
2982
|
+
rollup: rollupSummary(nodes),
|
|
2983
|
+
nodes,
|
|
2984
|
+
evidence,
|
|
2985
|
+
validation
|
|
2986
|
+
}, null, 2);
|
|
2987
|
+
}
|
|
2988
|
+
async function runGoal(store, input, opts = {}) {
|
|
2989
|
+
const spec = normalizeGoalSpec2(input);
|
|
2990
|
+
const model = opts.model ?? resolveGoalModel({ model: spec.model, env: opts.env });
|
|
2991
|
+
const startedAt = nowIso();
|
|
2992
|
+
const existing = store.findGoalByContext({
|
|
2993
|
+
loopRunId: opts.context?.loopRunId,
|
|
2994
|
+
workflowRunId: opts.context?.workflowRunId,
|
|
2995
|
+
workflowStepId: opts.context?.workflowStepId,
|
|
2996
|
+
sourceType: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : "manual",
|
|
2997
|
+
sourceId: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : spec.objective
|
|
2998
|
+
});
|
|
2999
|
+
let goal = existing ?? store.createGoal({
|
|
3000
|
+
objective: spec.objective,
|
|
3001
|
+
tokenBudget: spec.tokenBudget,
|
|
3002
|
+
autoExecute: spec.autoExecute,
|
|
3003
|
+
maxTokens: spec.maxTokens ?? spec.tokenBudget,
|
|
3004
|
+
sourceType: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : "manual",
|
|
3005
|
+
sourceId: opts.context?.loopRunId || opts.context?.workflowRunId ? undefined : spec.objective,
|
|
3006
|
+
loopId: opts.context?.loopId,
|
|
3007
|
+
loopRunId: opts.context?.loopRunId,
|
|
3008
|
+
workflowId: opts.context?.workflowId,
|
|
3009
|
+
workflowRunId: opts.context?.workflowRunId,
|
|
3010
|
+
workflowStepId: opts.context?.workflowStepId
|
|
3011
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3012
|
+
let nodes = await planGoal(store, goal, spec, model, opts);
|
|
3013
|
+
goal = store.requireGoal(goal.goalId);
|
|
3014
|
+
const evidence = [];
|
|
3015
|
+
let validation;
|
|
3016
|
+
let lastBlocker = "";
|
|
3017
|
+
let repeatedBlockerCount = 0;
|
|
3018
|
+
if (budgetExhausted(goal)) {
|
|
3019
|
+
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3020
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
|
|
3021
|
+
}
|
|
3022
|
+
for (let turn = 1;turn <= (spec.maxTurns ?? DEFAULT_MAX_TURNS); turn++) {
|
|
3023
|
+
if (opts.signal?.aborted) {
|
|
3024
|
+
goal = store.updateGoalStatus(goal.goalId, "cancelled", { daemonLeaseId: opts.daemonLeaseId });
|
|
3025
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
|
|
3026
|
+
}
|
|
3027
|
+
goal = store.requireGoal(goal.goalId);
|
|
3028
|
+
nodes = store.listGoalPlanNodes(goal.goalId);
|
|
3029
|
+
if (budgetExhausted(goal)) {
|
|
3030
|
+
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3031
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
|
|
3032
|
+
}
|
|
3033
|
+
const readyKeys = readyNodeKeys({
|
|
3034
|
+
status: goal.status === "active" ? "active" : goal.status === "budgetLimited" ? "budgetLimited" : "blocked",
|
|
3035
|
+
nodes
|
|
3036
|
+
});
|
|
3037
|
+
if (readyKeys.length > 0) {
|
|
3038
|
+
for (const key of readyKeys) {
|
|
3039
|
+
const node = store.listGoalPlanNodes(goal.goalId).find((entry) => entry.key === key);
|
|
3040
|
+
if (!node || node.status !== "pending")
|
|
3041
|
+
continue;
|
|
3042
|
+
opts.beforePersist?.();
|
|
3043
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { status: "active", ready: false }, { daemonLeaseId: opts.daemonLeaseId });
|
|
3044
|
+
const result = await executeUnderlyingTarget(opts.target, goal, node, opts);
|
|
3045
|
+
store.recordGoalEvent({
|
|
3046
|
+
goalId: goal.goalId,
|
|
3047
|
+
turn,
|
|
3048
|
+
phase: "execute",
|
|
3049
|
+
status: result.status === "succeeded" ? "complete" : "active",
|
|
3050
|
+
nodeKey: node.key,
|
|
3051
|
+
evidence: {
|
|
3052
|
+
status: result.status,
|
|
3053
|
+
exitCode: result.exitCode,
|
|
3054
|
+
stdout: result.stdout,
|
|
3055
|
+
stderr: result.stderr,
|
|
3056
|
+
error: result.error
|
|
3057
|
+
}
|
|
3058
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3059
|
+
if (result.status === "succeeded") {
|
|
3060
|
+
evidence.push(`node ${node.key} succeeded
|
|
3061
|
+
stdout:
|
|
3062
|
+
${result.stdout}
|
|
3063
|
+
stderr:
|
|
3064
|
+
${result.stderr}`);
|
|
3065
|
+
store.updateGoalPlanNode(goal.goalId, node.key, {
|
|
3066
|
+
status: "complete",
|
|
3067
|
+
timeUsedSeconds: Math.round(result.durationMs / 1000)
|
|
3068
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
const blocker2 = `node ${node.key} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
3072
|
+
if (blocker2 === lastBlocker)
|
|
3073
|
+
repeatedBlockerCount += 1;
|
|
3074
|
+
else {
|
|
3075
|
+
lastBlocker = blocker2;
|
|
3076
|
+
repeatedBlockerCount = 1;
|
|
3077
|
+
}
|
|
3078
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { status: repeatedBlockerCount >= 3 ? "blocked" : "pending" }, {
|
|
3079
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
3080
|
+
});
|
|
3081
|
+
if (repeatedBlockerCount >= 3) {
|
|
3082
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3083
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence), blocker2, startedAt);
|
|
3084
|
+
}
|
|
3085
|
+
break;
|
|
3086
|
+
}
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
if (nodes.every((node) => node.status === "complete")) {
|
|
3090
|
+
const judged = await generateObject({
|
|
3091
|
+
model,
|
|
3092
|
+
schema: AchievementSchema,
|
|
3093
|
+
temperature: 0,
|
|
3094
|
+
prompt: achievementPrompt(goal, nodes, evidence),
|
|
3095
|
+
abortSignal: opts.signal
|
|
3096
|
+
});
|
|
3097
|
+
const tokens = usageTotal(judged.usage);
|
|
3098
|
+
validation = judged.object;
|
|
3099
|
+
const achieved = judged.object.achieved && judged.object.adversarialReview.trim().length > 0;
|
|
3100
|
+
const unmet = achieved ? [] : judged.object.unmetRequirements.length > 0 ? judged.object.unmetRequirements : ["adversarial review did not prove completion"];
|
|
3101
|
+
store.recordGoalEvent({
|
|
3102
|
+
goalId: goal.goalId,
|
|
3103
|
+
turn,
|
|
3104
|
+
phase: "validate",
|
|
3105
|
+
status: achieved ? "complete" : "blocked",
|
|
3106
|
+
tokensUsed: tokens,
|
|
3107
|
+
evidence: {
|
|
3108
|
+
achieved,
|
|
3109
|
+
evidence: judged.object.evidence,
|
|
3110
|
+
unmetRequirements: unmet,
|
|
3111
|
+
adversarialReview: judged.object.adversarialReview
|
|
3112
|
+
},
|
|
3113
|
+
rawResponse: judged.object
|
|
3114
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3115
|
+
goal = store.requireGoal(goal.goalId);
|
|
3116
|
+
if (achieved) {
|
|
3117
|
+
goal = store.updateGoalStatus(goal.goalId, "complete", { daemonLeaseId: opts.daemonLeaseId });
|
|
3118
|
+
return resultFromGoal(goal, "succeeded", stdoutFor(goal, nodes, evidence, validation), undefined, startedAt);
|
|
3119
|
+
}
|
|
3120
|
+
const blocker2 = sameBlockerKey(unmet);
|
|
3121
|
+
if (blocker2 === lastBlocker)
|
|
3122
|
+
repeatedBlockerCount += 1;
|
|
3123
|
+
else {
|
|
3124
|
+
lastBlocker = blocker2;
|
|
3125
|
+
repeatedBlockerCount = 1;
|
|
3126
|
+
}
|
|
3127
|
+
if (repeatedBlockerCount >= 3) {
|
|
3128
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3129
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker2, startedAt);
|
|
3130
|
+
}
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3133
|
+
const blocker = "no ready goal nodes and goal plan is incomplete";
|
|
3134
|
+
if (blocker === lastBlocker)
|
|
3135
|
+
repeatedBlockerCount += 1;
|
|
3136
|
+
else {
|
|
3137
|
+
lastBlocker = blocker;
|
|
3138
|
+
repeatedBlockerCount = 1;
|
|
3139
|
+
}
|
|
3140
|
+
store.recordGoalEvent({
|
|
3141
|
+
goalId: goal.goalId,
|
|
3142
|
+
turn,
|
|
3143
|
+
phase: "status",
|
|
3144
|
+
status: repeatedBlockerCount >= 3 ? "blocked" : "active",
|
|
3145
|
+
evidence: { blocker }
|
|
3146
|
+
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
3147
|
+
if (repeatedBlockerCount >= 3) {
|
|
3148
|
+
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
3149
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
3153
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation), "goal max turns exhausted", startedAt);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
2270
3156
|
// src/lib/workflow-runner.ts
|
|
2271
3157
|
function targetWithStepAccount(step) {
|
|
2272
3158
|
const account = step.account ?? step.target.account;
|
|
@@ -2289,6 +3175,24 @@ function workflowResult(workflowRun, status, startedAt, finishedAt, stdout, erro
|
|
|
2289
3175
|
};
|
|
2290
3176
|
}
|
|
2291
3177
|
async function executeWorkflow(store, workflow, opts = {}) {
|
|
3178
|
+
if (workflow.goal) {
|
|
3179
|
+
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
3180
|
+
return runGoal(store, workflow.goal, {
|
|
3181
|
+
...opts,
|
|
3182
|
+
context: {
|
|
3183
|
+
loopId: opts.loop?.id,
|
|
3184
|
+
loopName: opts.loop?.name,
|
|
3185
|
+
loopRunId: opts.loopRun?.id,
|
|
3186
|
+
scheduledFor: opts.loopRun?.scheduledFor ?? opts.scheduledFor,
|
|
3187
|
+
workflowId: workflow.id,
|
|
3188
|
+
workflowName: workflow.name
|
|
3189
|
+
},
|
|
3190
|
+
executeNode: async (node) => executeWorkflow(store, workflowWithoutGoal, {
|
|
3191
|
+
...opts,
|
|
3192
|
+
idempotencyKey: `${opts.idempotencyKey ?? workflow.id}:goal:${node.key}`
|
|
3193
|
+
})
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
2292
3196
|
const run = store.createWorkflowRun({
|
|
2293
3197
|
workflow,
|
|
2294
3198
|
loop: opts.loop,
|
|
@@ -2364,16 +3268,34 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
2364
3268
|
}, opts.cancelPollMs ?? 500);
|
|
2365
3269
|
cancelTimer.unref();
|
|
2366
3270
|
try {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
3271
|
+
if (step.goal) {
|
|
3272
|
+
result = await runGoal(store, step.goal, {
|
|
3273
|
+
...opts,
|
|
3274
|
+
target: targetWithStepAccount(step),
|
|
3275
|
+
signal: controller.signal,
|
|
3276
|
+
context: {
|
|
3277
|
+
loopId: opts.loop?.id,
|
|
3278
|
+
loopName: opts.loop?.name,
|
|
3279
|
+
loopRunId: opts.loopRun?.id,
|
|
3280
|
+
scheduledFor: opts.loopRun?.scheduledFor ?? opts.scheduledFor,
|
|
3281
|
+
workflowId: workflow.id,
|
|
3282
|
+
workflowName: workflow.name,
|
|
3283
|
+
workflowRunId: run.id,
|
|
3284
|
+
workflowStepId: step.id
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
3287
|
+
} else {
|
|
3288
|
+
result = await executeTarget(targetWithStepAccount(step), metadata, {
|
|
3289
|
+
...opts,
|
|
3290
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
3291
|
+
signal: controller.signal,
|
|
3292
|
+
onSpawn: (pid) => {
|
|
3293
|
+
opts.beforePersist?.();
|
|
3294
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
3295
|
+
opts.onSpawn?.(pid);
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
2377
3299
|
} catch (error) {
|
|
2378
3300
|
const finishedAt2 = nowIso();
|
|
2379
3301
|
result = {
|
|
@@ -2451,9 +3373,42 @@ function preflightWorkflow(workflow, opts = {}) {
|
|
|
2451
3373
|
}, opts));
|
|
2452
3374
|
}
|
|
2453
3375
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
2454
|
-
if (loop.target.type !== "workflow")
|
|
3376
|
+
if (loop.target.type !== "workflow") {
|
|
3377
|
+
if (loop.goal) {
|
|
3378
|
+
return runGoal(store, loop.goal, {
|
|
3379
|
+
...opts,
|
|
3380
|
+
target: loop.target,
|
|
3381
|
+
context: {
|
|
3382
|
+
loopId: loop.id,
|
|
3383
|
+
loopName: loop.name,
|
|
3384
|
+
loopRunId: run.id,
|
|
3385
|
+
scheduledFor: run.scheduledFor
|
|
3386
|
+
}
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
2455
3389
|
return executeLoop(loop, run, opts);
|
|
3390
|
+
}
|
|
2456
3391
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
3392
|
+
if (loop.goal) {
|
|
3393
|
+
return runGoal(store, loop.goal, {
|
|
3394
|
+
...opts,
|
|
3395
|
+
context: {
|
|
3396
|
+
loopId: loop.id,
|
|
3397
|
+
loopName: loop.name,
|
|
3398
|
+
loopRunId: run.id,
|
|
3399
|
+
scheduledFor: run.scheduledFor,
|
|
3400
|
+
workflowId: workflow.id,
|
|
3401
|
+
workflowName: workflow.name
|
|
3402
|
+
},
|
|
3403
|
+
executeNode: async (node) => executeWorkflow(store, workflow, {
|
|
3404
|
+
...opts,
|
|
3405
|
+
loop,
|
|
3406
|
+
loopRun: run,
|
|
3407
|
+
scheduledFor: run.scheduledFor,
|
|
3408
|
+
idempotencyKey: `${loop.id}:${run.scheduledFor}:attempt:${run.attempt}:goal:${node.key}`
|
|
3409
|
+
})
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
2457
3412
|
const controller = loop.target.timeoutMs ? new AbortController : undefined;
|
|
2458
3413
|
let workflowTimedOut = false;
|
|
2459
3414
|
const externalAbort = () => controller?.abort();
|
|
@@ -2732,6 +3687,13 @@ class LoopsClient {
|
|
|
2732
3687
|
runs(loopId) {
|
|
2733
3688
|
return this.store.listRuns({ loopId });
|
|
2734
3689
|
}
|
|
3690
|
+
goal(idOrName) {
|
|
3691
|
+
const goal = this.store.getGoal(idOrName) ?? this.store.findGoalByLoop(idOrName) ?? this.store.findGoalByRunId(idOrName);
|
|
3692
|
+
return {
|
|
3693
|
+
goal,
|
|
3694
|
+
runs: goal ? this.store.listGoalRuns({ goalId: goal.goalId }) : []
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
2735
3697
|
async tick() {
|
|
2736
3698
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
2737
3699
|
}
|
|
@@ -2987,9 +3949,13 @@ export {
|
|
|
2987
3949
|
workflowExecutionOrder,
|
|
2988
3950
|
workflowBodyFromJson,
|
|
2989
3951
|
tick,
|
|
3952
|
+
runGoal,
|
|
2990
3953
|
runDoctor,
|
|
3954
|
+
rollupSummary,
|
|
2991
3955
|
resolveLoopMachine,
|
|
3956
|
+
resolveGoalModel,
|
|
2992
3957
|
refreshLoopMachine,
|
|
3958
|
+
readyNodeKeys,
|
|
2993
3959
|
preflightWorkflow,
|
|
2994
3960
|
preflightTarget,
|
|
2995
3961
|
parseDuration,
|
|
@@ -2997,6 +3963,7 @@ export {
|
|
|
2997
3963
|
nextCronRun,
|
|
2998
3964
|
loops,
|
|
2999
3965
|
listOpenMachines,
|
|
3966
|
+
isTerminal as isGoalTerminal,
|
|
3000
3967
|
initialNextRun,
|
|
3001
3968
|
executeWorkflow,
|
|
3002
3969
|
executeTarget,
|