@hasna/loops 0.3.57 → 0.3.58
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 +97 -2
- package/dist/daemon/index.js +22 -2
- package/dist/index.js +22 -2
- package/dist/lib/executor.d.ts +2 -0
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +15 -0
- package/dist/mcp/index.js +22 -2
- package/dist/sdk/index.js +21 -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(),
|
|
@@ -4733,6 +4748,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4733
4748
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4734
4749
|
return runGoal(store, workflow.goal, {
|
|
4735
4750
|
...opts,
|
|
4751
|
+
model: opts.goalModel,
|
|
4736
4752
|
context: {
|
|
4737
4753
|
loopId: opts.loop?.id,
|
|
4738
4754
|
loopName: opts.loop?.name,
|
|
@@ -4825,6 +4841,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4825
4841
|
if (step.goal) {
|
|
4826
4842
|
result = await runGoal(store, step.goal, {
|
|
4827
4843
|
...opts,
|
|
4844
|
+
model: opts.goalModel,
|
|
4828
4845
|
target: targetWithStepAccount(step),
|
|
4829
4846
|
signal: controller.signal,
|
|
4830
4847
|
context: {
|
|
@@ -4966,6 +4983,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4966
4983
|
if (loop.goal) {
|
|
4967
4984
|
return runGoal(store, loop.goal, {
|
|
4968
4985
|
...opts,
|
|
4986
|
+
model: opts.goalModel,
|
|
4969
4987
|
target: loop.target,
|
|
4970
4988
|
context: {
|
|
4971
4989
|
loopId: loop.id,
|
|
@@ -4987,8 +5005,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4987
5005
|
}
|
|
4988
5006
|
}
|
|
4989
5007
|
if (loop.goal) {
|
|
5008
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4990
5009
|
return runGoal(store, loop.goal, {
|
|
4991
5010
|
...opts,
|
|
5011
|
+
model: opts.goalModel,
|
|
4992
5012
|
context: {
|
|
4993
5013
|
loopId: loop.id,
|
|
4994
5014
|
loopName: loop.name,
|
|
@@ -4997,7 +5017,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4997
5017
|
workflowId: workflow.id,
|
|
4998
5018
|
workflowName: workflow.name
|
|
4999
5019
|
},
|
|
5000
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5020
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5001
5021
|
...opts,
|
|
5002
5022
|
loop,
|
|
5003
5023
|
loopRun: run,
|
|
@@ -6477,7 +6497,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
6477
6497
|
// package.json
|
|
6478
6498
|
var package_default = {
|
|
6479
6499
|
name: "@hasna/loops",
|
|
6480
|
-
version: "0.3.
|
|
6500
|
+
version: "0.3.58",
|
|
6481
6501
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
6482
6502
|
type: "module",
|
|
6483
6503
|
main: "dist/index.js",
|
|
@@ -8617,6 +8637,21 @@ function workflowTimeoutMigrationName(workflow, timeoutMs) {
|
|
|
8617
8637
|
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8618
8638
|
return `${workflow.name}-${policy}-${suffix}`;
|
|
8619
8639
|
}
|
|
8640
|
+
function workflowWithoutGoalWrapper(workflow, opts = {}) {
|
|
8641
|
+
return {
|
|
8642
|
+
body: {
|
|
8643
|
+
name: opts.name ?? workflow.name,
|
|
8644
|
+
description: workflow.description,
|
|
8645
|
+
version: workflow.version,
|
|
8646
|
+
steps: workflow.steps
|
|
8647
|
+
},
|
|
8648
|
+
changed: workflow.goal !== undefined
|
|
8649
|
+
};
|
|
8650
|
+
}
|
|
8651
|
+
function workflowGoalWrapperMigrationName(workflow) {
|
|
8652
|
+
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8653
|
+
return `${workflow.name}-no-workflow-goal-${suffix}`;
|
|
8654
|
+
}
|
|
8620
8655
|
function printTextOutput(value) {
|
|
8621
8656
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
8622
8657
|
console.log(line);
|
|
@@ -11268,6 +11303,66 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
|
|
|
11268
11303
|
store.close();
|
|
11269
11304
|
}
|
|
11270
11305
|
});
|
|
11306
|
+
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) => {
|
|
11307
|
+
const store = new Store;
|
|
11308
|
+
try {
|
|
11309
|
+
const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
|
|
11310
|
+
const rows = [];
|
|
11311
|
+
for (const loop of candidateLoops) {
|
|
11312
|
+
if (loop.archivedAt) {
|
|
11313
|
+
rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
|
|
11314
|
+
continue;
|
|
11315
|
+
}
|
|
11316
|
+
if (loop.target.type !== "workflow") {
|
|
11317
|
+
rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
|
|
11318
|
+
continue;
|
|
11319
|
+
}
|
|
11320
|
+
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
11321
|
+
const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
|
|
11322
|
+
const migration = workflowWithoutGoalWrapper(workflow, { name: nextWorkflowName });
|
|
11323
|
+
if (!migration.changed) {
|
|
11324
|
+
rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no top-level goal wrapper" });
|
|
11325
|
+
continue;
|
|
11326
|
+
}
|
|
11327
|
+
if (store.hasRunningRun(loop.id)) {
|
|
11328
|
+
rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
|
|
11329
|
+
continue;
|
|
11330
|
+
}
|
|
11331
|
+
if (!opts.apply) {
|
|
11332
|
+
rows.push({
|
|
11333
|
+
loop: publicLoop(loop),
|
|
11334
|
+
workflow: publicWorkflow(workflow),
|
|
11335
|
+
status: "would_migrate",
|
|
11336
|
+
nextWorkflowName,
|
|
11337
|
+
removedGoal: workflow.goal
|
|
11338
|
+
});
|
|
11339
|
+
continue;
|
|
11340
|
+
}
|
|
11341
|
+
const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
|
|
11342
|
+
workflowTimeoutMs: loop.target.timeoutMs,
|
|
11343
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
11344
|
+
});
|
|
11345
|
+
rows.push({
|
|
11346
|
+
loop: publicLoop(migrated.loop),
|
|
11347
|
+
previousWorkflow: publicWorkflow(migrated.previousWorkflow),
|
|
11348
|
+
workflow: publicWorkflow(migrated.workflow),
|
|
11349
|
+
archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
|
|
11350
|
+
status: "migrated"
|
|
11351
|
+
});
|
|
11352
|
+
}
|
|
11353
|
+
const summary = {
|
|
11354
|
+
apply: Boolean(opts.apply),
|
|
11355
|
+
total: rows.length,
|
|
11356
|
+
migrated: rows.filter((row) => row.status === "migrated").length,
|
|
11357
|
+
wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
|
|
11358
|
+
blocked: rows.filter((row) => row.status === "blocked").length,
|
|
11359
|
+
skipped: rows.filter((row) => row.status === "skipped").length
|
|
11360
|
+
};
|
|
11361
|
+
print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
|
|
11362
|
+
} finally {
|
|
11363
|
+
store.close();
|
|
11364
|
+
}
|
|
11365
|
+
});
|
|
11271
11366
|
workflows.command("archive <idOrName>").action((idOrName) => {
|
|
11272
11367
|
const store = new Store;
|
|
11273
11368
|
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(),
|
|
@@ -4617,6 +4632,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4617
4632
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4618
4633
|
return runGoal(store, workflow.goal, {
|
|
4619
4634
|
...opts,
|
|
4635
|
+
model: opts.goalModel,
|
|
4620
4636
|
context: {
|
|
4621
4637
|
loopId: opts.loop?.id,
|
|
4622
4638
|
loopName: opts.loop?.name,
|
|
@@ -4709,6 +4725,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4709
4725
|
if (step.goal) {
|
|
4710
4726
|
result = await runGoal(store, step.goal, {
|
|
4711
4727
|
...opts,
|
|
4728
|
+
model: opts.goalModel,
|
|
4712
4729
|
target: targetWithStepAccount(step),
|
|
4713
4730
|
signal: controller.signal,
|
|
4714
4731
|
context: {
|
|
@@ -4850,6 +4867,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4850
4867
|
if (loop.goal) {
|
|
4851
4868
|
return runGoal(store, loop.goal, {
|
|
4852
4869
|
...opts,
|
|
4870
|
+
model: opts.goalModel,
|
|
4853
4871
|
target: loop.target,
|
|
4854
4872
|
context: {
|
|
4855
4873
|
loopId: loop.id,
|
|
@@ -4871,8 +4889,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4871
4889
|
}
|
|
4872
4890
|
}
|
|
4873
4891
|
if (loop.goal) {
|
|
4892
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4874
4893
|
return runGoal(store, loop.goal, {
|
|
4875
4894
|
...opts,
|
|
4895
|
+
model: opts.goalModel,
|
|
4876
4896
|
context: {
|
|
4877
4897
|
loopId: loop.id,
|
|
4878
4898
|
loopName: loop.name,
|
|
@@ -4881,7 +4901,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4881
4901
|
workflowId: workflow.id,
|
|
4882
4902
|
workflowName: workflow.name
|
|
4883
4903
|
},
|
|
4884
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
4904
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
4885
4905
|
...opts,
|
|
4886
4906
|
loop,
|
|
4887
4907
|
loopRun: run,
|
|
@@ -5611,7 +5631,7 @@ function enableStartup(result) {
|
|
|
5611
5631
|
// package.json
|
|
5612
5632
|
var package_default = {
|
|
5613
5633
|
name: "@hasna/loops",
|
|
5614
|
-
version: "0.3.
|
|
5634
|
+
version: "0.3.58",
|
|
5615
5635
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5616
5636
|
type: "module",
|
|
5617
5637
|
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(),
|
|
@@ -4506,7 +4521,7 @@ function publicGoalRun(run) {
|
|
|
4506
4521
|
// package.json
|
|
4507
4522
|
var package_default = {
|
|
4508
4523
|
name: "@hasna/loops",
|
|
4509
|
-
version: "0.3.
|
|
4524
|
+
version: "0.3.58",
|
|
4510
4525
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4511
4526
|
type: "module",
|
|
4512
4527
|
main: "dist/index.js",
|
|
@@ -5047,6 +5062,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5047
5062
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
5048
5063
|
return runGoal(store, workflow.goal, {
|
|
5049
5064
|
...opts,
|
|
5065
|
+
model: opts.goalModel,
|
|
5050
5066
|
context: {
|
|
5051
5067
|
loopId: opts.loop?.id,
|
|
5052
5068
|
loopName: opts.loop?.name,
|
|
@@ -5139,6 +5155,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5139
5155
|
if (step.goal) {
|
|
5140
5156
|
result = await runGoal(store, step.goal, {
|
|
5141
5157
|
...opts,
|
|
5158
|
+
model: opts.goalModel,
|
|
5142
5159
|
target: targetWithStepAccount(step),
|
|
5143
5160
|
signal: controller.signal,
|
|
5144
5161
|
context: {
|
|
@@ -5280,6 +5297,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5280
5297
|
if (loop.goal) {
|
|
5281
5298
|
return runGoal(store, loop.goal, {
|
|
5282
5299
|
...opts,
|
|
5300
|
+
model: opts.goalModel,
|
|
5283
5301
|
target: loop.target,
|
|
5284
5302
|
context: {
|
|
5285
5303
|
loopId: loop.id,
|
|
@@ -5301,8 +5319,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5301
5319
|
}
|
|
5302
5320
|
}
|
|
5303
5321
|
if (loop.goal) {
|
|
5322
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
5304
5323
|
return runGoal(store, loop.goal, {
|
|
5305
5324
|
...opts,
|
|
5325
|
+
model: opts.goalModel,
|
|
5306
5326
|
context: {
|
|
5307
5327
|
loopId: loop.id,
|
|
5308
5328
|
loopName: loop.name,
|
|
@@ -5311,7 +5331,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5311
5331
|
workflowId: workflow.id,
|
|
5312
5332
|
workflowName: workflow.name
|
|
5313
5333
|
},
|
|
5314
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5334
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5315
5335
|
...opts,
|
|
5316
5336
|
loop,
|
|
5317
5337
|
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;
|
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(),
|
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(),
|
|
@@ -4508,7 +4523,7 @@ function publicGoalRun(run) {
|
|
|
4508
4523
|
// package.json
|
|
4509
4524
|
var package_default = {
|
|
4510
4525
|
name: "@hasna/loops",
|
|
4511
|
-
version: "0.3.
|
|
4526
|
+
version: "0.3.58",
|
|
4512
4527
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4513
4528
|
type: "module",
|
|
4514
4529
|
main: "dist/index.js",
|
|
@@ -5049,6 +5064,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5049
5064
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
5050
5065
|
return runGoal(store, workflow.goal, {
|
|
5051
5066
|
...opts,
|
|
5067
|
+
model: opts.goalModel,
|
|
5052
5068
|
context: {
|
|
5053
5069
|
loopId: opts.loop?.id,
|
|
5054
5070
|
loopName: opts.loop?.name,
|
|
@@ -5141,6 +5157,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
5141
5157
|
if (step.goal) {
|
|
5142
5158
|
result = await runGoal(store, step.goal, {
|
|
5143
5159
|
...opts,
|
|
5160
|
+
model: opts.goalModel,
|
|
5144
5161
|
target: targetWithStepAccount(step),
|
|
5145
5162
|
signal: controller.signal,
|
|
5146
5163
|
context: {
|
|
@@ -5282,6 +5299,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5282
5299
|
if (loop.goal) {
|
|
5283
5300
|
return runGoal(store, loop.goal, {
|
|
5284
5301
|
...opts,
|
|
5302
|
+
model: opts.goalModel,
|
|
5285
5303
|
target: loop.target,
|
|
5286
5304
|
context: {
|
|
5287
5305
|
loopId: loop.id,
|
|
@@ -5303,8 +5321,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5303
5321
|
}
|
|
5304
5322
|
}
|
|
5305
5323
|
if (loop.goal) {
|
|
5324
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
5306
5325
|
return runGoal(store, loop.goal, {
|
|
5307
5326
|
...opts,
|
|
5327
|
+
model: opts.goalModel,
|
|
5308
5328
|
context: {
|
|
5309
5329
|
loopId: loop.id,
|
|
5310
5330
|
loopName: loop.name,
|
|
@@ -5313,7 +5333,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
5313
5333
|
workflowId: workflow.id,
|
|
5314
5334
|
workflowName: workflow.name
|
|
5315
5335
|
},
|
|
5316
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
5336
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
5317
5337
|
...opts,
|
|
5318
5338
|
loop,
|
|
5319
5339
|
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(),
|
|
@@ -4607,6 +4622,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4607
4622
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4608
4623
|
return runGoal(store, workflow.goal, {
|
|
4609
4624
|
...opts,
|
|
4625
|
+
model: opts.goalModel,
|
|
4610
4626
|
context: {
|
|
4611
4627
|
loopId: opts.loop?.id,
|
|
4612
4628
|
loopName: opts.loop?.name,
|
|
@@ -4699,6 +4715,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4699
4715
|
if (step.goal) {
|
|
4700
4716
|
result = await runGoal(store, step.goal, {
|
|
4701
4717
|
...opts,
|
|
4718
|
+
model: opts.goalModel,
|
|
4702
4719
|
target: targetWithStepAccount(step),
|
|
4703
4720
|
signal: controller.signal,
|
|
4704
4721
|
context: {
|
|
@@ -4840,6 +4857,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4840
4857
|
if (loop.goal) {
|
|
4841
4858
|
return runGoal(store, loop.goal, {
|
|
4842
4859
|
...opts,
|
|
4860
|
+
model: opts.goalModel,
|
|
4843
4861
|
target: loop.target,
|
|
4844
4862
|
context: {
|
|
4845
4863
|
loopId: loop.id,
|
|
@@ -4861,8 +4879,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4861
4879
|
}
|
|
4862
4880
|
}
|
|
4863
4881
|
if (loop.goal) {
|
|
4882
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4864
4883
|
return runGoal(store, loop.goal, {
|
|
4865
4884
|
...opts,
|
|
4885
|
+
model: opts.goalModel,
|
|
4866
4886
|
context: {
|
|
4867
4887
|
loopId: loop.id,
|
|
4868
4888
|
loopName: loop.name,
|
|
@@ -4871,7 +4891,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4871
4891
|
workflowId: workflow.id,
|
|
4872
4892
|
workflowName: workflow.name
|
|
4873
4893
|
},
|
|
4874
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
4894
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
4875
4895
|
...opts,
|
|
4876
4896
|
loop,
|
|
4877
4897
|
loopRun: run,
|
package/package.json
CHANGED