@hasna/loops 0.4.5 → 0.4.7
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/CHANGELOG.md +40 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +613 -230
- package/dist/daemon/index.js +367 -170
- package/dist/index.js +413 -182
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/types.d.ts +6 -0
- package/dist/lib/storage/index.js +57 -5
- package/dist/lib/storage/sqlite.js +57 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +57 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/lib/templates.d.ts +10 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +371 -173
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +180 -19
- package/dist/sdk/index.js +370 -174
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/lib/executor.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { LanguageModel } from "ai";
|
|
2
|
-
import type { AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
|
|
2
|
+
import type { AgentProvider, AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
|
|
3
3
|
export interface SpawnedProcessInfo {
|
|
4
4
|
pid: number;
|
|
5
5
|
pgid: number;
|
|
6
6
|
processStartedAt: string;
|
|
7
7
|
}
|
|
8
|
+
export interface AgentProgressInfo {
|
|
9
|
+
provider: AgentProvider;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
statusReason?: string;
|
|
14
|
+
threadId?: string;
|
|
15
|
+
rolloutPath?: string;
|
|
16
|
+
pid?: number;
|
|
17
|
+
lastEventSeq?: number;
|
|
18
|
+
}
|
|
8
19
|
export interface ExecuteOptions extends PersistGuardOptions {
|
|
9
20
|
maxOutputBytes?: number;
|
|
10
21
|
env?: NodeJS.ProcessEnv;
|
|
@@ -14,6 +25,8 @@ export interface ExecuteOptions extends PersistGuardOptions {
|
|
|
14
25
|
onSpawn?: (pid: number) => void;
|
|
15
26
|
/** Children are spawned detached in their own process group, so pgid === pid. */
|
|
16
27
|
onSpawnProcess?: (info: SpawnedProcessInfo) => void;
|
|
28
|
+
/** Progress from durable provider controllers, for example Codewith background agents. */
|
|
29
|
+
onAgentProgress?: (info: AgentProgressInfo) => void;
|
|
17
30
|
machine?: LoopMachineRef;
|
|
18
31
|
machineResolver?: (machine: LoopMachineRef) => LoopMachineRef;
|
|
19
32
|
machineCommandResolver?: (machineId: string, command: string) => MachineCommandPlan;
|
package/dist/lib/mode.js
CHANGED
|
@@ -42,11 +42,15 @@ export interface TodosTaskRouteOptions {
|
|
|
42
42
|
preflight?: boolean;
|
|
43
43
|
dryRun?: boolean;
|
|
44
44
|
todosProject?: string;
|
|
45
|
+
/** Internal drain context; never derived from CLI/event fields. */
|
|
46
|
+
sourceTodosProjectPath?: string;
|
|
45
47
|
}
|
|
46
48
|
export interface TodosReadyTask {
|
|
47
49
|
id?: string;
|
|
48
50
|
task_id?: string;
|
|
49
51
|
taskId?: string;
|
|
52
|
+
source_project_path?: string;
|
|
53
|
+
sourceProjectPath?: string;
|
|
50
54
|
title?: string;
|
|
51
55
|
description?: string;
|
|
52
56
|
body?: string;
|
|
@@ -70,6 +74,8 @@ export interface TodosReadyTask {
|
|
|
70
74
|
}
|
|
71
75
|
export interface TodosDrainOptions extends TodosTaskRouteOptions {
|
|
72
76
|
todosProject?: string;
|
|
77
|
+
todosProjectsFromRegistry?: boolean;
|
|
78
|
+
todosProjectInclude?: string[];
|
|
73
79
|
todosProjectId?: string;
|
|
74
80
|
taskList?: string;
|
|
75
81
|
tags?: string;
|
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1491
1491
|
class Store {
|
|
1492
1492
|
db;
|
|
1493
1493
|
rootDir;
|
|
1494
|
+
memoryRootDir;
|
|
1494
1495
|
constructor(path) {
|
|
1495
1496
|
const file = path ?? dbPath();
|
|
1496
1497
|
if (file !== ":memory:")
|
|
1497
1498
|
ensurePrivateStorePath(file);
|
|
1498
1499
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1500
|
+
if (file === ":memory:")
|
|
1501
|
+
this.memoryRootDir = this.rootDir;
|
|
1499
1502
|
this.db = new Database(file);
|
|
1500
1503
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1501
1504
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1948,9 +1951,12 @@ class Store {
|
|
|
1948
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1949
1952
|
if (rows.length === 0)
|
|
1950
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1951
|
-
if (rows.length
|
|
1954
|
+
if (rows.length === 1)
|
|
1955
|
+
return rowToLoop(rows[0]);
|
|
1956
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1957
|
+
if (active.length !== 1)
|
|
1952
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1953
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1954
1960
|
}
|
|
1955
1961
|
requireLoop(idOrName) {
|
|
1956
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2291,6 +2297,7 @@ class Store {
|
|
|
2291
2297
|
return this.transact(() => {
|
|
2292
2298
|
const loop = this.requireLoop(idOrName);
|
|
2293
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2294
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2295
2302
|
return res.changes > 0;
|
|
2296
2303
|
});
|
|
@@ -2775,7 +2782,10 @@ class Store {
|
|
|
2775
2782
|
return rowToGoal(row);
|
|
2776
2783
|
}
|
|
2777
2784
|
if (context.sourceType && context.sourceId) {
|
|
2778
|
-
const
|
|
2785
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2786
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2787
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2788
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2779
2789
|
if (row)
|
|
2780
2790
|
return rowToGoal(row);
|
|
2781
2791
|
}
|
|
@@ -3195,6 +3205,41 @@ class Store {
|
|
|
3195
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3196
3206
|
return run;
|
|
3197
3207
|
}
|
|
3208
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3209
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3210
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3211
|
+
try {
|
|
3212
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3213
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3214
|
+
stderr=COALESCE($stderr, stderr),
|
|
3215
|
+
updated_at=$updated
|
|
3216
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3217
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3218
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3219
|
+
))`).run({
|
|
3220
|
+
$workflowRunId: workflowRunId,
|
|
3221
|
+
$stepId: stepId,
|
|
3222
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3223
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3224
|
+
$updated: now,
|
|
3225
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
|
+
$now: now
|
|
3227
|
+
});
|
|
3228
|
+
if (res.changes === 1) {
|
|
3229
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3230
|
+
}
|
|
3231
|
+
this.db.exec("COMMIT");
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.db.exec("ROLLBACK");
|
|
3235
|
+
} catch {}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3239
|
+
if (!run)
|
|
3240
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3241
|
+
return run;
|
|
3242
|
+
}
|
|
3198
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3199
3244
|
return this.transact(() => {
|
|
3200
3245
|
const now = nowIso();
|
|
@@ -3632,7 +3677,8 @@ class Store {
|
|
|
3632
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3633
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3634
3679
|
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3635
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3680
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3681
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3636
3682
|
const run = this.getRun(id);
|
|
3637
3683
|
if (!run)
|
|
3638
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4160,6 +4206,12 @@ class Store {
|
|
|
4160
4206
|
}
|
|
4161
4207
|
close() {
|
|
4162
4208
|
this.db.close();
|
|
4209
|
+
if (this.memoryRootDir) {
|
|
4210
|
+
try {
|
|
4211
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4212
|
+
} catch {}
|
|
4213
|
+
this.memoryRootDir = undefined;
|
|
4214
|
+
}
|
|
4163
4215
|
}
|
|
4164
4216
|
}
|
|
4165
4217
|
|
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1491
1491
|
class Store {
|
|
1492
1492
|
db;
|
|
1493
1493
|
rootDir;
|
|
1494
|
+
memoryRootDir;
|
|
1494
1495
|
constructor(path) {
|
|
1495
1496
|
const file = path ?? dbPath();
|
|
1496
1497
|
if (file !== ":memory:")
|
|
1497
1498
|
ensurePrivateStorePath(file);
|
|
1498
1499
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1500
|
+
if (file === ":memory:")
|
|
1501
|
+
this.memoryRootDir = this.rootDir;
|
|
1499
1502
|
this.db = new Database(file);
|
|
1500
1503
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1501
1504
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1948,9 +1951,12 @@ class Store {
|
|
|
1948
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1949
1952
|
if (rows.length === 0)
|
|
1950
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1951
|
-
if (rows.length
|
|
1954
|
+
if (rows.length === 1)
|
|
1955
|
+
return rowToLoop(rows[0]);
|
|
1956
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1957
|
+
if (active.length !== 1)
|
|
1952
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1953
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1954
1960
|
}
|
|
1955
1961
|
requireLoop(idOrName) {
|
|
1956
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2291,6 +2297,7 @@ class Store {
|
|
|
2291
2297
|
return this.transact(() => {
|
|
2292
2298
|
const loop = this.requireLoop(idOrName);
|
|
2293
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2294
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2295
2302
|
return res.changes > 0;
|
|
2296
2303
|
});
|
|
@@ -2775,7 +2782,10 @@ class Store {
|
|
|
2775
2782
|
return rowToGoal(row);
|
|
2776
2783
|
}
|
|
2777
2784
|
if (context.sourceType && context.sourceId) {
|
|
2778
|
-
const
|
|
2785
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2786
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2787
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2788
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2779
2789
|
if (row)
|
|
2780
2790
|
return rowToGoal(row);
|
|
2781
2791
|
}
|
|
@@ -3195,6 +3205,41 @@ class Store {
|
|
|
3195
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3196
3206
|
return run;
|
|
3197
3207
|
}
|
|
3208
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3209
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3210
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3211
|
+
try {
|
|
3212
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3213
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3214
|
+
stderr=COALESCE($stderr, stderr),
|
|
3215
|
+
updated_at=$updated
|
|
3216
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3217
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3218
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3219
|
+
))`).run({
|
|
3220
|
+
$workflowRunId: workflowRunId,
|
|
3221
|
+
$stepId: stepId,
|
|
3222
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3223
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3224
|
+
$updated: now,
|
|
3225
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
|
+
$now: now
|
|
3227
|
+
});
|
|
3228
|
+
if (res.changes === 1) {
|
|
3229
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3230
|
+
}
|
|
3231
|
+
this.db.exec("COMMIT");
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.db.exec("ROLLBACK");
|
|
3235
|
+
} catch {}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3239
|
+
if (!run)
|
|
3240
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3241
|
+
return run;
|
|
3242
|
+
}
|
|
3198
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3199
3244
|
return this.transact(() => {
|
|
3200
3245
|
const now = nowIso();
|
|
@@ -3632,7 +3677,8 @@ class Store {
|
|
|
3632
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3633
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3634
3679
|
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3635
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3680
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3681
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3636
3682
|
const run = this.getRun(id);
|
|
3637
3683
|
if (!run)
|
|
3638
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4160,6 +4206,12 @@ class Store {
|
|
|
4160
4206
|
}
|
|
4161
4207
|
close() {
|
|
4162
4208
|
this.db.close();
|
|
4209
|
+
if (this.memoryRootDir) {
|
|
4210
|
+
try {
|
|
4211
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4212
|
+
} catch {}
|
|
4213
|
+
this.memoryRootDir = undefined;
|
|
4214
|
+
}
|
|
4163
4215
|
}
|
|
4164
4216
|
}
|
|
4165
4217
|
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -125,6 +125,8 @@ export interface RecordGoalEventInput {
|
|
|
125
125
|
export declare class Store {
|
|
126
126
|
private db;
|
|
127
127
|
private rootDir;
|
|
128
|
+
/** Temp dir created for a `:memory:` store, removed in close() so tests/short-lived instances don't leak it. */
|
|
129
|
+
private memoryRootDir?;
|
|
128
130
|
constructor(path?: string);
|
|
129
131
|
private migrate;
|
|
130
132
|
private migrations;
|
|
@@ -279,6 +281,11 @@ export declare class Store {
|
|
|
279
281
|
isWorkflowRunTerminal(workflowRunId: string): boolean;
|
|
280
282
|
startWorkflowStepRun(workflowRunId: string, stepId: string, opts?: DaemonLeaseFence): WorkflowStepRun;
|
|
281
283
|
markWorkflowStepPid(workflowRunId: string, stepId: string, pid: number, opts?: DaemonLeaseFence): WorkflowStepRun;
|
|
284
|
+
recordWorkflowStepProgress(workflowRunId: string, stepId: string, progress: {
|
|
285
|
+
stdout?: string;
|
|
286
|
+
stderr?: string;
|
|
287
|
+
payload?: Record<string, unknown>;
|
|
288
|
+
}, opts?: DaemonLeaseFence): WorkflowStepRun;
|
|
282
289
|
recoverWorkflowRun(workflowRunId: string, reason?: string): {
|
|
283
290
|
run: WorkflowRun;
|
|
284
291
|
recoveredSteps: WorkflowStepRun[];
|
package/dist/lib/store.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1491
1491
|
class Store {
|
|
1492
1492
|
db;
|
|
1493
1493
|
rootDir;
|
|
1494
|
+
memoryRootDir;
|
|
1494
1495
|
constructor(path) {
|
|
1495
1496
|
const file = path ?? dbPath();
|
|
1496
1497
|
if (file !== ":memory:")
|
|
1497
1498
|
ensurePrivateStorePath(file);
|
|
1498
1499
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1500
|
+
if (file === ":memory:")
|
|
1501
|
+
this.memoryRootDir = this.rootDir;
|
|
1499
1502
|
this.db = new Database(file);
|
|
1500
1503
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1501
1504
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1948,9 +1951,12 @@ class Store {
|
|
|
1948
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1949
1952
|
if (rows.length === 0)
|
|
1950
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1951
|
-
if (rows.length
|
|
1954
|
+
if (rows.length === 1)
|
|
1955
|
+
return rowToLoop(rows[0]);
|
|
1956
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1957
|
+
if (active.length !== 1)
|
|
1952
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1953
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1954
1960
|
}
|
|
1955
1961
|
requireLoop(idOrName) {
|
|
1956
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2291,6 +2297,7 @@ class Store {
|
|
|
2291
2297
|
return this.transact(() => {
|
|
2292
2298
|
const loop = this.requireLoop(idOrName);
|
|
2293
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2294
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2295
2302
|
return res.changes > 0;
|
|
2296
2303
|
});
|
|
@@ -2775,7 +2782,10 @@ class Store {
|
|
|
2775
2782
|
return rowToGoal(row);
|
|
2776
2783
|
}
|
|
2777
2784
|
if (context.sourceType && context.sourceId) {
|
|
2778
|
-
const
|
|
2785
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2786
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2787
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2788
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2779
2789
|
if (row)
|
|
2780
2790
|
return rowToGoal(row);
|
|
2781
2791
|
}
|
|
@@ -3195,6 +3205,41 @@ class Store {
|
|
|
3195
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3196
3206
|
return run;
|
|
3197
3207
|
}
|
|
3208
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3209
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3210
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3211
|
+
try {
|
|
3212
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3213
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3214
|
+
stderr=COALESCE($stderr, stderr),
|
|
3215
|
+
updated_at=$updated
|
|
3216
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3217
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3218
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3219
|
+
))`).run({
|
|
3220
|
+
$workflowRunId: workflowRunId,
|
|
3221
|
+
$stepId: stepId,
|
|
3222
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3223
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3224
|
+
$updated: now,
|
|
3225
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
|
+
$now: now
|
|
3227
|
+
});
|
|
3228
|
+
if (res.changes === 1) {
|
|
3229
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3230
|
+
}
|
|
3231
|
+
this.db.exec("COMMIT");
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.db.exec("ROLLBACK");
|
|
3235
|
+
} catch {}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3239
|
+
if (!run)
|
|
3240
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3241
|
+
return run;
|
|
3242
|
+
}
|
|
3198
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3199
3244
|
return this.transact(() => {
|
|
3200
3245
|
const now = nowIso();
|
|
@@ -3632,7 +3677,8 @@ class Store {
|
|
|
3632
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3633
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3634
3679
|
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3635
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3680
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3681
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3636
3682
|
const run = this.getRun(id);
|
|
3637
3683
|
if (!run)
|
|
3638
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4160,6 +4206,12 @@ class Store {
|
|
|
4160
4206
|
}
|
|
4161
4207
|
close() {
|
|
4162
4208
|
this.db.close();
|
|
4209
|
+
if (this.memoryRootDir) {
|
|
4210
|
+
try {
|
|
4211
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4212
|
+
} catch {}
|
|
4213
|
+
this.memoryRootDir = undefined;
|
|
4214
|
+
}
|
|
4163
4215
|
}
|
|
4164
4216
|
}
|
|
4165
4217
|
export {
|
|
@@ -37,6 +37,12 @@ export declare function roleFragment(role: string, flow: BuiltinFlow): string;
|
|
|
37
37
|
* that rendering is contract-tested, so composition must preserve both shapes.
|
|
38
38
|
*/
|
|
39
39
|
export declare function goalHeaderFragment(goal: string, role: string, flow: BuiltinFlow): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Bounded lifecycle steps must not open native Codewith `/goal` state. The
|
|
42
|
+
* workflow itself already owns durability and sequencing; each agent step only
|
|
43
|
+
* needs a finite role/objective header plus exact task evidence instructions.
|
|
44
|
+
*/
|
|
45
|
+
export declare function boundedStepHeaderFragment(objective: string, role: string, flow: BuiltinFlow): string[];
|
|
40
46
|
export declare function adversarialReviewFragment(inspect: string, focus: string): string;
|
|
41
47
|
export declare const DEFAULT_VERIFIER_IDLE_TIMEOUT_MS: number;
|
|
42
48
|
export declare function verifierIdleTimeoutMs(input: {
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -39,9 +39,19 @@ export interface TodosTaskWorkflowTemplateInput extends AgentWorkflowTemplateBas
|
|
|
39
39
|
taskDescription?: string;
|
|
40
40
|
todosProjectPath?: string;
|
|
41
41
|
prHandoff?: boolean;
|
|
42
|
+
prReviewRouting?: PrReviewRoutingTemplateContext;
|
|
42
43
|
eventId?: string;
|
|
43
44
|
eventType?: string;
|
|
44
45
|
}
|
|
46
|
+
export interface PrReviewRoutingTemplateContext {
|
|
47
|
+
required?: boolean;
|
|
48
|
+
allowed?: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
author?: string;
|
|
51
|
+
reviewers?: string[];
|
|
52
|
+
selectedReviewer?: string;
|
|
53
|
+
signals?: string[];
|
|
54
|
+
}
|
|
45
55
|
export interface EventWorkflowTemplateInput extends AgentWorkflowTemplateBaseInput {
|
|
46
56
|
eventId: string;
|
|
47
57
|
eventType: string;
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface LoopsMcpToolMetadata {
|
|
|
9
9
|
guarded?: boolean;
|
|
10
10
|
requiresEnv?: string;
|
|
11
11
|
}
|
|
12
|
-
/** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and
|
|
12
|
+
/** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and the loops-mcp bin's `list-tools` argv. */
|
|
13
13
|
export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
|
|
14
14
|
export declare function createLoopsMcpServer(): McpServer;
|
|
15
15
|
export declare function listToolsForCli(): LoopsMcpToolMetadata[];
|