@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/sdk/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)
|
|
@@ -3414,6 +3503,9 @@ function appendBounded(current, chunk, maxBytes) {
|
|
|
3414
3503
|
return `[truncated ${overflow} bytes]
|
|
3415
3504
|
${next.slice(-maxBytes)}`;
|
|
3416
3505
|
}
|
|
3506
|
+
function appendBoundedText(current, chunk, maxBytes) {
|
|
3507
|
+
return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
|
|
3508
|
+
}
|
|
3417
3509
|
function killProcessGroup(pid) {
|
|
3418
3510
|
try {
|
|
3419
3511
|
process.kill(-pid, "SIGTERM");
|
|
@@ -3461,6 +3553,21 @@ function metadataEnv(metadata) {
|
|
|
3461
3553
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
3462
3554
|
return env;
|
|
3463
3555
|
}
|
|
3556
|
+
function codewithAgentIdempotencyKey(metadata) {
|
|
3557
|
+
const parts = [
|
|
3558
|
+
"openloops",
|
|
3559
|
+
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
3560
|
+
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
3561
|
+
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
3562
|
+
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
3563
|
+
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
3564
|
+
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
3565
|
+
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
3566
|
+
].filter(Boolean);
|
|
3567
|
+
if (parts.length > 1)
|
|
3568
|
+
return parts.join(":");
|
|
3569
|
+
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
3570
|
+
}
|
|
3464
3571
|
function allowlistEnv(allowlist) {
|
|
3465
3572
|
const env = {};
|
|
3466
3573
|
if (allowlist?.tools?.length)
|
|
@@ -3497,6 +3604,20 @@ function codewithLikeSandbox(target) {
|
|
|
3497
3604
|
function configStringValue(value) {
|
|
3498
3605
|
return JSON.stringify(value);
|
|
3499
3606
|
}
|
|
3607
|
+
var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
|
|
3608
|
+
"e",
|
|
3609
|
+
"exec",
|
|
3610
|
+
"agent",
|
|
3611
|
+
"start",
|
|
3612
|
+
"--ephemeral",
|
|
3613
|
+
"--ignore-rules",
|
|
3614
|
+
"--skip-git-repo-check",
|
|
3615
|
+
"--json",
|
|
3616
|
+
"--output-last-message",
|
|
3617
|
+
"-o",
|
|
3618
|
+
"--output-schema",
|
|
3619
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
3620
|
+
]);
|
|
3500
3621
|
function assertStringOption(value, label) {
|
|
3501
3622
|
if (value !== undefined && typeof value !== "string")
|
|
3502
3623
|
throw new Error(`${label} must be a string`);
|
|
@@ -3527,6 +3648,14 @@ function assertSupportedAgentOptions(target) {
|
|
|
3527
3648
|
}
|
|
3528
3649
|
if (target.provider === "codex" && target.agent !== undefined)
|
|
3529
3650
|
throw new Error("codex.agent is not supported");
|
|
3651
|
+
if (target.provider === "codewith" && target.agent !== undefined) {
|
|
3652
|
+
throw new Error("codewith.agent is not supported by the durable background-agent adapter");
|
|
3653
|
+
}
|
|
3654
|
+
if (target.provider === "codewith") {
|
|
3655
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
|
|
3656
|
+
if (unsafe)
|
|
3657
|
+
throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
|
|
3658
|
+
}
|
|
3530
3659
|
if (["codewith", "codex"].includes(target.provider)) {
|
|
3531
3660
|
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
3532
3661
|
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
@@ -3610,18 +3739,18 @@ function agentArgs(target) {
|
|
|
3610
3739
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
3611
3740
|
if (target.variant)
|
|
3612
3741
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
3613
|
-
args.push("--ask-for-approval", "never", "
|
|
3614
|
-
if (isolation === "safe")
|
|
3615
|
-
args.push("--ignore-rules");
|
|
3742
|
+
args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
|
|
3616
3743
|
if (target.cwd)
|
|
3617
3744
|
args.push("--cd", target.cwd);
|
|
3618
3745
|
for (const dir of target.addDirs ?? [])
|
|
3619
3746
|
args.push("--add-dir", dir);
|
|
3620
3747
|
if (target.model)
|
|
3621
3748
|
args.push("--model", target.model);
|
|
3622
|
-
if (target.agent)
|
|
3623
|
-
args.push("--agent", target.agent);
|
|
3624
3749
|
args.push(...target.extraArgs ?? []);
|
|
3750
|
+
args.push("agent", "start");
|
|
3751
|
+
if (target.cwd)
|
|
3752
|
+
args.push("--cwd", target.cwd);
|
|
3753
|
+
args.push(target.prompt);
|
|
3625
3754
|
return args;
|
|
3626
3755
|
case "codex":
|
|
3627
3756
|
if (target.variant)
|
|
@@ -3697,8 +3826,9 @@ function commandSpec(target) {
|
|
|
3697
3826
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
3698
3827
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
3699
3828
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
|
|
3700
|
-
stdin: agentTarget.prompt,
|
|
3701
|
-
allowlist: agentTarget.allowlist
|
|
3829
|
+
stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
|
|
3830
|
+
allowlist: agentTarget.allowlist,
|
|
3831
|
+
codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
|
|
3702
3832
|
};
|
|
3703
3833
|
}
|
|
3704
3834
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -3818,6 +3948,238 @@ function preflightNativeAuthProfile(spec, env) {
|
|
|
3818
3948
|
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
3819
3949
|
}
|
|
3820
3950
|
}
|
|
3951
|
+
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
3952
|
+
const args = agentArgs(target);
|
|
3953
|
+
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
3954
|
+
if (startIndex === -1)
|
|
3955
|
+
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
3956
|
+
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
3957
|
+
return args;
|
|
3958
|
+
}
|
|
3959
|
+
function codewithAgentControlArgs(target, command, agentId) {
|
|
3960
|
+
return [
|
|
3961
|
+
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
3962
|
+
"agent",
|
|
3963
|
+
command,
|
|
3964
|
+
...command === "logs" ? ["--limit", "20"] : [],
|
|
3965
|
+
agentId
|
|
3966
|
+
];
|
|
3967
|
+
}
|
|
3968
|
+
function parseJsonOutput(stdout, label) {
|
|
3969
|
+
try {
|
|
3970
|
+
const value = JSON.parse(stdout || "{}");
|
|
3971
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3972
|
+
throw new Error("not an object");
|
|
3973
|
+
return value;
|
|
3974
|
+
} catch (error) {
|
|
3975
|
+
throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function recordField(value, key) {
|
|
3979
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3980
|
+
return;
|
|
3981
|
+
const field = value[key];
|
|
3982
|
+
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
3983
|
+
}
|
|
3984
|
+
function stringField(value, key) {
|
|
3985
|
+
const field = value?.[key];
|
|
3986
|
+
return typeof field === "string" ? field : undefined;
|
|
3987
|
+
}
|
|
3988
|
+
function numberField(value, key) {
|
|
3989
|
+
const field = value?.[key];
|
|
3990
|
+
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
3991
|
+
}
|
|
3992
|
+
function codewithAgentStatus(readJson) {
|
|
3993
|
+
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
3994
|
+
}
|
|
3995
|
+
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
3996
|
+
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
3997
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
3998
|
+
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
3999
|
+
seq: numberField(event, "seq"),
|
|
4000
|
+
eventType: stringField(event, "eventType"),
|
|
4001
|
+
createdAt: numberField(event, "createdAt")
|
|
4002
|
+
})) : undefined;
|
|
4003
|
+
return JSON.stringify({
|
|
4004
|
+
codewithAgent: {
|
|
4005
|
+
agentId: stringField(agent, "agentId"),
|
|
4006
|
+
status: stringField(agent, "status"),
|
|
4007
|
+
desiredState: stringField(agent, "desiredState"),
|
|
4008
|
+
statusReason: stringField(agent, "statusReason"),
|
|
4009
|
+
threadId: stringField(agent, "threadId"),
|
|
4010
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
4011
|
+
pid: numberField(agent, "pid"),
|
|
4012
|
+
exitCode: numberField(agent, "exitCode"),
|
|
4013
|
+
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
4014
|
+
},
|
|
4015
|
+
statusSnapshot: statusSnapshot ? {
|
|
4016
|
+
seq: numberField(statusSnapshot, "seq"),
|
|
4017
|
+
status: stringField(statusSnapshot, "status"),
|
|
4018
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
4019
|
+
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
4020
|
+
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
4021
|
+
} : undefined,
|
|
4022
|
+
events
|
|
4023
|
+
}, null, 2);
|
|
4024
|
+
}
|
|
4025
|
+
function sleep(ms) {
|
|
4026
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4027
|
+
}
|
|
4028
|
+
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
4029
|
+
const target = spec.codewithDurableAgent?.target;
|
|
4030
|
+
if (!target)
|
|
4031
|
+
throw new Error("internal error: missing codewith durable target");
|
|
4032
|
+
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
4033
|
+
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
4034
|
+
const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
4035
|
+
cwd: spec.cwd,
|
|
4036
|
+
env,
|
|
4037
|
+
encoding: "utf8",
|
|
4038
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4039
|
+
timeout: 30000
|
|
4040
|
+
});
|
|
4041
|
+
let stderr = start.stderr || "";
|
|
4042
|
+
if (start.error || (start.status ?? 1) !== 0) {
|
|
4043
|
+
const finishedAt = nowIso();
|
|
4044
|
+
return {
|
|
4045
|
+
status: "failed",
|
|
4046
|
+
exitCode: start.status ?? undefined,
|
|
4047
|
+
stdout: start.stdout || "",
|
|
4048
|
+
stderr,
|
|
4049
|
+
error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
|
|
4050
|
+
startedAt,
|
|
4051
|
+
finishedAt,
|
|
4052
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
let startJson;
|
|
4056
|
+
try {
|
|
4057
|
+
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
4058
|
+
} catch (error) {
|
|
4059
|
+
const finishedAt = nowIso();
|
|
4060
|
+
return {
|
|
4061
|
+
status: "failed",
|
|
4062
|
+
stdout: start.stdout || "",
|
|
4063
|
+
stderr,
|
|
4064
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4065
|
+
startedAt,
|
|
4066
|
+
finishedAt,
|
|
4067
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
4071
|
+
if (!agentId) {
|
|
4072
|
+
const finishedAt = nowIso();
|
|
4073
|
+
return {
|
|
4074
|
+
status: "failed",
|
|
4075
|
+
stdout: start.stdout || "",
|
|
4076
|
+
stderr,
|
|
4077
|
+
error: "codewith agent start did not return agent.agentId",
|
|
4078
|
+
startedAt,
|
|
4079
|
+
finishedAt,
|
|
4080
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
4084
|
+
let lastReadJson = startJson;
|
|
4085
|
+
let lastLogsJson;
|
|
4086
|
+
let stdout = "";
|
|
4087
|
+
while (true) {
|
|
4088
|
+
if (opts.signal?.aborted) {
|
|
4089
|
+
spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
|
|
4090
|
+
const finishedAt = nowIso();
|
|
4091
|
+
return {
|
|
4092
|
+
status: "failed",
|
|
4093
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4094
|
+
stderr,
|
|
4095
|
+
error: "cancelled",
|
|
4096
|
+
startedAt,
|
|
4097
|
+
finishedAt,
|
|
4098
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
4102
|
+
cwd: spec.cwd,
|
|
4103
|
+
env,
|
|
4104
|
+
encoding: "utf8",
|
|
4105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4106
|
+
timeout: 30000
|
|
4107
|
+
});
|
|
4108
|
+
stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
|
|
4109
|
+
if (read.error || (read.status ?? 1) !== 0) {
|
|
4110
|
+
const finishedAt = nowIso();
|
|
4111
|
+
return {
|
|
4112
|
+
status: "failed",
|
|
4113
|
+
exitCode: read.status ?? undefined,
|
|
4114
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4115
|
+
stderr,
|
|
4116
|
+
error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
|
|
4117
|
+
startedAt,
|
|
4118
|
+
finishedAt,
|
|
4119
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
try {
|
|
4123
|
+
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
const finishedAt = nowIso();
|
|
4126
|
+
return {
|
|
4127
|
+
status: "failed",
|
|
4128
|
+
stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
|
|
4129
|
+
stderr,
|
|
4130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4131
|
+
startedAt,
|
|
4132
|
+
finishedAt,
|
|
4133
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
const status = codewithAgentStatus(lastReadJson);
|
|
4137
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
4138
|
+
const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
4139
|
+
cwd: spec.cwd,
|
|
4140
|
+
env,
|
|
4141
|
+
encoding: "utf8",
|
|
4142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4143
|
+
timeout: 30000
|
|
4144
|
+
});
|
|
4145
|
+
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
4146
|
+
try {
|
|
4147
|
+
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
4148
|
+
} catch {
|
|
4149
|
+
lastLogsJson = undefined;
|
|
4150
|
+
}
|
|
4151
|
+
} else {
|
|
4152
|
+
stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
|
|
4153
|
+
}
|
|
4154
|
+
const finishedAt = nowIso();
|
|
4155
|
+
const resultStatus = status === "completed" ? "succeeded" : "failed";
|
|
4156
|
+
return {
|
|
4157
|
+
status: resultStatus,
|
|
4158
|
+
exitCode: resultStatus === "succeeded" ? 0 : 1,
|
|
4159
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4160
|
+
stderr,
|
|
4161
|
+
error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
|
|
4162
|
+
startedAt,
|
|
4163
|
+
finishedAt,
|
|
4164
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
4168
|
+
spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
|
|
4169
|
+
const finishedAt = nowIso();
|
|
4170
|
+
return {
|
|
4171
|
+
status: "timed_out",
|
|
4172
|
+
stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
|
|
4173
|
+
stderr,
|
|
4174
|
+
error: `timed out after ${spec.timeoutMs}ms`,
|
|
4175
|
+
startedAt,
|
|
4176
|
+
finishedAt,
|
|
4177
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
await sleep(pollMs);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
3821
4183
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
3822
4184
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
3823
4185
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -3989,6 +4351,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
3989
4351
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
3990
4352
|
const spec = commandSpec(target);
|
|
3991
4353
|
const machine = resolvedMachine(opts);
|
|
4354
|
+
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
4355
|
+
const startedAt2 = nowIso();
|
|
4356
|
+
const finishedAt2 = nowIso();
|
|
4357
|
+
return {
|
|
4358
|
+
status: "failed",
|
|
4359
|
+
stdout: "",
|
|
4360
|
+
stderr: "",
|
|
4361
|
+
error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
|
|
4362
|
+
startedAt: startedAt2,
|
|
4363
|
+
finishedAt: finishedAt2,
|
|
4364
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
3992
4367
|
if (machine && !machine.local)
|
|
3993
4368
|
return executeRemoteSpec(spec, machine, metadata, opts);
|
|
3994
4369
|
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
@@ -4035,6 +4410,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
4035
4410
|
durationMs: 0
|
|
4036
4411
|
};
|
|
4037
4412
|
}
|
|
4413
|
+
if (spec.codewithDurableAgent) {
|
|
4414
|
+
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
4415
|
+
}
|
|
4038
4416
|
const child = spawn(spec.command, spec.args, {
|
|
4039
4417
|
cwd: spec.cwd,
|
|
4040
4418
|
env,
|
package/docs/USAGE.md
CHANGED
|
@@ -6,7 +6,7 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
|
|
|
6
6
|
|
|
7
7
|
- `claude`
|
|
8
8
|
- `agent` (Cursor Agent CLI)
|
|
9
|
-
- `codewith
|
|
9
|
+
- `codewith agent start`
|
|
10
10
|
- `aicopilot run`
|
|
11
11
|
- `opencode run`
|
|
12
12
|
- `codex exec`
|
|
@@ -893,13 +893,13 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
|
|
|
893
893
|
The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
|
|
894
894
|
|
|
895
895
|
- Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
|
|
896
|
-
- Codewith uses `codewith --ask-for-approval never exec
|
|
896
|
+
- Codewith uses durable `codewith --ask-for-approval never agent start` background-agent runs by default, then polls `codewith agent read` until the run is terminal and records compact status/event evidence. Remote Codewith agent steps fail closed until remote durable readback is implemented, so workflows do not advance immediately after enqueue. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, or other non-durable exec-only flags for task-lifecycle, planner, worker, verifier, reviewer, release, or other long-running agentic work.
|
|
897
897
|
- AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
|
|
898
898
|
- Cursor is CLI-first for now via the standalone `agent -p` binary. OpenLoops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
|
|
899
899
|
- Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
|
|
900
|
-
- Agent prompts are sent through child stdin instead of argv so
|
|
900
|
+
- Agent prompts are sent through child stdin instead of argv where the provider supports stdin. Codewith durable background agents currently accept prompts as native `agent start` arguments, so OpenLoops stores only bounded status/event evidence and omits raw Codewith event payloads from workflow stdout.
|
|
901
901
|
- When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
|
|
902
|
-
- `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `
|
|
902
|
+
- `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `agent start/read/logs`; they do not call OpenAccounts.
|
|
903
903
|
- `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
|
|
904
904
|
- `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
|
|
905
905
|
- `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
|
package/package.json
CHANGED