@hasna/loops 0.3.57 → 0.3.59
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/dist/cli/index.js +239 -15
- package/dist/daemon/index.js +86 -2
- package/dist/index.js +86 -2
- package/dist/lib/executor.d.ts +2 -0
- package/dist/lib/store.d.ts +11 -0
- package/dist/lib/store.js +79 -0
- package/dist/mcp/index.js +86 -2
- package/dist/sdk/index.js +85 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1260,12 +1260,21 @@ class Store {
|
|
|
1260
1260
|
if (!row)
|
|
1261
1261
|
throw new Error("daemon lease lost");
|
|
1262
1262
|
}
|
|
1263
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1264
|
+
if (!goal || target.type !== "workflow")
|
|
1265
|
+
return;
|
|
1266
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1267
|
+
if (workflow?.goal) {
|
|
1268
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1263
1271
|
createLoop(input, from = new Date) {
|
|
1264
1272
|
const now = nowIso();
|
|
1265
1273
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1266
1274
|
name: "loop-target-validation",
|
|
1267
1275
|
steps: [{ id: "target", target: input.target }]
|
|
1268
1276
|
}).steps[0].target;
|
|
1277
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1269
1278
|
const loop = {
|
|
1270
1279
|
id: genId(),
|
|
1271
1280
|
name: input.name,
|
|
@@ -1437,6 +1446,9 @@ class Store {
|
|
|
1437
1446
|
if (this.hasRunningRun(current.id))
|
|
1438
1447
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1439
1448
|
const workflow = this.requireWorkflow(workflowId);
|
|
1449
|
+
if (current.goal && workflow.goal) {
|
|
1450
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1451
|
+
}
|
|
1440
1452
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1441
1453
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1442
1454
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1475,6 +1487,9 @@ class Store {
|
|
|
1475
1487
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1476
1488
|
if (this.hasRunningRun(current.id))
|
|
1477
1489
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1490
|
+
if (current.goal && normalized.goal) {
|
|
1491
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1492
|
+
}
|
|
1478
1493
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1479
1494
|
const workflow = {
|
|
1480
1495
|
id: genId(),
|
|
@@ -1528,6 +1543,70 @@ class Store {
|
|
|
1528
1543
|
throw error;
|
|
1529
1544
|
}
|
|
1530
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1531
1610
|
renameLoop(id, name, opts = {}) {
|
|
1532
1611
|
const current = this.getLoop(id);
|
|
1533
1612
|
if (!current)
|
|
@@ -4733,6 +4812,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4733
4812
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4734
4813
|
return runGoal(store, workflow.goal, {
|
|
4735
4814
|
...opts,
|
|
4815
|
+
model: opts.goalModel,
|
|
4736
4816
|
context: {
|
|
4737
4817
|
loopId: opts.loop?.id,
|
|
4738
4818
|
loopName: opts.loop?.name,
|
|
@@ -4825,6 +4905,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4825
4905
|
if (step.goal) {
|
|
4826
4906
|
result = await runGoal(store, step.goal, {
|
|
4827
4907
|
...opts,
|
|
4908
|
+
model: opts.goalModel,
|
|
4828
4909
|
target: targetWithStepAccount(step),
|
|
4829
4910
|
signal: controller.signal,
|
|
4830
4911
|
context: {
|
|
@@ -4966,6 +5047,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4966
5047
|
if (loop.goal) {
|
|
4967
5048
|
return runGoal(store, loop.goal, {
|
|
4968
5049
|
...opts,
|
|
5050
|
+
model: opts.goalModel,
|
|
4969
5051
|
target: loop.target,
|
|
4970
5052
|
context: {
|
|
4971
5053
|
loopId: loop.id,
|
|
@@ -4987,8 +5069,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4987
5069
|
}
|
|
4988
5070
|
}
|
|
4989
5071
|
if (loop.goal) {
|
|
5072
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4990
5073
|
return runGoal(store, loop.goal, {
|
|
4991
5074
|
...opts,
|
|
5075
|
+
model: opts.goalModel,
|
|
4992
5076
|
context: {
|
|
4993
5077
|
loopId: loop.id,
|
|
4994
5078
|
loopName: loop.name,
|
|
@@ -4997,7 +5081,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4997
5081
|
workflowId: workflow.id,
|
|
4998
5082
|
workflowName: workflow.name
|
|
4999
5083
|
},
|
|
5000
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5084
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5001
5085
|
...opts,
|
|
5002
5086
|
loop,
|
|
5003
5087
|
loopRun: run,
|
|
@@ -6477,7 +6561,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
6477
6561
|
// package.json
|
|
6478
6562
|
var package_default = {
|
|
6479
6563
|
name: "@hasna/loops",
|
|
6480
|
-
version: "0.3.
|
|
6564
|
+
version: "0.3.59",
|
|
6481
6565
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
6482
6566
|
type: "module",
|
|
6483
6567
|
main: "dist/index.js",
|
|
@@ -8617,6 +8701,47 @@ function workflowTimeoutMigrationName(workflow, timeoutMs) {
|
|
|
8617
8701
|
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8618
8702
|
return `${workflow.name}-${policy}-${suffix}`;
|
|
8619
8703
|
}
|
|
8704
|
+
function workflowGoalWrapperMigrationName(workflow) {
|
|
8705
|
+
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8706
|
+
return `${workflow.name}-no-workflow-goal-${suffix}`;
|
|
8707
|
+
}
|
|
8708
|
+
function publicMigrationGoalSummary(goal) {
|
|
8709
|
+
if (!goal)
|
|
8710
|
+
return;
|
|
8711
|
+
return {
|
|
8712
|
+
objective: redact(goal.objective),
|
|
8713
|
+
model: goal.model,
|
|
8714
|
+
tokenBudget: goal.tokenBudget,
|
|
8715
|
+
maxTurns: goal.maxTurns
|
|
8716
|
+
};
|
|
8717
|
+
}
|
|
8718
|
+
function publicMigrationWorkflowSummary(workflow) {
|
|
8719
|
+
return {
|
|
8720
|
+
id: workflow.id,
|
|
8721
|
+
name: workflow.name,
|
|
8722
|
+
version: workflow.version,
|
|
8723
|
+
status: workflow.status,
|
|
8724
|
+
stepCount: workflow.steps.length,
|
|
8725
|
+
hasGoal: Boolean(workflow.goal),
|
|
8726
|
+
goal: publicMigrationGoalSummary(workflow.goal)
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
function publicMigrationLoopSummary(loop) {
|
|
8730
|
+
return {
|
|
8731
|
+
id: loop.id,
|
|
8732
|
+
name: loop.name,
|
|
8733
|
+
status: loop.status,
|
|
8734
|
+
archivedAt: loop.archivedAt,
|
|
8735
|
+
nextRunAt: loop.nextRunAt,
|
|
8736
|
+
hasGoal: Boolean(loop.goal),
|
|
8737
|
+
goal: publicMigrationGoalSummary(loop.goal),
|
|
8738
|
+
target: loop.target.type === "workflow" ? { type: "workflow", workflowId: loop.target.workflowId, timeoutMs: loop.target.timeoutMs } : { type: loop.target.type }
|
|
8739
|
+
};
|
|
8740
|
+
}
|
|
8741
|
+
function migrationErrorReason(error) {
|
|
8742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8743
|
+
return redact(message, 240);
|
|
8744
|
+
}
|
|
8620
8745
|
function printTextOutput(value) {
|
|
8621
8746
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
8622
8747
|
console.log(line);
|
|
@@ -11240,19 +11365,30 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
|
|
|
11240
11365
|
});
|
|
11241
11366
|
continue;
|
|
11242
11367
|
}
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11368
|
+
try {
|
|
11369
|
+
const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
|
|
11370
|
+
workflowTimeoutMs: timeoutMs,
|
|
11371
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
11372
|
+
});
|
|
11373
|
+
rows.push({
|
|
11374
|
+
loop: publicLoop(migrated.loop),
|
|
11375
|
+
previousWorkflow: publicWorkflow(migrated.previousWorkflow),
|
|
11376
|
+
workflow: publicWorkflow(migrated.workflow),
|
|
11377
|
+
archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
|
|
11378
|
+
status: "migrated",
|
|
11379
|
+
agentStepIds: migration.agentStepIds,
|
|
11380
|
+
timeoutMs
|
|
11381
|
+
});
|
|
11382
|
+
} catch (error) {
|
|
11383
|
+
rows.push({
|
|
11384
|
+
loop: publicLoop(loop),
|
|
11385
|
+
workflow: publicWorkflow(workflow),
|
|
11386
|
+
status: "blocked",
|
|
11387
|
+
reason: migrationErrorReason(error),
|
|
11388
|
+
agentStepIds: migration.agentStepIds,
|
|
11389
|
+
timeoutMs
|
|
11390
|
+
});
|
|
11391
|
+
}
|
|
11256
11392
|
}
|
|
11257
11393
|
const summary = {
|
|
11258
11394
|
apply: Boolean(opts.apply),
|
|
@@ -11268,6 +11404,94 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
|
|
|
11268
11404
|
store.close();
|
|
11269
11405
|
}
|
|
11270
11406
|
});
|
|
11407
|
+
workflows.command("migrate-goal-wrappers").description("append-only migrate active workflow loops away from workflow-level goal wrappers").option("--loop <idOrName>", "migrate only one loop instead of all active workflow loops").option("--apply", "create new workflow specs and retarget eligible loops").option("--archive-old", "archive old workflow specs after retargeting when no active loops still reference them").action((opts) => {
|
|
11408
|
+
const store = new Store;
|
|
11409
|
+
try {
|
|
11410
|
+
const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
|
|
11411
|
+
const rows = [];
|
|
11412
|
+
for (const loop of candidateLoops) {
|
|
11413
|
+
if (loop.archivedAt) {
|
|
11414
|
+
rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is archived" });
|
|
11415
|
+
continue;
|
|
11416
|
+
}
|
|
11417
|
+
if (loop.target.type !== "workflow") {
|
|
11418
|
+
rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is not a workflow loop" });
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
11422
|
+
if (!loop.goal) {
|
|
11423
|
+
rows.push({
|
|
11424
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11425
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11426
|
+
status: "skipped",
|
|
11427
|
+
reason: "loop has no loop-level goal wrapper"
|
|
11428
|
+
});
|
|
11429
|
+
continue;
|
|
11430
|
+
}
|
|
11431
|
+
if (!workflow.goal) {
|
|
11432
|
+
rows.push({
|
|
11433
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11434
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11435
|
+
status: "skipped",
|
|
11436
|
+
reason: "workflow has no top-level goal wrapper"
|
|
11437
|
+
});
|
|
11438
|
+
continue;
|
|
11439
|
+
}
|
|
11440
|
+
const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
|
|
11441
|
+
if (store.hasRunningRun(loop.id)) {
|
|
11442
|
+
rows.push({
|
|
11443
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11444
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11445
|
+
status: "blocked",
|
|
11446
|
+
reason: "loop has a running run; retry after it finishes"
|
|
11447
|
+
});
|
|
11448
|
+
continue;
|
|
11449
|
+
}
|
|
11450
|
+
if (!opts.apply) {
|
|
11451
|
+
rows.push({
|
|
11452
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11453
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11454
|
+
status: "would_migrate",
|
|
11455
|
+
nextWorkflowName,
|
|
11456
|
+
removedGoal: publicMigrationGoalSummary(workflow.goal)
|
|
11457
|
+
});
|
|
11458
|
+
continue;
|
|
11459
|
+
}
|
|
11460
|
+
try {
|
|
11461
|
+
const migrated = store.cloneWorkflowWithoutGoalAndRetargetLoop(loop.id, {
|
|
11462
|
+
workflowName: nextWorkflowName,
|
|
11463
|
+
workflowTimeoutMs: loop.target.timeoutMs,
|
|
11464
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
11465
|
+
});
|
|
11466
|
+
rows.push({
|
|
11467
|
+
loop: publicMigrationLoopSummary(migrated.loop),
|
|
11468
|
+
previousWorkflow: publicMigrationWorkflowSummary(migrated.previousWorkflow),
|
|
11469
|
+
workflow: publicMigrationWorkflowSummary(migrated.workflow),
|
|
11470
|
+
archivedOld: migrated.archivedOld ? publicMigrationWorkflowSummary(migrated.archivedOld) : undefined,
|
|
11471
|
+
status: "migrated"
|
|
11472
|
+
});
|
|
11473
|
+
} catch (error) {
|
|
11474
|
+
rows.push({
|
|
11475
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11476
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11477
|
+
status: "blocked",
|
|
11478
|
+
reason: migrationErrorReason(error)
|
|
11479
|
+
});
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11482
|
+
const summary = {
|
|
11483
|
+
apply: Boolean(opts.apply),
|
|
11484
|
+
total: rows.length,
|
|
11485
|
+
migrated: rows.filter((row) => row.status === "migrated").length,
|
|
11486
|
+
wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
|
|
11487
|
+
blocked: rows.filter((row) => row.status === "blocked").length,
|
|
11488
|
+
skipped: rows.filter((row) => row.status === "skipped").length
|
|
11489
|
+
};
|
|
11490
|
+
print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
|
|
11491
|
+
} finally {
|
|
11492
|
+
store.close();
|
|
11493
|
+
}
|
|
11494
|
+
});
|
|
11271
11495
|
workflows.command("archive <idOrName>").action((idOrName) => {
|
|
11272
11496
|
const store = new Store;
|
|
11273
11497
|
try {
|
package/dist/daemon/index.js
CHANGED
|
@@ -1260,12 +1260,21 @@ class Store {
|
|
|
1260
1260
|
if (!row)
|
|
1261
1261
|
throw new Error("daemon lease lost");
|
|
1262
1262
|
}
|
|
1263
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1264
|
+
if (!goal || target.type !== "workflow")
|
|
1265
|
+
return;
|
|
1266
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1267
|
+
if (workflow?.goal) {
|
|
1268
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1263
1271
|
createLoop(input, from = new Date) {
|
|
1264
1272
|
const now = nowIso();
|
|
1265
1273
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1266
1274
|
name: "loop-target-validation",
|
|
1267
1275
|
steps: [{ id: "target", target: input.target }]
|
|
1268
1276
|
}).steps[0].target;
|
|
1277
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1269
1278
|
const loop = {
|
|
1270
1279
|
id: genId(),
|
|
1271
1280
|
name: input.name,
|
|
@@ -1437,6 +1446,9 @@ class Store {
|
|
|
1437
1446
|
if (this.hasRunningRun(current.id))
|
|
1438
1447
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1439
1448
|
const workflow = this.requireWorkflow(workflowId);
|
|
1449
|
+
if (current.goal && workflow.goal) {
|
|
1450
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1451
|
+
}
|
|
1440
1452
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1441
1453
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1442
1454
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1475,6 +1487,9 @@ class Store {
|
|
|
1475
1487
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1476
1488
|
if (this.hasRunningRun(current.id))
|
|
1477
1489
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1490
|
+
if (current.goal && normalized.goal) {
|
|
1491
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1492
|
+
}
|
|
1478
1493
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1479
1494
|
const workflow = {
|
|
1480
1495
|
id: genId(),
|
|
@@ -1528,6 +1543,70 @@ class Store {
|
|
|
1528
1543
|
throw error;
|
|
1529
1544
|
}
|
|
1530
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1531
1610
|
renameLoop(id, name, opts = {}) {
|
|
1532
1611
|
const current = this.getLoop(id);
|
|
1533
1612
|
if (!current)
|
|
@@ -4617,6 +4696,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4617
4696
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4618
4697
|
return runGoal(store, workflow.goal, {
|
|
4619
4698
|
...opts,
|
|
4699
|
+
model: opts.goalModel,
|
|
4620
4700
|
context: {
|
|
4621
4701
|
loopId: opts.loop?.id,
|
|
4622
4702
|
loopName: opts.loop?.name,
|
|
@@ -4709,6 +4789,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4709
4789
|
if (step.goal) {
|
|
4710
4790
|
result = await runGoal(store, step.goal, {
|
|
4711
4791
|
...opts,
|
|
4792
|
+
model: opts.goalModel,
|
|
4712
4793
|
target: targetWithStepAccount(step),
|
|
4713
4794
|
signal: controller.signal,
|
|
4714
4795
|
context: {
|
|
@@ -4850,6 +4931,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4850
4931
|
if (loop.goal) {
|
|
4851
4932
|
return runGoal(store, loop.goal, {
|
|
4852
4933
|
...opts,
|
|
4934
|
+
model: opts.goalModel,
|
|
4853
4935
|
target: loop.target,
|
|
4854
4936
|
context: {
|
|
4855
4937
|
loopId: loop.id,
|
|
@@ -4871,8 +4953,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4871
4953
|
}
|
|
4872
4954
|
}
|
|
4873
4955
|
if (loop.goal) {
|
|
4956
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4874
4957
|
return runGoal(store, loop.goal, {
|
|
4875
4958
|
...opts,
|
|
4959
|
+
model: opts.goalModel,
|
|
4876
4960
|
context: {
|
|
4877
4961
|
loopId: loop.id,
|
|
4878
4962
|
loopName: loop.name,
|
|
@@ -4881,7 +4965,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4881
4965
|
workflowId: workflow.id,
|
|
4882
4966
|
workflowName: workflow.name
|
|
4883
4967
|
},
|
|
4884
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
4968
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
4885
4969
|
...opts,
|
|
4886
4970
|
loop,
|
|
4887
4971
|
loopRun: run,
|
|
@@ -5611,7 +5695,7 @@ function enableStartup(result) {
|
|
|
5611
5695
|
// package.json
|
|
5612
5696
|
var package_default = {
|
|
5613
5697
|
name: "@hasna/loops",
|
|
5614
|
-
version: "0.3.
|
|
5698
|
+
version: "0.3.59",
|
|
5615
5699
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5616
5700
|
type: "module",
|
|
5617
5701
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -1258,12 +1258,21 @@ class Store {
|
|
|
1258
1258
|
if (!row)
|
|
1259
1259
|
throw new Error("daemon lease lost");
|
|
1260
1260
|
}
|
|
1261
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1262
|
+
if (!goal || target.type !== "workflow")
|
|
1263
|
+
return;
|
|
1264
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1265
|
+
if (workflow?.goal) {
|
|
1266
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1261
1269
|
createLoop(input, from = new Date) {
|
|
1262
1270
|
const now = nowIso();
|
|
1263
1271
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1264
1272
|
name: "loop-target-validation",
|
|
1265
1273
|
steps: [{ id: "target", target: input.target }]
|
|
1266
1274
|
}).steps[0].target;
|
|
1275
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1267
1276
|
const loop = {
|
|
1268
1277
|
id: genId(),
|
|
1269
1278
|
name: input.name,
|
|
@@ -1435,6 +1444,9 @@ class Store {
|
|
|
1435
1444
|
if (this.hasRunningRun(current.id))
|
|
1436
1445
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1437
1446
|
const workflow = this.requireWorkflow(workflowId);
|
|
1447
|
+
if (current.goal && workflow.goal) {
|
|
1448
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1449
|
+
}
|
|
1438
1450
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1439
1451
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1440
1452
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1473,6 +1485,9 @@ class Store {
|
|
|
1473
1485
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1474
1486
|
if (this.hasRunningRun(current.id))
|
|
1475
1487
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1488
|
+
if (current.goal && normalized.goal) {
|
|
1489
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1490
|
+
}
|
|
1476
1491
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1477
1492
|
const workflow = {
|
|
1478
1493
|
id: genId(),
|
|
@@ -1526,6 +1541,70 @@ class Store {
|
|
|
1526
1541
|
throw error;
|
|
1527
1542
|
}
|
|
1528
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1529
1608
|
renameLoop(id, name, opts = {}) {
|
|
1530
1609
|
const current = this.getLoop(id);
|
|
1531
1610
|
if (!current)
|
|
@@ -4506,7 +4585,7 @@ function publicGoalRun(run) {
|
|
|
4506
4585
|
// package.json
|
|
4507
4586
|
var package_default = {
|
|
4508
4587
|
name: "@hasna/loops",
|
|
4509
|
-
version: "0.3.
|
|
4588
|
+
version: "0.3.59",
|
|
4510
4589
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4511
4590
|
type: "module",
|
|
4512
4591
|
main: "dist/index.js",
|
|
@@ -5047,6 +5126,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5047
5126
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
5048
5127
|
return runGoal(store, workflow.goal, {
|
|
5049
5128
|
...opts,
|
|
5129
|
+
model: opts.goalModel,
|
|
5050
5130
|
context: {
|
|
5051
5131
|
loopId: opts.loop?.id,
|
|
5052
5132
|
loopName: opts.loop?.name,
|
|
@@ -5139,6 +5219,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5139
5219
|
if (step.goal) {
|
|
5140
5220
|
result = await runGoal(store, step.goal, {
|
|
5141
5221
|
...opts,
|
|
5222
|
+
model: opts.goalModel,
|
|
5142
5223
|
target: targetWithStepAccount(step),
|
|
5143
5224
|
signal: controller.signal,
|
|
5144
5225
|
context: {
|
|
@@ -5280,6 +5361,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5280
5361
|
if (loop.goal) {
|
|
5281
5362
|
return runGoal(store, loop.goal, {
|
|
5282
5363
|
...opts,
|
|
5364
|
+
model: opts.goalModel,
|
|
5283
5365
|
target: loop.target,
|
|
5284
5366
|
context: {
|
|
5285
5367
|
loopId: loop.id,
|
|
@@ -5301,8 +5383,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5301
5383
|
}
|
|
5302
5384
|
}
|
|
5303
5385
|
if (loop.goal) {
|
|
5386
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
5304
5387
|
return runGoal(store, loop.goal, {
|
|
5305
5388
|
...opts,
|
|
5389
|
+
model: opts.goalModel,
|
|
5306
5390
|
context: {
|
|
5307
5391
|
loopId: loop.id,
|
|
5308
5392
|
loopName: loop.name,
|
|
@@ -5311,7 +5395,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5311
5395
|
workflowId: workflow.id,
|
|
5312
5396
|
workflowName: workflow.name
|
|
5313
5397
|
},
|
|
5314
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5398
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5315
5399
|
...opts,
|
|
5316
5400
|
loop,
|
|
5317
5401
|
loopRun: run,
|
package/dist/lib/executor.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
1
2
|
import type { ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
|
|
2
3
|
export interface ExecuteOptions extends PersistGuardOptions {
|
|
3
4
|
maxOutputBytes?: number;
|
|
4
5
|
env?: NodeJS.ProcessEnv;
|
|
6
|
+
goalModel?: LanguageModel;
|
|
5
7
|
log?: (message: string) => void;
|
|
6
8
|
signal?: AbortSignal;
|
|
7
9
|
onSpawn?: (pid: number) => void;
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare class Store {
|
|
|
71
71
|
private addColumnIfMissing;
|
|
72
72
|
private createWorkflowRunBackfillIndexes;
|
|
73
73
|
private assertDaemonLeaseFence;
|
|
74
|
+
private assertNoNestedWorkflowGoal;
|
|
74
75
|
createLoop(input: CreateLoopInput, from?: Date): Loop;
|
|
75
76
|
getLoop(id: string): Loop | undefined;
|
|
76
77
|
findLoopByName(name: string): Loop | undefined;
|
|
@@ -98,6 +99,16 @@ export declare class Store {
|
|
|
98
99
|
previousWorkflow: WorkflowSpec;
|
|
99
100
|
archivedOld?: WorkflowSpec;
|
|
100
101
|
};
|
|
102
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName: string, opts: DaemonLeaseFence & {
|
|
103
|
+
workflowName: string;
|
|
104
|
+
workflowTimeoutMs?: TimeoutMs;
|
|
105
|
+
archiveOld?: boolean;
|
|
106
|
+
}): {
|
|
107
|
+
loop: Loop;
|
|
108
|
+
workflow: WorkflowSpec;
|
|
109
|
+
previousWorkflow: WorkflowSpec;
|
|
110
|
+
archivedOld?: WorkflowSpec;
|
|
111
|
+
};
|
|
101
112
|
renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
|
|
102
113
|
archiveLoop(idOrName: string): Loop;
|
|
103
114
|
unarchiveLoop(idOrName: string): Loop;
|
package/dist/lib/store.js
CHANGED
|
@@ -1258,12 +1258,21 @@ class Store {
|
|
|
1258
1258
|
if (!row)
|
|
1259
1259
|
throw new Error("daemon lease lost");
|
|
1260
1260
|
}
|
|
1261
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1262
|
+
if (!goal || target.type !== "workflow")
|
|
1263
|
+
return;
|
|
1264
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1265
|
+
if (workflow?.goal) {
|
|
1266
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1261
1269
|
createLoop(input, from = new Date) {
|
|
1262
1270
|
const now = nowIso();
|
|
1263
1271
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1264
1272
|
name: "loop-target-validation",
|
|
1265
1273
|
steps: [{ id: "target", target: input.target }]
|
|
1266
1274
|
}).steps[0].target;
|
|
1275
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1267
1276
|
const loop = {
|
|
1268
1277
|
id: genId(),
|
|
1269
1278
|
name: input.name,
|
|
@@ -1435,6 +1444,9 @@ class Store {
|
|
|
1435
1444
|
if (this.hasRunningRun(current.id))
|
|
1436
1445
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1437
1446
|
const workflow = this.requireWorkflow(workflowId);
|
|
1447
|
+
if (current.goal && workflow.goal) {
|
|
1448
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1449
|
+
}
|
|
1438
1450
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1439
1451
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1440
1452
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1473,6 +1485,9 @@ class Store {
|
|
|
1473
1485
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1474
1486
|
if (this.hasRunningRun(current.id))
|
|
1475
1487
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1488
|
+
if (current.goal && normalized.goal) {
|
|
1489
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1490
|
+
}
|
|
1476
1491
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1477
1492
|
const workflow = {
|
|
1478
1493
|
id: genId(),
|
|
@@ -1526,6 +1541,70 @@ class Store {
|
|
|
1526
1541
|
throw error;
|
|
1527
1542
|
}
|
|
1528
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1529
1608
|
renameLoop(id, name, opts = {}) {
|
|
1530
1609
|
const current = this.getLoop(id);
|
|
1531
1610
|
if (!current)
|
package/dist/mcp/index.js
CHANGED
|
@@ -1260,12 +1260,21 @@ class Store {
|
|
|
1260
1260
|
if (!row)
|
|
1261
1261
|
throw new Error("daemon lease lost");
|
|
1262
1262
|
}
|
|
1263
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1264
|
+
if (!goal || target.type !== "workflow")
|
|
1265
|
+
return;
|
|
1266
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1267
|
+
if (workflow?.goal) {
|
|
1268
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1263
1271
|
createLoop(input, from = new Date) {
|
|
1264
1272
|
const now = nowIso();
|
|
1265
1273
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1266
1274
|
name: "loop-target-validation",
|
|
1267
1275
|
steps: [{ id: "target", target: input.target }]
|
|
1268
1276
|
}).steps[0].target;
|
|
1277
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1269
1278
|
const loop = {
|
|
1270
1279
|
id: genId(),
|
|
1271
1280
|
name: input.name,
|
|
@@ -1437,6 +1446,9 @@ class Store {
|
|
|
1437
1446
|
if (this.hasRunningRun(current.id))
|
|
1438
1447
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1439
1448
|
const workflow = this.requireWorkflow(workflowId);
|
|
1449
|
+
if (current.goal && workflow.goal) {
|
|
1450
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1451
|
+
}
|
|
1440
1452
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1441
1453
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1442
1454
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1475,6 +1487,9 @@ class Store {
|
|
|
1475
1487
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1476
1488
|
if (this.hasRunningRun(current.id))
|
|
1477
1489
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1490
|
+
if (current.goal && normalized.goal) {
|
|
1491
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1492
|
+
}
|
|
1478
1493
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1479
1494
|
const workflow = {
|
|
1480
1495
|
id: genId(),
|
|
@@ -1528,6 +1543,70 @@ class Store {
|
|
|
1528
1543
|
throw error;
|
|
1529
1544
|
}
|
|
1530
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1531
1610
|
renameLoop(id, name, opts = {}) {
|
|
1532
1611
|
const current = this.getLoop(id);
|
|
1533
1612
|
if (!current)
|
|
@@ -4508,7 +4587,7 @@ function publicGoalRun(run) {
|
|
|
4508
4587
|
// package.json
|
|
4509
4588
|
var package_default = {
|
|
4510
4589
|
name: "@hasna/loops",
|
|
4511
|
-
version: "0.3.
|
|
4590
|
+
version: "0.3.59",
|
|
4512
4591
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4513
4592
|
type: "module",
|
|
4514
4593
|
main: "dist/index.js",
|
|
@@ -5049,6 +5128,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5049
5128
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
5050
5129
|
return runGoal(store, workflow.goal, {
|
|
5051
5130
|
...opts,
|
|
5131
|
+
model: opts.goalModel,
|
|
5052
5132
|
context: {
|
|
5053
5133
|
loopId: opts.loop?.id,
|
|
5054
5134
|
loopName: opts.loop?.name,
|
|
@@ -5141,6 +5221,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5141
5221
|
if (step.goal) {
|
|
5142
5222
|
result = await runGoal(store, step.goal, {
|
|
5143
5223
|
...opts,
|
|
5224
|
+
model: opts.goalModel,
|
|
5144
5225
|
target: targetWithStepAccount(step),
|
|
5145
5226
|
signal: controller.signal,
|
|
5146
5227
|
context: {
|
|
@@ -5282,6 +5363,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5282
5363
|
if (loop.goal) {
|
|
5283
5364
|
return runGoal(store, loop.goal, {
|
|
5284
5365
|
...opts,
|
|
5366
|
+
model: opts.goalModel,
|
|
5285
5367
|
target: loop.target,
|
|
5286
5368
|
context: {
|
|
5287
5369
|
loopId: loop.id,
|
|
@@ -5303,8 +5385,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5303
5385
|
}
|
|
5304
5386
|
}
|
|
5305
5387
|
if (loop.goal) {
|
|
5388
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
5306
5389
|
return runGoal(store, loop.goal, {
|
|
5307
5390
|
...opts,
|
|
5391
|
+
model: opts.goalModel,
|
|
5308
5392
|
context: {
|
|
5309
5393
|
loopId: loop.id,
|
|
5310
5394
|
loopName: loop.name,
|
|
@@ -5313,7 +5397,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5313
5397
|
workflowId: workflow.id,
|
|
5314
5398
|
workflowName: workflow.name
|
|
5315
5399
|
},
|
|
5316
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5400
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5317
5401
|
...opts,
|
|
5318
5402
|
loop,
|
|
5319
5403
|
loopRun: run,
|
package/dist/sdk/index.js
CHANGED
|
@@ -1258,12 +1258,21 @@ class Store {
|
|
|
1258
1258
|
if (!row)
|
|
1259
1259
|
throw new Error("daemon lease lost");
|
|
1260
1260
|
}
|
|
1261
|
+
assertNoNestedWorkflowGoal(target, goal) {
|
|
1262
|
+
if (!goal || target.type !== "workflow")
|
|
1263
|
+
return;
|
|
1264
|
+
const workflow = this.getWorkflow(target.workflowId);
|
|
1265
|
+
if (workflow?.goal) {
|
|
1266
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1261
1269
|
createLoop(input, from = new Date) {
|
|
1262
1270
|
const now = nowIso();
|
|
1263
1271
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1264
1272
|
name: "loop-target-validation",
|
|
1265
1273
|
steps: [{ id: "target", target: input.target }]
|
|
1266
1274
|
}).steps[0].target;
|
|
1275
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1267
1276
|
const loop = {
|
|
1268
1277
|
id: genId(),
|
|
1269
1278
|
name: input.name,
|
|
@@ -1435,6 +1444,9 @@ class Store {
|
|
|
1435
1444
|
if (this.hasRunningRun(current.id))
|
|
1436
1445
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1437
1446
|
const workflow = this.requireWorkflow(workflowId);
|
|
1447
|
+
if (current.goal && workflow.goal) {
|
|
1448
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
|
|
1449
|
+
}
|
|
1438
1450
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1439
1451
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1440
1452
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1473,6 +1485,9 @@ class Store {
|
|
|
1473
1485
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1474
1486
|
if (this.hasRunningRun(current.id))
|
|
1475
1487
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1488
|
+
if (current.goal && normalized.goal) {
|
|
1489
|
+
throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
|
|
1490
|
+
}
|
|
1476
1491
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1477
1492
|
const workflow = {
|
|
1478
1493
|
id: genId(),
|
|
@@ -1526,6 +1541,70 @@ class Store {
|
|
|
1526
1541
|
throw error;
|
|
1527
1542
|
}
|
|
1528
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1529
1608
|
renameLoop(id, name, opts = {}) {
|
|
1530
1609
|
const current = this.getLoop(id);
|
|
1531
1610
|
if (!current)
|
|
@@ -4607,6 +4686,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4607
4686
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4608
4687
|
return runGoal(store, workflow.goal, {
|
|
4609
4688
|
...opts,
|
|
4689
|
+
model: opts.goalModel,
|
|
4610
4690
|
context: {
|
|
4611
4691
|
loopId: opts.loop?.id,
|
|
4612
4692
|
loopName: opts.loop?.name,
|
|
@@ -4699,6 +4779,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4699
4779
|
if (step.goal) {
|
|
4700
4780
|
result = await runGoal(store, step.goal, {
|
|
4701
4781
|
...opts,
|
|
4782
|
+
model: opts.goalModel,
|
|
4702
4783
|
target: targetWithStepAccount(step),
|
|
4703
4784
|
signal: controller.signal,
|
|
4704
4785
|
context: {
|
|
@@ -4840,6 +4921,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4840
4921
|
if (loop.goal) {
|
|
4841
4922
|
return runGoal(store, loop.goal, {
|
|
4842
4923
|
...opts,
|
|
4924
|
+
model: opts.goalModel,
|
|
4843
4925
|
target: loop.target,
|
|
4844
4926
|
context: {
|
|
4845
4927
|
loopId: loop.id,
|
|
@@ -4861,8 +4943,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4861
4943
|
}
|
|
4862
4944
|
}
|
|
4863
4945
|
if (loop.goal) {
|
|
4946
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4864
4947
|
return runGoal(store, loop.goal, {
|
|
4865
4948
|
...opts,
|
|
4949
|
+
model: opts.goalModel,
|
|
4866
4950
|
context: {
|
|
4867
4951
|
loopId: loop.id,
|
|
4868
4952
|
loopName: loop.name,
|
|
@@ -4871,7 +4955,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4871
4955
|
workflowId: workflow.id,
|
|
4872
4956
|
workflowName: workflow.name
|
|
4873
4957
|
},
|
|
4874
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
4958
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
4875
4959
|
...opts,
|
|
4876
4960
|
loop,
|
|
4877
4961
|
loopRun: run,
|
package/package.json
CHANGED