@hasna/loops 0.3.58 → 0.3.60
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 +4 -4
- package/dist/cli/index.js +525 -82
- package/dist/daemon/index.js +394 -16
- package/dist/index.js +387 -9
- package/dist/lib/store.d.ts +10 -0
- package/dist/lib/store.js +90 -1
- package/dist/mcp/index.js +387 -9
- package/dist/sdk/index.js +386 -8
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -356,6 +356,20 @@ function optionalStringArray(value, label) {
|
|
|
356
356
|
}).filter(Boolean);
|
|
357
357
|
return values.length ? values : undefined;
|
|
358
358
|
}
|
|
359
|
+
var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
|
|
360
|
+
"e",
|
|
361
|
+
"exec",
|
|
362
|
+
"agent",
|
|
363
|
+
"start",
|
|
364
|
+
"--ephemeral",
|
|
365
|
+
"--ignore-rules",
|
|
366
|
+
"--skip-git-repo-check",
|
|
367
|
+
"--json",
|
|
368
|
+
"--output-last-message",
|
|
369
|
+
"-o",
|
|
370
|
+
"--output-schema",
|
|
371
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
372
|
+
]);
|
|
359
373
|
function optionalAccountRef(value, label) {
|
|
360
374
|
if (value === undefined)
|
|
361
375
|
return;
|
|
@@ -470,6 +484,15 @@ function validateTarget(value, label, opts) {
|
|
|
470
484
|
throw new Error(`${label}.variant is not supported for provider cursor`);
|
|
471
485
|
if (value.provider === "codex" && value.agent !== undefined)
|
|
472
486
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
487
|
+
if (value.provider === "codewith" && value.agent !== undefined) {
|
|
488
|
+
throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
|
|
489
|
+
}
|
|
490
|
+
const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
|
|
491
|
+
if (value.provider === "codewith") {
|
|
492
|
+
const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
|
|
493
|
+
if (unsafe)
|
|
494
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
|
|
495
|
+
}
|
|
473
496
|
optionalStringArray(value.addDirs, `${label}.addDirs`);
|
|
474
497
|
if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
|
|
475
498
|
throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
|
|
@@ -545,7 +568,9 @@ function validateTarget(value, label, opts) {
|
|
|
545
568
|
if (value.routing.eventSource !== undefined)
|
|
546
569
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
547
570
|
}
|
|
548
|
-
const target = { ...value };
|
|
571
|
+
const target = { ...value, extraArgs };
|
|
572
|
+
if (!extraArgs)
|
|
573
|
+
delete target.extraArgs;
|
|
549
574
|
delete target.promptFile;
|
|
550
575
|
delete target.promptSource;
|
|
551
576
|
return { ...target, ...promptFields };
|
|
@@ -1541,6 +1566,70 @@ class Store {
|
|
|
1541
1566
|
throw error;
|
|
1542
1567
|
}
|
|
1543
1568
|
}
|
|
1569
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1570
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1571
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1572
|
+
try {
|
|
1573
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1574
|
+
if (current.target.type !== "workflow")
|
|
1575
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1576
|
+
if (this.hasRunningRun(current.id))
|
|
1577
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1578
|
+
if (!current.goal)
|
|
1579
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1580
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1581
|
+
if (!previousWorkflow.goal)
|
|
1582
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1583
|
+
const workflow = {
|
|
1584
|
+
id: genId(),
|
|
1585
|
+
name: opts.workflowName,
|
|
1586
|
+
description: previousWorkflow.description,
|
|
1587
|
+
version: previousWorkflow.version,
|
|
1588
|
+
status: "active",
|
|
1589
|
+
steps: previousWorkflow.steps,
|
|
1590
|
+
createdAt: updated,
|
|
1591
|
+
updatedAt: updated
|
|
1592
|
+
};
|
|
1593
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1594
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1595
|
+
$id: workflow.id,
|
|
1596
|
+
$name: workflow.name,
|
|
1597
|
+
$description: workflow.description ?? null,
|
|
1598
|
+
$version: workflow.version,
|
|
1599
|
+
$status: workflow.status,
|
|
1600
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1601
|
+
$created: workflow.createdAt,
|
|
1602
|
+
$updated: workflow.updatedAt
|
|
1603
|
+
});
|
|
1604
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1605
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1606
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1607
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1608
|
+
WHERE id=$id
|
|
1609
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1610
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1611
|
+
))`).run({
|
|
1612
|
+
$id: current.id,
|
|
1613
|
+
$target: JSON.stringify(target),
|
|
1614
|
+
$updated: updated,
|
|
1615
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1616
|
+
$now: updated
|
|
1617
|
+
});
|
|
1618
|
+
if (res.changes !== 1)
|
|
1619
|
+
throw new Error("daemon lease lost");
|
|
1620
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1621
|
+
this.db.exec("COMMIT");
|
|
1622
|
+
const loop = this.getLoop(current.id);
|
|
1623
|
+
if (!loop)
|
|
1624
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1625
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
try {
|
|
1628
|
+
this.db.exec("ROLLBACK");
|
|
1629
|
+
} catch {}
|
|
1630
|
+
throw error;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1544
1633
|
renameLoop(id, name, opts = {}) {
|
|
1545
1634
|
const current = this.getLoop(id);
|
|
1546
1635
|
if (!current)
|
|
@@ -3549,6 +3638,9 @@ function appendBounded(current, chunk, maxBytes) {
|
|
|
3549
3638
|
return `[truncated ${overflow} bytes]
|
|
3550
3639
|
${next.slice(-maxBytes)}`;
|
|
3551
3640
|
}
|
|
3641
|
+
function appendBoundedText(current, chunk, maxBytes) {
|
|
3642
|
+
return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
|
|
3643
|
+
}
|
|
3552
3644
|
function killProcessGroup(pid) {
|
|
3553
3645
|
try {
|
|
3554
3646
|
process.kill(-pid, "SIGTERM");
|
|
@@ -3596,6 +3688,21 @@ function metadataEnv(metadata) {
|
|
|
3596
3688
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
3597
3689
|
return env;
|
|
3598
3690
|
}
|
|
3691
|
+
function codewithAgentIdempotencyKey(metadata) {
|
|
3692
|
+
const parts = [
|
|
3693
|
+
"openloops",
|
|
3694
|
+
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
3695
|
+
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
3696
|
+
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
3697
|
+
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
3698
|
+
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
3699
|
+
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
3700
|
+
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
3701
|
+
].filter(Boolean);
|
|
3702
|
+
if (parts.length > 1)
|
|
3703
|
+
return parts.join(":");
|
|
3704
|
+
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
3705
|
+
}
|
|
3599
3706
|
function allowlistEnv(allowlist) {
|
|
3600
3707
|
const env = {};
|
|
3601
3708
|
if (allowlist?.tools?.length)
|
|
@@ -3632,6 +3739,20 @@ function codewithLikeSandbox(target) {
|
|
|
3632
3739
|
function configStringValue(value) {
|
|
3633
3740
|
return JSON.stringify(value);
|
|
3634
3741
|
}
|
|
3742
|
+
var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
|
|
3743
|
+
"e",
|
|
3744
|
+
"exec",
|
|
3745
|
+
"agent",
|
|
3746
|
+
"start",
|
|
3747
|
+
"--ephemeral",
|
|
3748
|
+
"--ignore-rules",
|
|
3749
|
+
"--skip-git-repo-check",
|
|
3750
|
+
"--json",
|
|
3751
|
+
"--output-last-message",
|
|
3752
|
+
"-o",
|
|
3753
|
+
"--output-schema",
|
|
3754
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
3755
|
+
]);
|
|
3635
3756
|
function assertStringOption(value, label) {
|
|
3636
3757
|
if (value !== undefined && typeof value !== "string")
|
|
3637
3758
|
throw new Error(`${label} must be a string`);
|
|
@@ -3662,6 +3783,14 @@ function assertSupportedAgentOptions(target) {
|
|
|
3662
3783
|
}
|
|
3663
3784
|
if (target.provider === "codex" && target.agent !== undefined)
|
|
3664
3785
|
throw new Error("codex.agent is not supported");
|
|
3786
|
+
if (target.provider === "codewith" && target.agent !== undefined) {
|
|
3787
|
+
throw new Error("codewith.agent is not supported by the durable background-agent adapter");
|
|
3788
|
+
}
|
|
3789
|
+
if (target.provider === "codewith") {
|
|
3790
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
|
|
3791
|
+
if (unsafe)
|
|
3792
|
+
throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
|
|
3793
|
+
}
|
|
3665
3794
|
if (["codewith", "codex"].includes(target.provider)) {
|
|
3666
3795
|
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
3667
3796
|
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
@@ -3745,18 +3874,18 @@ function agentArgs(target) {
|
|
|
3745
3874
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
3746
3875
|
if (target.variant)
|
|
3747
3876
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
3748
|
-
args.push("--ask-for-approval", "never", "
|
|
3749
|
-
if (isolation === "safe")
|
|
3750
|
-
args.push("--ignore-rules");
|
|
3877
|
+
args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
|
|
3751
3878
|
if (target.cwd)
|
|
3752
3879
|
args.push("--cd", target.cwd);
|
|
3753
3880
|
for (const dir of target.addDirs ?? [])
|
|
3754
3881
|
args.push("--add-dir", dir);
|
|
3755
3882
|
if (target.model)
|
|
3756
3883
|
args.push("--model", target.model);
|
|
3757
|
-
if (target.agent)
|
|
3758
|
-
args.push("--agent", target.agent);
|
|
3759
3884
|
args.push(...target.extraArgs ?? []);
|
|
3885
|
+
args.push("agent", "start");
|
|
3886
|
+
if (target.cwd)
|
|
3887
|
+
args.push("--cwd", target.cwd);
|
|
3888
|
+
args.push(target.prompt);
|
|
3760
3889
|
return args;
|
|
3761
3890
|
case "codex":
|
|
3762
3891
|
if (target.variant)
|
|
@@ -3832,8 +3961,9 @@ function commandSpec(target) {
|
|
|
3832
3961
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
3833
3962
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
3834
3963
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
|
|
3835
|
-
stdin: agentTarget.prompt,
|
|
3836
|
-
allowlist: agentTarget.allowlist
|
|
3964
|
+
stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
|
|
3965
|
+
allowlist: agentTarget.allowlist,
|
|
3966
|
+
codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
|
|
3837
3967
|
};
|
|
3838
3968
|
}
|
|
3839
3969
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -3953,6 +4083,238 @@ function preflightNativeAuthProfile(spec, env) {
|
|
|
3953
4083
|
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
3954
4084
|
}
|
|
3955
4085
|
}
|
|
4086
|
+
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
4087
|
+
const args = agentArgs(target);
|
|
4088
|
+
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
4089
|
+
if (startIndex === -1)
|
|
4090
|
+
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
4091
|
+
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
4092
|
+
return args;
|
|
4093
|
+
}
|
|
4094
|
+
function codewithAgentControlArgs(target, command, agentId) {
|
|
4095
|
+
return [
|
|
4096
|
+
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
4097
|
+
"agent",
|
|
4098
|
+
command,
|
|
4099
|
+
...command === "logs" ? ["--limit", "20"] : [],
|
|
4100
|
+
agentId
|
|
4101
|
+
];
|
|
4102
|
+
}
|
|
4103
|
+
function parseJsonOutput(stdout, label) {
|
|
4104
|
+
try {
|
|
4105
|
+
const value = JSON.parse(stdout || "{}");
|
|
4106
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4107
|
+
throw new Error("not an object");
|
|
4108
|
+
return value;
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
function recordField(value, key) {
|
|
4114
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4115
|
+
return;
|
|
4116
|
+
const field = value[key];
|
|
4117
|
+
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
4118
|
+
}
|
|
4119
|
+
function stringField(value, key) {
|
|
4120
|
+
const field = value?.[key];
|
|
4121
|
+
return typeof field === "string" ? field : undefined;
|
|
4122
|
+
}
|
|
4123
|
+
function numberField(value, key) {
|
|
4124
|
+
const field = value?.[key];
|
|
4125
|
+
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
4126
|
+
}
|
|
4127
|
+
function codewithAgentStatus(readJson) {
|
|
4128
|
+
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
4129
|
+
}
|
|
4130
|
+
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
4131
|
+
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
4132
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
4133
|
+
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
4134
|
+
seq: numberField(event, "seq"),
|
|
4135
|
+
eventType: stringField(event, "eventType"),
|
|
4136
|
+
createdAt: numberField(event, "createdAt")
|
|
4137
|
+
})) : undefined;
|
|
4138
|
+
return JSON.stringify({
|
|
4139
|
+
codewithAgent: {
|
|
4140
|
+
agentId: stringField(agent, "agentId"),
|
|
4141
|
+
status: stringField(agent, "status"),
|
|
4142
|
+
desiredState: stringField(agent, "desiredState"),
|
|
4143
|
+
statusReason: stringField(agent, "statusReason"),
|
|
4144
|
+
threadId: stringField(agent, "threadId"),
|
|
4145
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
4146
|
+
pid: numberField(agent, "pid"),
|
|
4147
|
+
exitCode: numberField(agent, "exitCode"),
|
|
4148
|
+
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
4149
|
+
},
|
|
4150
|
+
statusSnapshot: statusSnapshot ? {
|
|
4151
|
+
seq: numberField(statusSnapshot, "seq"),
|
|
4152
|
+
status: stringField(statusSnapshot, "status"),
|
|
4153
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
4154
|
+
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
4155
|
+
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
4156
|
+
} : undefined,
|
|
4157
|
+
events
|
|
4158
|
+
}, null, 2);
|
|
4159
|
+
}
|
|
4160
|
+
function sleep(ms) {
|
|
4161
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4162
|
+
}
|
|
4163
|
+
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
4164
|
+
const target = spec.codewithDurableAgent?.target;
|
|
4165
|
+
if (!target)
|
|
4166
|
+
throw new Error("internal error: missing codewith durable target");
|
|
4167
|
+
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
4168
|
+
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
4169
|
+
const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
4170
|
+
cwd: spec.cwd,
|
|
4171
|
+
env,
|
|
4172
|
+
encoding: "utf8",
|
|
4173
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4174
|
+
timeout: 30000
|
|
4175
|
+
});
|
|
4176
|
+
let stderr = start.stderr || "";
|
|
4177
|
+
if (start.error || (start.status ?? 1) !== 0) {
|
|
4178
|
+
const finishedAt = nowIso();
|
|
4179
|
+
return {
|
|
4180
|
+
status: "failed",
|
|
4181
|
+
exitCode: start.status ?? undefined,
|
|
4182
|
+
stdout: start.stdout || "",
|
|
4183
|
+
stderr,
|
|
4184
|
+
error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
|
|
4185
|
+
startedAt,
|
|
4186
|
+
finishedAt,
|
|
4187
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
let startJson;
|
|
4191
|
+
try {
|
|
4192
|
+
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
4193
|
+
} catch (error) {
|
|
4194
|
+
const finishedAt = nowIso();
|
|
4195
|
+
return {
|
|
4196
|
+
status: "failed",
|
|
4197
|
+
stdout: start.stdout || "",
|
|
4198
|
+
stderr,
|
|
4199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4200
|
+
startedAt,
|
|
4201
|
+
finishedAt,
|
|
4202
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
4206
|
+
if (!agentId) {
|
|
4207
|
+
const finishedAt = nowIso();
|
|
4208
|
+
return {
|
|
4209
|
+
status: "failed",
|
|
4210
|
+
stdout: start.stdout || "",
|
|
4211
|
+
stderr,
|
|
4212
|
+
error: "codewith agent start did not return agent.agentId",
|
|
4213
|
+
startedAt,
|
|
4214
|
+
finishedAt,
|
|
4215
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
4219
|
+
let lastReadJson = startJson;
|
|
4220
|
+
let lastLogsJson;
|
|
4221
|
+
let stdout = "";
|
|
4222
|
+
while (true) {
|
|
4223
|
+
if (opts.signal?.aborted) {
|
|
4224
|
+
spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
|
|
4225
|
+
const finishedAt = nowIso();
|
|
4226
|
+
return {
|
|
4227
|
+
status: "failed",
|
|
4228
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4229
|
+
stderr,
|
|
4230
|
+
error: "cancelled",
|
|
4231
|
+
startedAt,
|
|
4232
|
+
finishedAt,
|
|
4233
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
4237
|
+
cwd: spec.cwd,
|
|
4238
|
+
env,
|
|
4239
|
+
encoding: "utf8",
|
|
4240
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4241
|
+
timeout: 30000
|
|
4242
|
+
});
|
|
4243
|
+
stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
|
|
4244
|
+
if (read.error || (read.status ?? 1) !== 0) {
|
|
4245
|
+
const finishedAt = nowIso();
|
|
4246
|
+
return {
|
|
4247
|
+
status: "failed",
|
|
4248
|
+
exitCode: read.status ?? undefined,
|
|
4249
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4250
|
+
stderr,
|
|
4251
|
+
error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
|
|
4252
|
+
startedAt,
|
|
4253
|
+
finishedAt,
|
|
4254
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
try {
|
|
4258
|
+
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
4259
|
+
} catch (error) {
|
|
4260
|
+
const finishedAt = nowIso();
|
|
4261
|
+
return {
|
|
4262
|
+
status: "failed",
|
|
4263
|
+
stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
|
|
4264
|
+
stderr,
|
|
4265
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4266
|
+
startedAt,
|
|
4267
|
+
finishedAt,
|
|
4268
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4269
|
+
};
|
|
4270
|
+
}
|
|
4271
|
+
const status = codewithAgentStatus(lastReadJson);
|
|
4272
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
4273
|
+
const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
4274
|
+
cwd: spec.cwd,
|
|
4275
|
+
env,
|
|
4276
|
+
encoding: "utf8",
|
|
4277
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4278
|
+
timeout: 30000
|
|
4279
|
+
});
|
|
4280
|
+
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
4281
|
+
try {
|
|
4282
|
+
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
4283
|
+
} catch {
|
|
4284
|
+
lastLogsJson = undefined;
|
|
4285
|
+
}
|
|
4286
|
+
} else {
|
|
4287
|
+
stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
|
|
4288
|
+
}
|
|
4289
|
+
const finishedAt = nowIso();
|
|
4290
|
+
const resultStatus = status === "completed" ? "succeeded" : "failed";
|
|
4291
|
+
return {
|
|
4292
|
+
status: resultStatus,
|
|
4293
|
+
exitCode: resultStatus === "succeeded" ? 0 : 1,
|
|
4294
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4295
|
+
stderr,
|
|
4296
|
+
error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
|
|
4297
|
+
startedAt,
|
|
4298
|
+
finishedAt,
|
|
4299
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
4303
|
+
spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
|
|
4304
|
+
const finishedAt = nowIso();
|
|
4305
|
+
return {
|
|
4306
|
+
status: "timed_out",
|
|
4307
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4308
|
+
stderr,
|
|
4309
|
+
error: `timed out after ${spec.timeoutMs}ms`,
|
|
4310
|
+
startedAt,
|
|
4311
|
+
finishedAt,
|
|
4312
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4315
|
+
await sleep(pollMs);
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
3956
4318
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
3957
4319
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
3958
4320
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -4124,6 +4486,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
4124
4486
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
4125
4487
|
const spec = commandSpec(target);
|
|
4126
4488
|
const machine = resolvedMachine(opts);
|
|
4489
|
+
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
4490
|
+
const startedAt2 = nowIso();
|
|
4491
|
+
const finishedAt2 = nowIso();
|
|
4492
|
+
return {
|
|
4493
|
+
status: "failed",
|
|
4494
|
+
stdout: "",
|
|
4495
|
+
stderr: "",
|
|
4496
|
+
error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
|
|
4497
|
+
startedAt: startedAt2,
|
|
4498
|
+
finishedAt: finishedAt2,
|
|
4499
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4127
4502
|
if (machine && !machine.local)
|
|
4128
4503
|
return executeRemoteSpec(spec, machine, metadata, opts);
|
|
4129
4504
|
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
@@ -4170,6 +4545,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
4170
4545
|
durationMs: 0
|
|
4171
4546
|
};
|
|
4172
4547
|
}
|
|
4548
|
+
if (spec.codewithDurableAgent) {
|
|
4549
|
+
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
4550
|
+
}
|
|
4173
4551
|
const child = spawn(spec.command, spec.args, {
|
|
4174
4552
|
cwd: spec.cwd,
|
|
4175
4553
|
env,
|
|
@@ -4521,7 +4899,7 @@ function publicGoalRun(run) {
|
|
|
4521
4899
|
// package.json
|
|
4522
4900
|
var package_default = {
|
|
4523
4901
|
name: "@hasna/loops",
|
|
4524
|
-
version: "0.3.
|
|
4902
|
+
version: "0.3.60",
|
|
4525
4903
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4526
4904
|
type: "module",
|
|
4527
4905
|
main: "dist/index.js",
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -99,6 +99,16 @@ export declare class Store {
|
|
|
99
99
|
previousWorkflow: WorkflowSpec;
|
|
100
100
|
archivedOld?: WorkflowSpec;
|
|
101
101
|
};
|
|
102
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName: string, opts: DaemonLeaseFence & {
|
|
103
|
+
workflowName: string;
|
|
104
|
+
workflowTimeoutMs?: TimeoutMs;
|
|
105
|
+
archiveOld?: boolean;
|
|
106
|
+
}): {
|
|
107
|
+
loop: Loop;
|
|
108
|
+
workflow: WorkflowSpec;
|
|
109
|
+
previousWorkflow: WorkflowSpec;
|
|
110
|
+
archivedOld?: WorkflowSpec;
|
|
111
|
+
};
|
|
102
112
|
renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
|
|
103
113
|
archiveLoop(idOrName: string): Loop;
|
|
104
114
|
unarchiveLoop(idOrName: string): Loop;
|
package/dist/lib/store.js
CHANGED
|
@@ -356,6 +356,20 @@ function optionalStringArray(value, label) {
|
|
|
356
356
|
}).filter(Boolean);
|
|
357
357
|
return values.length ? values : undefined;
|
|
358
358
|
}
|
|
359
|
+
var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
|
|
360
|
+
"e",
|
|
361
|
+
"exec",
|
|
362
|
+
"agent",
|
|
363
|
+
"start",
|
|
364
|
+
"--ephemeral",
|
|
365
|
+
"--ignore-rules",
|
|
366
|
+
"--skip-git-repo-check",
|
|
367
|
+
"--json",
|
|
368
|
+
"--output-last-message",
|
|
369
|
+
"-o",
|
|
370
|
+
"--output-schema",
|
|
371
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
372
|
+
]);
|
|
359
373
|
function optionalAccountRef(value, label) {
|
|
360
374
|
if (value === undefined)
|
|
361
375
|
return;
|
|
@@ -470,6 +484,15 @@ function validateTarget(value, label, opts) {
|
|
|
470
484
|
throw new Error(`${label}.variant is not supported for provider cursor`);
|
|
471
485
|
if (value.provider === "codex" && value.agent !== undefined)
|
|
472
486
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
487
|
+
if (value.provider === "codewith" && value.agent !== undefined) {
|
|
488
|
+
throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
|
|
489
|
+
}
|
|
490
|
+
const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
|
|
491
|
+
if (value.provider === "codewith") {
|
|
492
|
+
const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
|
|
493
|
+
if (unsafe)
|
|
494
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
|
|
495
|
+
}
|
|
473
496
|
optionalStringArray(value.addDirs, `${label}.addDirs`);
|
|
474
497
|
if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
|
|
475
498
|
throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
|
|
@@ -545,7 +568,9 @@ function validateTarget(value, label, opts) {
|
|
|
545
568
|
if (value.routing.eventSource !== undefined)
|
|
546
569
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
547
570
|
}
|
|
548
|
-
const target = { ...value };
|
|
571
|
+
const target = { ...value, extraArgs };
|
|
572
|
+
if (!extraArgs)
|
|
573
|
+
delete target.extraArgs;
|
|
549
574
|
delete target.promptFile;
|
|
550
575
|
delete target.promptSource;
|
|
551
576
|
return { ...target, ...promptFields };
|
|
@@ -1541,6 +1566,70 @@ class Store {
|
|
|
1541
1566
|
throw error;
|
|
1542
1567
|
}
|
|
1543
1568
|
}
|
|
1569
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1570
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1571
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1572
|
+
try {
|
|
1573
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1574
|
+
if (current.target.type !== "workflow")
|
|
1575
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1576
|
+
if (this.hasRunningRun(current.id))
|
|
1577
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1578
|
+
if (!current.goal)
|
|
1579
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1580
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1581
|
+
if (!previousWorkflow.goal)
|
|
1582
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1583
|
+
const workflow = {
|
|
1584
|
+
id: genId(),
|
|
1585
|
+
name: opts.workflowName,
|
|
1586
|
+
description: previousWorkflow.description,
|
|
1587
|
+
version: previousWorkflow.version,
|
|
1588
|
+
status: "active",
|
|
1589
|
+
steps: previousWorkflow.steps,
|
|
1590
|
+
createdAt: updated,
|
|
1591
|
+
updatedAt: updated
|
|
1592
|
+
};
|
|
1593
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1594
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1595
|
+
$id: workflow.id,
|
|
1596
|
+
$name: workflow.name,
|
|
1597
|
+
$description: workflow.description ?? null,
|
|
1598
|
+
$version: workflow.version,
|
|
1599
|
+
$status: workflow.status,
|
|
1600
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1601
|
+
$created: workflow.createdAt,
|
|
1602
|
+
$updated: workflow.updatedAt
|
|
1603
|
+
});
|
|
1604
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1605
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1606
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1607
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1608
|
+
WHERE id=$id
|
|
1609
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1610
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1611
|
+
))`).run({
|
|
1612
|
+
$id: current.id,
|
|
1613
|
+
$target: JSON.stringify(target),
|
|
1614
|
+
$updated: updated,
|
|
1615
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1616
|
+
$now: updated
|
|
1617
|
+
});
|
|
1618
|
+
if (res.changes !== 1)
|
|
1619
|
+
throw new Error("daemon lease lost");
|
|
1620
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1621
|
+
this.db.exec("COMMIT");
|
|
1622
|
+
const loop = this.getLoop(current.id);
|
|
1623
|
+
if (!loop)
|
|
1624
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1625
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
try {
|
|
1628
|
+
this.db.exec("ROLLBACK");
|
|
1629
|
+
} catch {}
|
|
1630
|
+
throw error;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1544
1633
|
renameLoop(id, name, opts = {}) {
|
|
1545
1634
|
const current = this.getLoop(id);
|
|
1546
1635
|
if (!current)
|