@hasna/loops 0.3.56 → 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/README.md +188 -14
- package/dist/cli/index.js +1421 -176
- package/dist/daemon/index.js +164 -20
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4049 -2874
- package/dist/lib/executor.d.ts +2 -0
- package/dist/lib/health.d.ts +2 -2
- package/dist/lib/store.d.ts +4 -0
- package/dist/lib/store.js +61 -8
- package/dist/lib/templates.d.ts +4 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +5841 -0
- package/dist/sdk/index.js +155 -17
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +228 -0
- package/docs/USAGE.md +123 -15
- package/package.json +9 -3
package/dist/daemon/index.js
CHANGED
|
@@ -461,6 +461,9 @@ function validateTarget(value, label, opts) {
|
|
|
461
461
|
}
|
|
462
462
|
if (value.model !== undefined)
|
|
463
463
|
assertString(value.model, `${label}.model`);
|
|
464
|
+
if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
|
|
465
|
+
throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
|
|
466
|
+
}
|
|
464
467
|
if (value.variant !== undefined)
|
|
465
468
|
assertString(value.variant, `${label}.variant`);
|
|
466
469
|
if (value.agent !== undefined)
|
|
@@ -1257,12 +1260,21 @@ class Store {
|
|
|
1257
1260
|
if (!row)
|
|
1258
1261
|
throw new Error("daemon lease lost");
|
|
1259
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
|
+
}
|
|
1260
1271
|
createLoop(input, from = new Date) {
|
|
1261
1272
|
const now = nowIso();
|
|
1262
1273
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1263
1274
|
name: "loop-target-validation",
|
|
1264
1275
|
steps: [{ id: "target", target: input.target }]
|
|
1265
1276
|
}).steps[0].target;
|
|
1277
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1266
1278
|
const loop = {
|
|
1267
1279
|
id: genId(),
|
|
1268
1280
|
name: input.name,
|
|
@@ -1434,6 +1446,9 @@ class Store {
|
|
|
1434
1446
|
if (this.hasRunningRun(current.id))
|
|
1435
1447
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1436
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
|
+
}
|
|
1437
1452
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1438
1453
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1439
1454
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1472,6 +1487,9 @@ class Store {
|
|
|
1472
1487
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1473
1488
|
if (this.hasRunningRun(current.id))
|
|
1474
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
|
+
}
|
|
1475
1493
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1476
1494
|
const workflow = {
|
|
1477
1495
|
id: genId(),
|
|
@@ -1763,7 +1781,7 @@ class Store {
|
|
|
1763
1781
|
if (!sourceDedupeKey)
|
|
1764
1782
|
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1765
1783
|
const now = nowIso();
|
|
1766
|
-
const claimableStatuses = ["queued", "deferred"
|
|
1784
|
+
const claimableStatuses = ["queued", "deferred"];
|
|
1767
1785
|
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1768
1786
|
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1769
1787
|
const result = this.db.query(`UPDATE workflow_invocations
|
|
@@ -1844,28 +1862,35 @@ class Store {
|
|
|
1844
1862
|
project_group=excluded.project_group,
|
|
1845
1863
|
priority=excluded.priority,
|
|
1846
1864
|
status=CASE
|
|
1847
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1865
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
|
|
1848
1866
|
THEN workflow_work_items.status
|
|
1849
1867
|
ELSE excluded.status
|
|
1850
1868
|
END,
|
|
1851
1869
|
workflow_id=CASE
|
|
1852
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1870
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
|
|
1853
1871
|
ELSE NULL
|
|
1854
1872
|
END,
|
|
1855
1873
|
loop_id=CASE
|
|
1856
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1874
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
|
|
1857
1875
|
ELSE NULL
|
|
1858
1876
|
END,
|
|
1859
1877
|
workflow_run_id=CASE
|
|
1860
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1878
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
|
|
1861
1879
|
ELSE NULL
|
|
1862
1880
|
END,
|
|
1863
1881
|
lease_expires_at=CASE
|
|
1864
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1882
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
|
|
1865
1883
|
ELSE NULL
|
|
1866
1884
|
END,
|
|
1867
1885
|
next_attempt_at=excluded.next_attempt_at,
|
|
1868
|
-
last_reason=
|
|
1886
|
+
last_reason=CASE
|
|
1887
|
+
WHEN workflow_work_items.attempts > 0
|
|
1888
|
+
AND workflow_work_items.status IN ('queued', 'deferred')
|
|
1889
|
+
AND workflow_work_items.last_reason IS NOT NULL
|
|
1890
|
+
AND excluded.last_reason IS NOT NULL
|
|
1891
|
+
THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
|
|
1892
|
+
ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
|
|
1893
|
+
END,
|
|
1869
1894
|
updated_at=excluded.updated_at`).run({
|
|
1870
1895
|
$id: id,
|
|
1871
1896
|
$routeKey: input.routeKey,
|
|
@@ -1918,11 +1943,39 @@ class Store {
|
|
|
1918
1943
|
const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
|
|
1919
1944
|
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1920
1945
|
}
|
|
1946
|
+
requeueWorkflowWorkItem(id, patch = {}) {
|
|
1947
|
+
const current = this.getWorkflowWorkItem(id);
|
|
1948
|
+
if (!current)
|
|
1949
|
+
throw new Error(`workflow work item not found: ${id}`);
|
|
1950
|
+
const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
|
|
1951
|
+
if (!requeueableStatuses.includes(current.status)) {
|
|
1952
|
+
throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
|
|
1953
|
+
}
|
|
1954
|
+
const now = nowIso();
|
|
1955
|
+
const reason = patch.reason?.trim() || `requeued from ${current.status}`;
|
|
1956
|
+
const placeholders = requeueableStatuses.map(() => "?").join(",");
|
|
1957
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1958
|
+
SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
|
|
1959
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
|
|
1960
|
+
WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
|
|
1961
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1962
|
+
if (!item)
|
|
1963
|
+
throw new Error(`workflow work item not found after requeue: ${id}`);
|
|
1964
|
+
if (res.changes !== 1)
|
|
1965
|
+
throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
|
|
1966
|
+
return item;
|
|
1967
|
+
}
|
|
1921
1968
|
admitWorkflowWorkItem(id, patch) {
|
|
1922
1969
|
const now = nowIso();
|
|
1923
1970
|
const res = this.db.query(`UPDATE workflow_work_items
|
|
1924
1971
|
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1925
|
-
next_attempt_at=NULL,
|
|
1972
|
+
next_attempt_at=NULL,
|
|
1973
|
+
lease_expires_at=NULL,
|
|
1974
|
+
last_reason=CASE
|
|
1975
|
+
WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
|
|
1976
|
+
ELSE COALESCE($reason, last_reason)
|
|
1977
|
+
END,
|
|
1978
|
+
updated_at=$updated
|
|
1926
1979
|
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1927
1980
|
$id: id,
|
|
1928
1981
|
$workflowId: patch.workflowId,
|
|
@@ -3464,6 +3517,9 @@ function assertSupportedAgentOptions(target) {
|
|
|
3464
3517
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3465
3518
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3466
3519
|
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3520
|
+
if (target.provider === "opencode" && (target.model === undefined || target.model.trim() === "")) {
|
|
3521
|
+
throw new Error("opencode.model is required; pass a provider/model id such as openrouter/google/gemini-2.5-flash");
|
|
3522
|
+
}
|
|
3467
3523
|
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3468
3524
|
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3469
3525
|
}
|
|
@@ -4236,6 +4292,78 @@ function sameBlockerKey(values) {
|
|
|
4236
4292
|
return values.map((value) => value.trim()).filter(Boolean).join(`
|
|
4237
4293
|
`) || "goal completion remains unproven";
|
|
4238
4294
|
}
|
|
4295
|
+
function planStatusForGoal(goal) {
|
|
4296
|
+
if (goal.status === "usageLimited")
|
|
4297
|
+
return "blocked";
|
|
4298
|
+
return goal.status;
|
|
4299
|
+
}
|
|
4300
|
+
function syncReadyFlags(store, goal, nodes, opts) {
|
|
4301
|
+
const withReady = updateReadyFlags(nodes, planStatusForGoal(goal));
|
|
4302
|
+
for (const node of withReady) {
|
|
4303
|
+
const current = nodes.find((entry) => entry.key === node.key);
|
|
4304
|
+
if (current && current.ready !== node.ready) {
|
|
4305
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { ready: node.ready }, { daemonLeaseId: opts.daemonLeaseId });
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
return withReady;
|
|
4309
|
+
}
|
|
4310
|
+
function summarizeLabels(values, limit = 5) {
|
|
4311
|
+
if (values.length <= limit)
|
|
4312
|
+
return values.join(", ");
|
|
4313
|
+
return `${values.slice(0, limit).join(", ")} and ${values.length - limit} more`;
|
|
4314
|
+
}
|
|
4315
|
+
function noReadyDiagnostic(goal, nodes) {
|
|
4316
|
+
const planStatus = planStatusForGoal(goal);
|
|
4317
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
4318
|
+
const pendingNodes = nodes.filter((node) => node.status === "pending");
|
|
4319
|
+
const incompleteNodes = nodes.filter((node) => node.status !== "complete");
|
|
4320
|
+
const pendingDetails = pendingNodes.map((node) => ({
|
|
4321
|
+
key: node.key,
|
|
4322
|
+
status: node.status,
|
|
4323
|
+
ready: node.ready,
|
|
4324
|
+
tokenBudget: node.tokenBudget,
|
|
4325
|
+
tokensUsed: node.tokensUsed,
|
|
4326
|
+
budgetExhausted: nodeBudgetExhausted(node),
|
|
4327
|
+
unmetDependencies: node.dependsOn.map((dependency) => ({ key: dependency, status: byKey.get(dependency)?.status ?? "missing" })).filter((dependency) => dependency.status !== "complete")
|
|
4328
|
+
}));
|
|
4329
|
+
const incompleteDetails = incompleteNodes.map((node) => ({
|
|
4330
|
+
key: node.key,
|
|
4331
|
+
status: node.status,
|
|
4332
|
+
ready: node.ready
|
|
4333
|
+
}));
|
|
4334
|
+
let owner = "goal-plan";
|
|
4335
|
+
let cause = "goal plan has no ready nodes";
|
|
4336
|
+
if (planStatus !== "active") {
|
|
4337
|
+
owner = "goal";
|
|
4338
|
+
cause = `goal status is ${goal.status}`;
|
|
4339
|
+
} else if (pendingNodes.length === 0) {
|
|
4340
|
+
owner = "goal-plan";
|
|
4341
|
+
cause = `goal plan has no pending runnable nodes; incomplete nodes: ${summarizeLabels(incompleteDetails.map((node) => `${node.key}:${node.status}`))}`;
|
|
4342
|
+
} else if (pendingDetails.every((node) => node.budgetExhausted)) {
|
|
4343
|
+
owner = "goal-plan-node";
|
|
4344
|
+
cause = `all pending nodes are budget-exhausted: ${summarizeLabels(pendingDetails.map((node) => node.key))}`;
|
|
4345
|
+
} else {
|
|
4346
|
+
const missingDependencies = pendingDetails.flatMap((node) => node.unmetDependencies.filter((dependency) => dependency.status === "missing").map((dependency) => `${node.key}->${dependency.key}`));
|
|
4347
|
+
if (missingDependencies.length > 0) {
|
|
4348
|
+
owner = "goal-plan";
|
|
4349
|
+
cause = `pending nodes reference missing dependencies: ${summarizeLabels(missingDependencies)}`;
|
|
4350
|
+
} else if (pendingDetails.every((node) => node.unmetDependencies.length > 0 || node.budgetExhausted)) {
|
|
4351
|
+
owner = "goal-plan-node";
|
|
4352
|
+
const waiting = pendingDetails.filter((node) => node.unmetDependencies.length > 0).map((node) => `${node.key} waits on ${node.unmetDependencies.map((dependency) => `${dependency.key}:${dependency.status}`).join(",")}`);
|
|
4353
|
+
const exhausted = pendingDetails.filter((node) => node.budgetExhausted).map((node) => `${node.key} budget exhausted`);
|
|
4354
|
+
cause = `pending nodes are blocked by prerequisites: ${summarizeLabels([...waiting, ...exhausted])}`;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
const blocker = `${cause}; owner=${owner}`;
|
|
4358
|
+
return {
|
|
4359
|
+
owner,
|
|
4360
|
+
blocker,
|
|
4361
|
+
planStatus,
|
|
4362
|
+
rollup: rollupSummary(nodes),
|
|
4363
|
+
pendingNodes: pendingDetails,
|
|
4364
|
+
incompleteNodes: incompleteDetails
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4239
4367
|
function metadataFor(goal, node, context) {
|
|
4240
4368
|
return {
|
|
4241
4369
|
loopId: context?.loopId,
|
|
@@ -4296,13 +4424,14 @@ async function planGoal(store, goal, spec, model, opts) {
|
|
|
4296
4424
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4297
4425
|
return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
|
|
4298
4426
|
}
|
|
4299
|
-
function stdoutFor(goal, nodes, evidence, validation) {
|
|
4427
|
+
function stdoutFor(goal, nodes, evidence, validation, diagnostics) {
|
|
4300
4428
|
return JSON.stringify({
|
|
4301
4429
|
goal,
|
|
4302
4430
|
rollup: rollupSummary(nodes),
|
|
4303
4431
|
nodes,
|
|
4304
4432
|
evidence,
|
|
4305
|
-
validation
|
|
4433
|
+
validation,
|
|
4434
|
+
diagnostics
|
|
4306
4435
|
}, null, 2);
|
|
4307
4436
|
}
|
|
4308
4437
|
async function runGoal(store, input, opts = {}) {
|
|
@@ -4335,6 +4464,7 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4335
4464
|
let validation;
|
|
4336
4465
|
let lastBlocker = "";
|
|
4337
4466
|
let repeatedBlockerCount = 0;
|
|
4467
|
+
let lastDiagnostic;
|
|
4338
4468
|
if (budgetExhausted(goal)) {
|
|
4339
4469
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4340
4470
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
|
|
@@ -4345,13 +4475,13 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4345
4475
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
|
|
4346
4476
|
}
|
|
4347
4477
|
goal = store.requireGoal(goal.goalId);
|
|
4348
|
-
nodes = store.listGoalPlanNodes(goal.goalId);
|
|
4478
|
+
nodes = syncReadyFlags(store, goal, store.listGoalPlanNodes(goal.goalId), opts);
|
|
4349
4479
|
if (budgetExhausted(goal)) {
|
|
4350
4480
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4351
4481
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
|
|
4352
4482
|
}
|
|
4353
4483
|
const readyKeys = readyNodeKeys({
|
|
4354
|
-
status: goal
|
|
4484
|
+
status: planStatusForGoal(goal),
|
|
4355
4485
|
nodes
|
|
4356
4486
|
});
|
|
4357
4487
|
if (readyKeys.length > 0) {
|
|
@@ -4450,7 +4580,9 @@ ${result.stderr}`);
|
|
|
4450
4580
|
}
|
|
4451
4581
|
continue;
|
|
4452
4582
|
}
|
|
4453
|
-
const
|
|
4583
|
+
const diagnostic = noReadyDiagnostic(goal, nodes);
|
|
4584
|
+
lastDiagnostic = diagnostic;
|
|
4585
|
+
const blocker = diagnostic.blocker;
|
|
4454
4586
|
if (blocker === lastBlocker)
|
|
4455
4587
|
repeatedBlockerCount += 1;
|
|
4456
4588
|
else {
|
|
@@ -4462,15 +4594,16 @@ ${result.stderr}`);
|
|
|
4462
4594
|
turn,
|
|
4463
4595
|
phase: "status",
|
|
4464
4596
|
status: repeatedBlockerCount >= 3 ? "blocked" : "active",
|
|
4465
|
-
evidence:
|
|
4597
|
+
evidence: diagnostic
|
|
4466
4598
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4467
4599
|
if (repeatedBlockerCount >= 3) {
|
|
4468
4600
|
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
4469
|
-
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
|
|
4601
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation, diagnostic), blocker, startedAt);
|
|
4470
4602
|
}
|
|
4471
4603
|
}
|
|
4472
4604
|
goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4473
|
-
|
|
4605
|
+
const exhaustedError = lastDiagnostic?.blocker ?? (lastBlocker ? `${lastBlocker}; max turns exhausted` : "goal max turns exhausted");
|
|
4606
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation, lastDiagnostic), exhaustedError, startedAt);
|
|
4474
4607
|
}
|
|
4475
4608
|
|
|
4476
4609
|
// src/lib/workflow-runner.ts
|
|
@@ -4499,6 +4632,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4499
4632
|
const workflowWithoutGoal = { ...workflow, goal: undefined };
|
|
4500
4633
|
return runGoal(store, workflow.goal, {
|
|
4501
4634
|
...opts,
|
|
4635
|
+
model: opts.goalModel,
|
|
4502
4636
|
context: {
|
|
4503
4637
|
loopId: opts.loop?.id,
|
|
4504
4638
|
loopName: opts.loop?.name,
|
|
@@ -4591,6 +4725,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
4591
4725
|
if (step.goal) {
|
|
4592
4726
|
result = await runGoal(store, step.goal, {
|
|
4593
4727
|
...opts,
|
|
4728
|
+
model: opts.goalModel,
|
|
4594
4729
|
target: targetWithStepAccount(step),
|
|
4595
4730
|
signal: controller.signal,
|
|
4596
4731
|
context: {
|
|
@@ -4732,6 +4867,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4732
4867
|
if (loop.goal) {
|
|
4733
4868
|
return runGoal(store, loop.goal, {
|
|
4734
4869
|
...opts,
|
|
4870
|
+
model: opts.goalModel,
|
|
4735
4871
|
target: loop.target,
|
|
4736
4872
|
context: {
|
|
4737
4873
|
loopId: loop.id,
|
|
@@ -4753,8 +4889,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4753
4889
|
}
|
|
4754
4890
|
}
|
|
4755
4891
|
if (loop.goal) {
|
|
4892
|
+
const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
|
|
4756
4893
|
return runGoal(store, loop.goal, {
|
|
4757
4894
|
...opts,
|
|
4895
|
+
model: opts.goalModel,
|
|
4758
4896
|
context: {
|
|
4759
4897
|
loopId: loop.id,
|
|
4760
4898
|
loopName: loop.name,
|
|
@@ -4763,7 +4901,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4763
4901
|
workflowId: workflow.id,
|
|
4764
4902
|
workflowName: workflow.name
|
|
4765
4903
|
},
|
|
4766
|
-
executeNode: async (node) => executeWorkflow(store,
|
|
4904
|
+
executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
|
|
4767
4905
|
...opts,
|
|
4768
4906
|
loop,
|
|
4769
4907
|
loopRun: run,
|
|
@@ -5493,14 +5631,15 @@ function enableStartup(result) {
|
|
|
5493
5631
|
// package.json
|
|
5494
5632
|
var package_default = {
|
|
5495
5633
|
name: "@hasna/loops",
|
|
5496
|
-
version: "0.3.
|
|
5634
|
+
version: "0.3.58",
|
|
5497
5635
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5498
5636
|
type: "module",
|
|
5499
5637
|
main: "dist/index.js",
|
|
5500
5638
|
types: "dist/index.d.ts",
|
|
5501
5639
|
bin: {
|
|
5502
5640
|
loops: "dist/cli/index.js",
|
|
5503
|
-
"loops-daemon": "dist/daemon/index.js"
|
|
5641
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
5642
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
5504
5643
|
},
|
|
5505
5644
|
exports: {
|
|
5506
5645
|
".": {
|
|
@@ -5511,6 +5650,10 @@ var package_default = {
|
|
|
5511
5650
|
types: "./dist/sdk/index.d.ts",
|
|
5512
5651
|
import: "./dist/sdk/index.js"
|
|
5513
5652
|
},
|
|
5653
|
+
"./mcp": {
|
|
5654
|
+
types: "./dist/mcp/index.d.ts",
|
|
5655
|
+
import: "./dist/mcp/index.js"
|
|
5656
|
+
},
|
|
5514
5657
|
"./storage": {
|
|
5515
5658
|
types: "./dist/lib/store.d.ts",
|
|
5516
5659
|
import: "./dist/lib/store.js"
|
|
@@ -5523,7 +5666,7 @@ var package_default = {
|
|
|
5523
5666
|
"LICENSE"
|
|
5524
5667
|
],
|
|
5525
5668
|
scripts: {
|
|
5526
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
5669
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
5527
5670
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
5528
5671
|
typecheck: "tsc --noEmit",
|
|
5529
5672
|
test: "bun test",
|
|
@@ -5559,6 +5702,7 @@ var package_default = {
|
|
|
5559
5702
|
dependencies: {
|
|
5560
5703
|
"@hasna/events": "^0.1.9",
|
|
5561
5704
|
"@hasna/machines": "0.0.49",
|
|
5705
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
5562
5706
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
5563
5707
|
ai: "6.0.204",
|
|
5564
5708
|
commander: "^13.1.0",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export { LoopsClient, loops, openAutomationsRuntimeBinding } from "./sdk/index.js";
|
|
3
3
|
export type { LoopsClientOptions } from "./sdk/index.js";
|
|
4
|
+
export { createLoopsMcpServer, listToolsForCli, LOOPS_MCP_TOOLS } from "./mcp/index.js";
|
|
5
|
+
export type { LoopsMcpToolMetadata } from "./mcp/index.js";
|
|
4
6
|
export { Store } from "./lib/store.js";
|
|
5
7
|
export { parseDuration, parseCron, nextCronRun, initialNextRun, computeNextAfter } from "./lib/schedule.js";
|
|
6
8
|
export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";
|