@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/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/health.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Loop, LoopRun } from "../types.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
-
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
3
|
+
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
4
4
|
export interface RunFailureSignal {
|
|
5
5
|
classification: RunFailureClassification;
|
|
6
6
|
fingerprint: string;
|
|
@@ -34,7 +34,7 @@ export interface LoopExpectationResult {
|
|
|
34
34
|
loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt">;
|
|
35
35
|
ok: boolean;
|
|
36
36
|
check: {
|
|
37
|
-
id: "latest-run-succeeded";
|
|
37
|
+
id: "latest-run-succeeded" | "route-functional-health";
|
|
38
38
|
status: "pass" | "fail" | "warn";
|
|
39
39
|
message: string;
|
|
40
40
|
};
|
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;
|
|
@@ -140,6 +141,9 @@ export declare class Store {
|
|
|
140
141
|
project: number;
|
|
141
142
|
projectGroup?: number;
|
|
142
143
|
};
|
|
144
|
+
requeueWorkflowWorkItem(id: string, patch?: {
|
|
145
|
+
reason?: string;
|
|
146
|
+
}): WorkflowWorkItem;
|
|
143
147
|
admitWorkflowWorkItem(id: string, patch: {
|
|
144
148
|
workflowId: string;
|
|
145
149
|
loopId: string;
|
package/dist/lib/store.js
CHANGED
|
@@ -459,6 +459,9 @@ function validateTarget(value, label, opts) {
|
|
|
459
459
|
}
|
|
460
460
|
if (value.model !== undefined)
|
|
461
461
|
assertString(value.model, `${label}.model`);
|
|
462
|
+
if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
|
|
463
|
+
throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
|
|
464
|
+
}
|
|
462
465
|
if (value.variant !== undefined)
|
|
463
466
|
assertString(value.variant, `${label}.variant`);
|
|
464
467
|
if (value.agent !== undefined)
|
|
@@ -1255,12 +1258,21 @@ class Store {
|
|
|
1255
1258
|
if (!row)
|
|
1256
1259
|
throw new Error("daemon lease lost");
|
|
1257
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
|
+
}
|
|
1258
1269
|
createLoop(input, from = new Date) {
|
|
1259
1270
|
const now = nowIso();
|
|
1260
1271
|
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1261
1272
|
name: "loop-target-validation",
|
|
1262
1273
|
steps: [{ id: "target", target: input.target }]
|
|
1263
1274
|
}).steps[0].target;
|
|
1275
|
+
this.assertNoNestedWorkflowGoal(target, input.goal);
|
|
1264
1276
|
const loop = {
|
|
1265
1277
|
id: genId(),
|
|
1266
1278
|
name: input.name,
|
|
@@ -1432,6 +1444,9 @@ class Store {
|
|
|
1432
1444
|
if (this.hasRunningRun(current.id))
|
|
1433
1445
|
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1434
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
|
+
}
|
|
1435
1450
|
const target = { ...current.target, workflowId: workflow.id };
|
|
1436
1451
|
if (opts.workflowTimeoutMs !== undefined)
|
|
1437
1452
|
target.timeoutMs = opts.workflowTimeoutMs;
|
|
@@ -1470,6 +1485,9 @@ class Store {
|
|
|
1470
1485
|
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1471
1486
|
if (this.hasRunningRun(current.id))
|
|
1472
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
|
+
}
|
|
1473
1491
|
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1474
1492
|
const workflow = {
|
|
1475
1493
|
id: genId(),
|
|
@@ -1761,7 +1779,7 @@ class Store {
|
|
|
1761
1779
|
if (!sourceDedupeKey)
|
|
1762
1780
|
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1763
1781
|
const now = nowIso();
|
|
1764
|
-
const claimableStatuses = ["queued", "deferred"
|
|
1782
|
+
const claimableStatuses = ["queued", "deferred"];
|
|
1765
1783
|
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1766
1784
|
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1767
1785
|
const result = this.db.query(`UPDATE workflow_invocations
|
|
@@ -1842,28 +1860,35 @@ class Store {
|
|
|
1842
1860
|
project_group=excluded.project_group,
|
|
1843
1861
|
priority=excluded.priority,
|
|
1844
1862
|
status=CASE
|
|
1845
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1863
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
|
|
1846
1864
|
THEN workflow_work_items.status
|
|
1847
1865
|
ELSE excluded.status
|
|
1848
1866
|
END,
|
|
1849
1867
|
workflow_id=CASE
|
|
1850
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1868
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
|
|
1851
1869
|
ELSE NULL
|
|
1852
1870
|
END,
|
|
1853
1871
|
loop_id=CASE
|
|
1854
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1872
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
|
|
1855
1873
|
ELSE NULL
|
|
1856
1874
|
END,
|
|
1857
1875
|
workflow_run_id=CASE
|
|
1858
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1876
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
|
|
1859
1877
|
ELSE NULL
|
|
1860
1878
|
END,
|
|
1861
1879
|
lease_expires_at=CASE
|
|
1862
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1880
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
|
|
1863
1881
|
ELSE NULL
|
|
1864
1882
|
END,
|
|
1865
1883
|
next_attempt_at=excluded.next_attempt_at,
|
|
1866
|
-
last_reason=
|
|
1884
|
+
last_reason=CASE
|
|
1885
|
+
WHEN workflow_work_items.attempts > 0
|
|
1886
|
+
AND workflow_work_items.status IN ('queued', 'deferred')
|
|
1887
|
+
AND workflow_work_items.last_reason IS NOT NULL
|
|
1888
|
+
AND excluded.last_reason IS NOT NULL
|
|
1889
|
+
THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
|
|
1890
|
+
ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
|
|
1891
|
+
END,
|
|
1867
1892
|
updated_at=excluded.updated_at`).run({
|
|
1868
1893
|
$id: id,
|
|
1869
1894
|
$routeKey: input.routeKey,
|
|
@@ -1916,11 +1941,39 @@ class Store {
|
|
|
1916
1941
|
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;
|
|
1917
1942
|
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1918
1943
|
}
|
|
1944
|
+
requeueWorkflowWorkItem(id, patch = {}) {
|
|
1945
|
+
const current = this.getWorkflowWorkItem(id);
|
|
1946
|
+
if (!current)
|
|
1947
|
+
throw new Error(`workflow work item not found: ${id}`);
|
|
1948
|
+
const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
|
|
1949
|
+
if (!requeueableStatuses.includes(current.status)) {
|
|
1950
|
+
throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
|
|
1951
|
+
}
|
|
1952
|
+
const now = nowIso();
|
|
1953
|
+
const reason = patch.reason?.trim() || `requeued from ${current.status}`;
|
|
1954
|
+
const placeholders = requeueableStatuses.map(() => "?").join(",");
|
|
1955
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1956
|
+
SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
|
|
1957
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
|
|
1958
|
+
WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
|
|
1959
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1960
|
+
if (!item)
|
|
1961
|
+
throw new Error(`workflow work item not found after requeue: ${id}`);
|
|
1962
|
+
if (res.changes !== 1)
|
|
1963
|
+
throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
|
|
1964
|
+
return item;
|
|
1965
|
+
}
|
|
1919
1966
|
admitWorkflowWorkItem(id, patch) {
|
|
1920
1967
|
const now = nowIso();
|
|
1921
1968
|
const res = this.db.query(`UPDATE workflow_work_items
|
|
1922
1969
|
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1923
|
-
next_attempt_at=NULL,
|
|
1970
|
+
next_attempt_at=NULL,
|
|
1971
|
+
lease_expires_at=NULL,
|
|
1972
|
+
last_reason=CASE
|
|
1973
|
+
WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
|
|
1974
|
+
ELSE COALESCE($reason, last_reason)
|
|
1975
|
+
END,
|
|
1976
|
+
updated_at=$updated
|
|
1924
1977
|
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1925
1978
|
$id: id,
|
|
1926
1979
|
$workflowId: patch.workflowId,
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface TodosTaskWorkflowTemplateInput {
|
|
|
41
41
|
worktreeRoot?: string;
|
|
42
42
|
worktreeBranchPrefix?: string;
|
|
43
43
|
timeoutMs?: TimeoutMs;
|
|
44
|
+
verifierIdleTimeoutMs?: number;
|
|
45
|
+
prHandoff?: boolean;
|
|
44
46
|
eventId?: string;
|
|
45
47
|
eventType?: string;
|
|
46
48
|
}
|
|
@@ -78,6 +80,7 @@ export interface EventWorkflowTemplateInput {
|
|
|
78
80
|
worktreeRoot?: string;
|
|
79
81
|
worktreeBranchPrefix?: string;
|
|
80
82
|
timeoutMs?: TimeoutMs;
|
|
83
|
+
verifierIdleTimeoutMs?: number;
|
|
81
84
|
}
|
|
82
85
|
export interface BoundedAgentWorkflowTemplateInput {
|
|
83
86
|
name?: string;
|
|
@@ -110,6 +113,7 @@ export interface BoundedAgentWorkflowTemplateInput {
|
|
|
110
113
|
worktreeRoot?: string;
|
|
111
114
|
worktreeBranchPrefix?: string;
|
|
112
115
|
timeoutMs?: TimeoutMs;
|
|
116
|
+
verifierIdleTimeoutMs?: number;
|
|
113
117
|
}
|
|
114
118
|
export type LoopTemplateSourceFilter = LoopTemplateSource | "all";
|
|
115
119
|
export interface ListLoopTemplatesOptions {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export interface LoopsMcpToolMetadata {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
readOnly: boolean;
|
|
7
|
+
guarded?: boolean;
|
|
8
|
+
requiresEnv?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
|
|
11
|
+
export declare function createLoopsMcpServer(): McpServer;
|
|
12
|
+
export declare function listToolsForCli(): LoopsMcpToolMetadata[];
|