@loops-adk/core 0.1.0 → 0.2.0
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 +120 -13
- package/assets/logo.png +0 -0
- package/bin/loops.mjs +5 -5
- package/dist/{agent-sdk-RF5VJZAT.js → agent-sdk-4QJDWM7N.js} +3 -3
- package/dist/{agent-sdk-RF5VJZAT.js.map → agent-sdk-4QJDWM7N.js.map} +1 -1
- package/dist/api.d.ts +177 -3
- package/dist/api.js +26 -10
- package/dist/api.js.map +1 -1
- package/dist/{chunk-XC46B4FD.js → chunk-MA6NDQMO.js} +2 -2
- package/dist/chunk-MA6NDQMO.js.map +1 -0
- package/dist/{chunk-3BPU34DE.js → chunk-WM5QVHM2.js} +789 -46
- package/dist/chunk-WM5QVHM2.js.map +1 -0
- package/dist/{claude-cli-U7WEVAOL.js → claude-cli-75AOQUKG.js} +3 -3
- package/dist/{claude-cli-U7WEVAOL.js.map → claude-cli-75AOQUKG.js.map} +1 -1
- package/dist/{codex-6I5UZ2HM.js → codex-LYZF52WL.js} +25 -13
- package/dist/codex-LYZF52WL.js.map +1 -0
- package/dist/env/command.d.ts +1 -1
- package/dist/env/docker.d.ts +1 -1
- package/dist/env/sst.d.ts +1 -1
- package/dist/index.js +249 -11
- package/dist/index.js.map +1 -1
- package/dist/{types-B4wGVpqo.d.ts → types-Cv_3ymr9.d.ts} +118 -37
- package/package.json +10 -1
- package/skills/author-loop/SKILL.md +25 -14
- package/skills/design-agent-team/SKILL.md +108 -0
- package/skills/supervise-loop-run/SKILL.md +64 -0
- package/dist/chunk-3BPU34DE.js.map +0 -1
- package/dist/chunk-XC46B4FD.js.map +0 -1
- package/dist/codex-6I5UZ2HM.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { redactSecrets } from './chunk-JFTXJ7I2.js';
|
|
2
2
|
import { newAccumulator, mapMessage } from './chunk-CXEPZHSR.js';
|
|
3
|
-
import { SUBAGENT_TOOLS } from './chunk-
|
|
3
|
+
import { SUBAGENT_TOOLS } from './chunk-MA6NDQMO.js';
|
|
4
4
|
import { LoopError } from './chunk-I3STY7U6.js';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
|
|
@@ -120,5 +120,5 @@ ${stdout}`);
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
export { ClaudeCliEngine, buildClaudeArgs, classifyCliLimit };
|
|
123
|
-
//# sourceMappingURL=claude-cli-
|
|
124
|
-
//# sourceMappingURL=claude-cli-
|
|
123
|
+
//# sourceMappingURL=claude-cli-75AOQUKG.js.map
|
|
124
|
+
//# sourceMappingURL=claude-cli-75AOQUKG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/engines/claude-cli.ts"],"names":[],"mappings":"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","file":"claude-cli-U7WEVAOL.js","sourcesContent":["/**\n * Engine adapter: the `claude` CLI as a subprocess. A fresh process per call =\n * a fresh context. Robust spawning + abort + timeout via `execa`; output is the\n * same stream-json schema the Agent SDK emits, so we reuse `mapMessage`.\n */\n\nimport { execa } from 'execa';\nimport {\n SUBAGENT_TOOLS,\n type AgentRequest,\n type AgentResult,\n type Engine,\n type EngineEventSink,\n type EngineOptions,\n} from './engine.ts';\nimport { mapMessage, newAccumulator } from './message-map.ts';\nimport { LoopError } from '../core/errors.ts';\nimport { redactSecrets } from '../core/redact.ts';\n\n/**\n * Classify a failed `claude` subprocess into a provider-limit `LoopError`, or\n * return `undefined` to fall through to the generic ENGINE/TIMEOUT mapping. The\n * CLI has no structured limit channel on a hard failure, so we read its\n * (already-redacted) output text:\n * - a usage/quota limit (\"usage limit reached\", \"out of credits\") → QUOTA.\n * A reset time, when the message states one (epoch seconds or an absolute\n * time the CLI prints), makes it auto-waitable; otherwise QUOTA has no\n * reset and the loop policy checkpoints-and-pauses.\n * - a plain \"rate limit\" → RATE_LIMIT (resets on its own).\n * Order matters: usage/quota is checked first so a usage message that also\n * contains the words \"rate limit\" is not mis-tagged as a transient throttle.\n *\n * Exported for unit testing without spawning a subprocess (mirrors\n * `buildClaudeArgs`).\n */\nexport function classifyCliLimit(text: string): LoopError | undefined {\n const lower = text.toLowerCase();\n const isUsage =\n /usage limit|out of credits|insufficient credits|quota|billing/.test(lower);\n const isRate = /rate limit|rate-limit|too many requests|429/.test(lower);\n if (!isUsage && !isRate) return undefined;\n\n const resetAt = parseResetAt(text);\n if (isUsage) {\n return new LoopError({\n code: 'QUOTA',\n phase: 'engine',\n message: `claude usage limit: ${text}`,\n resetAt,\n });\n }\n return new LoopError({\n code: 'RATE_LIMIT',\n phase: 'engine',\n message: `claude rate limited: ${text}`,\n resetAt,\n });\n}\n\n/**\n * Pull a reset time (epoch ms) out of CLI limit text. The CLI states a reset as\n * an epoch-seconds value (e.g. `resets at 1700000000`); convert to ms. Returns\n * `undefined` when no reset is stated — a quota with no parseable reset is not\n * auto-waitable.\n */\nfunction parseResetAt(text: string): number | undefined {\n const m = /(?:reset|resets|retry|available)\\D{0,20}(\\d{10,13})/i.exec(text);\n if (!m) return undefined;\n const n = Number(m[1]);\n if (!Number.isFinite(n)) return undefined;\n // 10-digit values are epoch seconds; 13-digit are already ms.\n return m[1]!.length <= 10 ? n * 1000 : n;\n}\n\n/**\n * Build the `claude` argv for one run. Extracted (and exported) so the flag\n * wiring — model, system prompt, tool allowlist, permission mode, the `--`\n * argument-smuggling guard — is unit-testable without spawning a process.\n */\nexport function buildClaudeArgs(\n req: AgentRequest,\n opts: EngineOptions,\n): string[] {\n const model = req.model ?? opts.defaultModel;\n const args = ['-p', '--output-format', 'stream-json', '--verbose'];\n if (model) args.push('--model', model);\n if (req.system) args.push('--append-system-prompt', req.system);\n if (req.allowedTools?.length)\n args.push('--allowedTools', req.allowedTools.join(','));\n // A leaf agent may not spawn sub-agents — disallow the spawn tool (wins over any allowlist).\n if (req.leaf) args.push('--disallowedTools', SUBAGENT_TOOLS.join(','));\n if (opts.permissionMode) args.push('--permission-mode', opts.permissionMode);\n if (opts.cliArgs?.length) args.push(...opts.cliArgs);\n // `--` ends option parsing so a prompt starting with `-` can't be\n // mis-interpreted by `claude` as a flag (argument smuggling).\n args.push('--', req.prompt);\n return args;\n}\n\nexport class ClaudeCliEngine implements Engine {\n readonly name = 'claude-cli';\n constructor(private readonly opts: EngineOptions = {}) {}\n\n async run(\n req: AgentRequest,\n onEvent: EngineEventSink,\n signal: AbortSignal,\n ): Promise<AgentResult> {\n const bin = this.opts.cliBinary ?? 'claude';\n const model = req.model ?? this.opts.defaultModel;\n const args = buildClaudeArgs(req, this.opts);\n\n const acc = newAccumulator(model ?? 'claude-cli');\n // Buffered (default) so `stderr` is a string for error messages; we still\n // attach a `data` listener to stream stdout line-by-line as it arrives.\n const sub = execa(bin, args, {\n cwd: req.cwd,\n cancelSignal: signal,\n // The prompt is passed as an argument, not piped — don't let `claude -p`\n // stall waiting on stdin.\n stdin: 'ignore',\n // If the child ignores the SIGTERM from an abort/timeout, escalate to\n // SIGKILL so a wedged subprocess can't make Ctrl-C hang.\n forceKillAfterDelay: 5000,\n reject: false,\n timeout: req.timeoutMs,\n stripFinalNewline: false,\n });\n\n let buffer = '';\n const flush = (line: string) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n try {\n mapMessage(JSON.parse(trimmed), acc, onEvent);\n } catch {\n /* ignore non-JSON banner lines */\n }\n };\n sub.stdout?.setEncoding('utf8');\n sub.stdout?.on('data', (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf('\\n')) >= 0) {\n flush(buffer.slice(0, idx));\n buffer = buffer.slice(idx + 1);\n }\n });\n\n const result = await sub;\n if (buffer) flush(buffer);\n\n if (signal.aborted)\n throw new LoopError({\n code: 'ABORTED',\n phase: 'engine',\n message: 'claude-cli run aborted',\n });\n if (result.failed) {\n // The child's stderr is outside our control and may echo credentials on\n // an auth failure — redact before it lands in events/logs/the summary.\n const stderr =\n typeof result.stderr === 'string'\n ? redactSecrets(result.stderr.slice(0, 400))\n : '';\n // A rate/usage limit can land on either stream; check both (redacted)\n // before falling through to the generic exit-code error.\n if (!result.timedOut) {\n const stdout =\n typeof result.stdout === 'string'\n ? redactSecrets(result.stdout.slice(0, 400))\n : '';\n const limit = classifyCliLimit(`${stderr}\\n${stdout}`);\n if (limit) throw limit;\n }\n throw new LoopError({\n code: result.timedOut ? 'TIMEOUT' : 'ENGINE',\n phase: 'engine',\n message: `claude exited ${result.exitCode ?? '?'}${stderr ? `: ${stderr}` : ''}`,\n });\n }\n\n onEvent({ type: 'usage', usage: acc.usage, model: acc.model });\n return {\n text: acc.text,\n usage: acc.usage,\n model: acc.model,\n stopReason: acc.stopReason,\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/engines/claude-cli.ts"],"names":[],"mappings":"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","file":"claude-cli-75AOQUKG.js","sourcesContent":["/**\n * Engine adapter: the `claude` CLI as a subprocess. A fresh process per call =\n * a fresh context. Robust spawning + abort + timeout via `execa`; output is the\n * same stream-json schema the Agent SDK emits, so we reuse `mapMessage`.\n */\n\nimport { execa } from 'execa';\nimport {\n SUBAGENT_TOOLS,\n type AgentRequest,\n type AgentResult,\n type Engine,\n type EngineEventSink,\n type EngineOptions,\n} from './engine.ts';\nimport { mapMessage, newAccumulator } from './message-map.ts';\nimport { LoopError } from '../core/errors.ts';\nimport { redactSecrets } from '../core/redact.ts';\n\n/**\n * Classify a failed `claude` subprocess into a provider-limit `LoopError`, or\n * return `undefined` to fall through to the generic ENGINE/TIMEOUT mapping. The\n * CLI has no structured limit channel on a hard failure, so we read its\n * (already-redacted) output text:\n * - a usage/quota limit (\"usage limit reached\", \"out of credits\") → QUOTA.\n * A reset time, when the message states one (epoch seconds or an absolute\n * time the CLI prints), makes it auto-waitable; otherwise QUOTA has no\n * reset and the loop policy checkpoints-and-pauses.\n * - a plain \"rate limit\" → RATE_LIMIT (resets on its own).\n * Order matters: usage/quota is checked first so a usage message that also\n * contains the words \"rate limit\" is not mis-tagged as a transient throttle.\n *\n * Exported for unit testing without spawning a subprocess (mirrors\n * `buildClaudeArgs`).\n */\nexport function classifyCliLimit(text: string): LoopError | undefined {\n const lower = text.toLowerCase();\n const isUsage =\n /usage limit|out of credits|insufficient credits|quota|billing/.test(lower);\n const isRate = /rate limit|rate-limit|too many requests|429/.test(lower);\n if (!isUsage && !isRate) return undefined;\n\n const resetAt = parseResetAt(text);\n if (isUsage) {\n return new LoopError({\n code: 'QUOTA',\n phase: 'engine',\n message: `claude usage limit: ${text}`,\n resetAt,\n });\n }\n return new LoopError({\n code: 'RATE_LIMIT',\n phase: 'engine',\n message: `claude rate limited: ${text}`,\n resetAt,\n });\n}\n\n/**\n * Pull a reset time (epoch ms) out of CLI limit text. The CLI states a reset as\n * an epoch-seconds value (e.g. `resets at 1700000000`); convert to ms. Returns\n * `undefined` when no reset is stated — a quota with no parseable reset is not\n * auto-waitable.\n */\nfunction parseResetAt(text: string): number | undefined {\n const m = /(?:reset|resets|retry|available)\\D{0,20}(\\d{10,13})/i.exec(text);\n if (!m) return undefined;\n const n = Number(m[1]);\n if (!Number.isFinite(n)) return undefined;\n // 10-digit values are epoch seconds; 13-digit are already ms.\n return m[1]!.length <= 10 ? n * 1000 : n;\n}\n\n/**\n * Build the `claude` argv for one run. Extracted (and exported) so the flag\n * wiring — model, system prompt, tool allowlist, permission mode, the `--`\n * argument-smuggling guard — is unit-testable without spawning a process.\n */\nexport function buildClaudeArgs(\n req: AgentRequest,\n opts: EngineOptions,\n): string[] {\n const model = req.model ?? opts.defaultModel;\n const args = ['-p', '--output-format', 'stream-json', '--verbose'];\n if (model) args.push('--model', model);\n if (req.system) args.push('--append-system-prompt', req.system);\n if (req.allowedTools?.length)\n args.push('--allowedTools', req.allowedTools.join(','));\n // A leaf agent may not spawn sub-agents — disallow the spawn tool (wins over any allowlist).\n if (req.leaf) args.push('--disallowedTools', SUBAGENT_TOOLS.join(','));\n if (opts.permissionMode) args.push('--permission-mode', opts.permissionMode);\n if (opts.cliArgs?.length) args.push(...opts.cliArgs);\n // `--` ends option parsing so a prompt starting with `-` can't be\n // mis-interpreted by `claude` as a flag (argument smuggling).\n args.push('--', req.prompt);\n return args;\n}\n\nexport class ClaudeCliEngine implements Engine {\n readonly name = 'claude-cli';\n constructor(private readonly opts: EngineOptions = {}) {}\n\n async run(\n req: AgentRequest,\n onEvent: EngineEventSink,\n signal: AbortSignal,\n ): Promise<AgentResult> {\n const bin = this.opts.cliBinary ?? 'claude';\n const model = req.model ?? this.opts.defaultModel;\n const args = buildClaudeArgs(req, this.opts);\n\n const acc = newAccumulator(model ?? 'claude-cli');\n // Buffered (default) so `stderr` is a string for error messages; we still\n // attach a `data` listener to stream stdout line-by-line as it arrives.\n const sub = execa(bin, args, {\n cwd: req.cwd,\n cancelSignal: signal,\n // The prompt is passed as an argument, not piped — don't let `claude -p`\n // stall waiting on stdin.\n stdin: 'ignore',\n // If the child ignores the SIGTERM from an abort/timeout, escalate to\n // SIGKILL so a wedged subprocess can't make Ctrl-C hang.\n forceKillAfterDelay: 5000,\n reject: false,\n timeout: req.timeoutMs,\n stripFinalNewline: false,\n });\n\n let buffer = '';\n const flush = (line: string) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n try {\n mapMessage(JSON.parse(trimmed), acc, onEvent);\n } catch {\n /* ignore non-JSON banner lines */\n }\n };\n sub.stdout?.setEncoding('utf8');\n sub.stdout?.on('data', (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf('\\n')) >= 0) {\n flush(buffer.slice(0, idx));\n buffer = buffer.slice(idx + 1);\n }\n });\n\n const result = await sub;\n if (buffer) flush(buffer);\n\n if (signal.aborted)\n throw new LoopError({\n code: 'ABORTED',\n phase: 'engine',\n message: 'claude-cli run aborted',\n });\n if (result.failed) {\n // The child's stderr is outside our control and may echo credentials on\n // an auth failure — redact before it lands in events/logs/the summary.\n const stderr =\n typeof result.stderr === 'string'\n ? redactSecrets(result.stderr.slice(0, 400))\n : '';\n // A rate/usage limit can land on either stream; check both (redacted)\n // before falling through to the generic exit-code error.\n if (!result.timedOut) {\n const stdout =\n typeof result.stdout === 'string'\n ? redactSecrets(result.stdout.slice(0, 400))\n : '';\n const limit = classifyCliLimit(`${stderr}\\n${stdout}`);\n if (limit) throw limit;\n }\n throw new LoopError({\n code: result.timedOut ? 'TIMEOUT' : 'ENGINE',\n phase: 'engine',\n message: `claude exited ${result.exitCode ?? '?'}${stderr ? `: ${stderr}` : ''}`,\n });\n }\n\n onEvent({ type: 'usage', usage: acc.usage, model: acc.model });\n return {\n text: acc.text,\n usage: acc.usage,\n model: acc.model,\n stopReason: acc.stopReason,\n };\n }\n}\n"]}
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
import { redactSecrets } from './chunk-JFTXJ7I2.js';
|
|
1
2
|
import { LoopError } from './chunk-I3STY7U6.js';
|
|
2
3
|
import { mkdtempSync, readFileSync, rmSync } from 'fs';
|
|
3
4
|
import { tmpdir } from 'os';
|
|
4
5
|
import { join } from 'path';
|
|
5
6
|
import { execa } from 'execa';
|
|
6
7
|
|
|
8
|
+
function buildCodexArgs(req, opts, outFile) {
|
|
9
|
+
const model = req.model ?? opts.defaultModel;
|
|
10
|
+
const prompt = req.system ? `${req.system}
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
${req.prompt}` : req.prompt;
|
|
15
|
+
const args = ["exec", "--ephemeral", "--skip-git-repo-check", "--color", "never"];
|
|
16
|
+
if (opts.permissionMode === "bypassPermissions") {
|
|
17
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
18
|
+
} else {
|
|
19
|
+
args.push("-s", "read-only");
|
|
20
|
+
}
|
|
21
|
+
if (req.cwd) args.push("-C", req.cwd);
|
|
22
|
+
if (model) args.push("-m", model);
|
|
23
|
+
if (opts.cliArgs?.length) args.push(...opts.cliArgs);
|
|
24
|
+
args.push("-o", outFile, prompt);
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
7
27
|
var CodexEngine = class {
|
|
8
28
|
constructor(opts = {}) {
|
|
9
29
|
this.opts = opts;
|
|
@@ -14,15 +34,7 @@ var CodexEngine = class {
|
|
|
14
34
|
const model = req.model ?? this.opts.defaultModel;
|
|
15
35
|
const dir = mkdtempSync(join(tmpdir(), "loops-codex-"));
|
|
16
36
|
const outFile = join(dir, "last.txt");
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
${req.prompt}` : req.prompt;
|
|
22
|
-
const args = ["exec", "--ephemeral", "-s", "read-only", "--skip-git-repo-check"];
|
|
23
|
-
if (req.cwd) args.push("-C", req.cwd);
|
|
24
|
-
if (model) args.push("-m", model);
|
|
25
|
-
args.push("-o", outFile, prompt);
|
|
37
|
+
const args = buildCodexArgs(req, this.opts, outFile);
|
|
26
38
|
try {
|
|
27
39
|
const sub = await execa(this.opts.cliBinary ?? "codex", args, {
|
|
28
40
|
stdin: "ignore",
|
|
@@ -43,7 +55,7 @@ ${req.prompt}` : req.prompt;
|
|
|
43
55
|
throw new LoopError({
|
|
44
56
|
code: sub.timedOut ? "TIMEOUT" : "ENGINE",
|
|
45
57
|
phase: "engine",
|
|
46
|
-
message: `codex exited ${sub.exitCode ?? "?"}${typeof sub.stderr === "string" ? `: ${sub.stderr.slice(0, 300)}` : ""}`
|
|
58
|
+
message: `codex exited ${sub.exitCode ?? "?"}${typeof sub.stderr === "string" ? `: ${redactSecrets(sub.stderr.slice(0, 300))}` : ""}`
|
|
47
59
|
});
|
|
48
60
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
49
61
|
if (text) onEvent({ type: "text", delta: text });
|
|
@@ -55,6 +67,6 @@ ${req.prompt}` : req.prompt;
|
|
|
55
67
|
}
|
|
56
68
|
};
|
|
57
69
|
|
|
58
|
-
export { CodexEngine };
|
|
59
|
-
//# sourceMappingURL=codex-
|
|
60
|
-
//# sourceMappingURL=codex-
|
|
70
|
+
export { CodexEngine, buildCodexArgs };
|
|
71
|
+
//# sourceMappingURL=codex-LYZF52WL.js.map
|
|
72
|
+
//# sourceMappingURL=codex-LYZF52WL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engines/codex.ts"],"names":[],"mappings":"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","file":"codex-LYZF52WL.js","sourcesContent":["/**\n * Engine adapter: the `codex` CLI (GPT-5) as a non-interactive subprocess. The\n * point is a genuinely DIFFERENT model behind the same `Engine` seam — point any\n * reviewer at `engine: 'codex'` for a second-model adversarial signal, with no\n * bespoke integration. Read-only by default: a report-only reviewer never edits,\n * so the sandbox forbids writes and the run cannot touch the workspace.\n *\n * `codex exec` is non-interactive but blocks on an open stdin, so stdin is always\n * ignored; the final assistant message is captured via `-o <file>` rather than\n * scraped from the event stream.\n */\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport type {\n AgentRequest,\n AgentResult,\n Engine,\n EngineEventSink,\n EngineOptions,\n} from './engine.ts';\nimport { LoopError } from '../core/errors.ts';\nimport { redactSecrets } from '../core/redact.ts';\n\nexport function buildCodexArgs(\n req: AgentRequest,\n opts: EngineOptions,\n outFile: string,\n): string[] {\n const model = req.model ?? opts.defaultModel;\n const prompt = req.system ? `${req.system}\\n\\n---\\n\\n${req.prompt}` : req.prompt;\n const args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never'];\n\n if (opts.permissionMode === 'bypassPermissions') {\n args.push('--dangerously-bypass-approvals-and-sandbox');\n } else {\n args.push('-s', 'read-only');\n }\n\n if (req.cwd) args.push('-C', req.cwd);\n if (model) args.push('-m', model);\n if (opts.cliArgs?.length) args.push(...opts.cliArgs);\n args.push('-o', outFile, prompt);\n return args;\n}\n\nexport class CodexEngine implements Engine {\n readonly name = 'codex';\n constructor(private readonly opts: EngineOptions = {}) {}\n\n async run(\n req: AgentRequest,\n onEvent: EngineEventSink,\n signal: AbortSignal,\n ): Promise<AgentResult> {\n const model = req.model ?? this.opts.defaultModel;\n const dir = mkdtempSync(join(tmpdir(), 'loops-codex-'));\n const outFile = join(dir, 'last.txt');\n const args = buildCodexArgs(req, this.opts, outFile);\n\n try {\n const sub = await execa(this.opts.cliBinary ?? 'codex', args, {\n stdin: 'ignore', // codex exec stalls on an open stdin\n cancelSignal: signal,\n forceKillAfterDelay: 5000,\n reject: false,\n timeout: req.timeoutMs,\n });\n if (signal.aborted)\n throw new LoopError({ code: 'ABORTED', phase: 'engine', message: 'codex run aborted' });\n\n let text = '';\n try {\n text = readFileSync(outFile, 'utf8').trim();\n } catch {\n /* no final message written */\n }\n if (!text && sub.failed)\n throw new LoopError({\n code: sub.timedOut ? 'TIMEOUT' : 'ENGINE',\n phase: 'engine',\n message: `codex exited ${sub.exitCode ?? '?'}${\n typeof sub.stderr === 'string' ? `: ${redactSecrets(sub.stderr.slice(0, 300))}` : ''\n }`,\n });\n\n // codex bills a separate (GPT-5) account, so its tokens are out-of-band for\n // the loops token budget — report zero rather than conflate providers.\n const usage = { inputTokens: 0, outputTokens: 0 };\n if (text) onEvent({ type: 'text', delta: text });\n onEvent({ type: 'usage', usage, model: model ?? 'codex' });\n return { text, usage, model: model ?? 'codex', stopReason: 'end_turn' };\n } finally {\n rmSync(dir, { recursive: true, force: true });\n }\n }\n}\n"]}
|
package/dist/env/command.d.ts
CHANGED
package/dist/env/docker.d.ts
CHANGED
package/dist/env/sst.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { renderPlan,
|
|
2
|
+
import { jobMeta, renderPlan, listRuns, runsHome, readRunStatus, runEventsPath, formatEvent, run, exitCodeFor, loop, runSemanticRecordsPath, agentJob, agentCheck, bodyPassed, gateJob } from './chunk-WM5QVHM2.js';
|
|
3
3
|
import './chunk-JFTXJ7I2.js';
|
|
4
|
-
import './chunk-
|
|
4
|
+
import './chunk-MA6NDQMO.js';
|
|
5
5
|
import './chunk-Y2SD7GBL.js';
|
|
6
6
|
import './chunk-I3STY7U6.js';
|
|
7
7
|
import fs from 'fs';
|
|
@@ -474,6 +474,7 @@ async function execute(file, flags) {
|
|
|
474
474
|
recordTo: flags.record,
|
|
475
475
|
checkpoint: flags.checkpoint,
|
|
476
476
|
resumeFrom: flags.resume,
|
|
477
|
+
supervise: flags.supervise,
|
|
477
478
|
onLimit,
|
|
478
479
|
maxWaitMs,
|
|
479
480
|
resumeCommand
|
|
@@ -540,6 +541,67 @@ Paused at a limit. Resume with:
|
|
|
540
541
|
);
|
|
541
542
|
}
|
|
542
543
|
}
|
|
544
|
+
function relAge(ms2) {
|
|
545
|
+
const s = Math.max(0, Math.round(ms2 / 1e3));
|
|
546
|
+
if (s < 60) return `${s}s`;
|
|
547
|
+
const m = Math.round(s / 60);
|
|
548
|
+
if (m < 60) return `${m}m`;
|
|
549
|
+
const h = Math.round(m / 60);
|
|
550
|
+
if (h < 48) return `${h}h`;
|
|
551
|
+
return `${Math.round(h / 24)}d`;
|
|
552
|
+
}
|
|
553
|
+
function readSemanticRecords(runId) {
|
|
554
|
+
const path2 = runSemanticRecordsPath(runId);
|
|
555
|
+
if (!fs.existsSync(path2)) return void 0;
|
|
556
|
+
const raw = fs.readFileSync(path2, "utf8").trim();
|
|
557
|
+
if (!raw) return [];
|
|
558
|
+
const records = [];
|
|
559
|
+
for (const line of raw.split("\n")) {
|
|
560
|
+
try {
|
|
561
|
+
records.push(JSON.parse(line));
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return records;
|
|
566
|
+
}
|
|
567
|
+
function parsePositiveIntFlag(value, flag) {
|
|
568
|
+
const parsed = Number(value);
|
|
569
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
570
|
+
throw new Error(`${flag} must be a positive integer, got "${value}"`);
|
|
571
|
+
}
|
|
572
|
+
return parsed;
|
|
573
|
+
}
|
|
574
|
+
function parseSinceFlag(value) {
|
|
575
|
+
const trimmed = value.trim();
|
|
576
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
577
|
+
const parsed = Date.parse(trimmed);
|
|
578
|
+
if (!Number.isFinite(parsed)) {
|
|
579
|
+
throw new Error(`--since must be epoch ms or an ISO timestamp, got "${value}"`);
|
|
580
|
+
}
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
function normalizeRecordPath(value) {
|
|
584
|
+
return value.split(/[\/›>]+/).map((part) => part.trim()).filter(Boolean).join("/");
|
|
585
|
+
}
|
|
586
|
+
function matchesRecordPath(record, prefix) {
|
|
587
|
+
const path2 = record.path.join("/");
|
|
588
|
+
return path2 === prefix || path2.startsWith(`${prefix}/`);
|
|
589
|
+
}
|
|
590
|
+
function formatSemanticRecord(record) {
|
|
591
|
+
const at = record.path.length ? `${record.path.join(" \u203A ")} ` : "";
|
|
592
|
+
switch (record.kind) {
|
|
593
|
+
case "dispatch":
|
|
594
|
+
return `${at}dispatch ${record.unit}${record.label ? ` ${record.label}` : ""}${record.node ? ` ${record.node}` : ""}`;
|
|
595
|
+
case "completion":
|
|
596
|
+
return `${at}completion ${record.unit}${record.label ? ` ${record.label}` : ""}: ${record.outcome.status}${record.outcome.summary ? ` \u2014 ${record.outcome.summary}` : ""}`;
|
|
597
|
+
case "surfacing":
|
|
598
|
+
return `${at}surfacing ${record.source} ${record.decision}${record.severity ? ` [${record.severity}]` : ""}: ${record.reason}`;
|
|
599
|
+
case "revision-emitted":
|
|
600
|
+
return `${at}revision emitted ${record.sourceEvent}${record.revision.target ? ` -> ${record.revision.target}` : ""}: ${record.revision.reason}`;
|
|
601
|
+
case "revision-routed":
|
|
602
|
+
return `${at}revision routed ${record.sourceEvent} ${record.decision}${record.revision.target ? ` -> ${record.revision.target}` : ""}: ${record.revision.reason}`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
543
605
|
async function main(argv = process.argv) {
|
|
544
606
|
const program = new Command();
|
|
545
607
|
program.name("loops").description(
|
|
@@ -553,7 +615,7 @@ async function main(argv = process.argv) {
|
|
|
553
615
|
"read the worker prompt from a file (no-file mode)"
|
|
554
616
|
).option(
|
|
555
617
|
"-e, --engine <name>",
|
|
556
|
-
"default engine: agent-sdk | claude-cli | anthropic-api"
|
|
618
|
+
"default engine: codex | agent-sdk | claude-cli | anthropic-api"
|
|
557
619
|
).option("--default-model <id>", "fallback model id for engines").option("--worker-model <id>", "model for the worker job").option(
|
|
558
620
|
"--validator-model <id>",
|
|
559
621
|
"small model for agent-validated conditions"
|
|
@@ -566,13 +628,13 @@ async function main(argv = process.argv) {
|
|
|
566
628
|
"0.85"
|
|
567
629
|
).option("-i, --interval <dur>", "delay between iterations (e.g. 30s, 5m)").option("--max-tokens <n>", "max output tokens per agent turn").option("--api-key <key>", "Anthropic API key (anthropic-api engine)").option(
|
|
568
630
|
"--cli-binary <path>",
|
|
569
|
-
"path to
|
|
631
|
+
"path to a CLI engine binary"
|
|
570
632
|
).option(
|
|
571
633
|
"--permission-mode <mode>",
|
|
572
|
-
"tool permission mode for
|
|
634
|
+
"tool permission mode for CLI/SDK engines (default | acceptEdits | bypassPermissions | plan | dontAsk | auto)"
|
|
573
635
|
).option(
|
|
574
636
|
"--engine-arg <arg>",
|
|
575
|
-
"extra arg forwarded to
|
|
637
|
+
"extra arg forwarded to CLI-backed engines (repeatable)",
|
|
576
638
|
(v, acc) => acc.concat(v),
|
|
577
639
|
[]
|
|
578
640
|
).option("--state <json>", "seed the shared run state (JSON)").option("--budget <tokens>", "cap total tokens (input+output) for the run").option("--record <path>", "append a JSONL run record to this path").option(
|
|
@@ -587,14 +649,25 @@ async function main(argv = process.argv) {
|
|
|
587
649
|
).option(
|
|
588
650
|
"--max-wait <dur>",
|
|
589
651
|
"cap an auto/wait limit-wait (e.g. 5m, 30s); default 5m"
|
|
590
|
-
).option("--json", "emit NDJSON events to stdout (no TUI)").option("--no-tui", "plain line output instead of the Ink TUI").
|
|
652
|
+
).option("--json", "emit NDJSON events to stdout (no TUI)").option("--no-tui", "plain line output instead of the Ink TUI").option(
|
|
653
|
+
"--supervise",
|
|
654
|
+
"register this run in ~/.loops/runs so `loops list`/`status`/`tail` can observe it from another process"
|
|
655
|
+
).action(
|
|
591
656
|
(file, flags) => execute(file, flags)
|
|
592
657
|
);
|
|
593
658
|
program.command("validate").argument("<file>", "a loop-definition file to check").description(
|
|
594
659
|
"load a .loop.ts and print its shape without running it: the cheap, no-model pre-flight an agent runs before `loops run`"
|
|
595
|
-
).action(async (file) => {
|
|
660
|
+
).option("--json", "emit JSON with the loaded job shape").action(async (file, flags) => {
|
|
596
661
|
const { job } = await loadJob(file);
|
|
597
|
-
const
|
|
662
|
+
const shape = jobMeta(job);
|
|
663
|
+
if (flags.json) {
|
|
664
|
+
process.stdout.write(
|
|
665
|
+
`${JSON.stringify({ file, ok: true, executed: false, shape }, null, 2)}
|
|
666
|
+
`
|
|
667
|
+
);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const plan = renderPlan(shape);
|
|
598
671
|
process.stdout.write(
|
|
599
672
|
`\u2713 ${file} loads (not executed)
|
|
600
673
|
${plan.map((l) => ` ${l}`).join("\n")}
|
|
@@ -603,10 +676,175 @@ ${plan.map((l) => ` ${l}`).join("\n")}
|
|
|
603
676
|
});
|
|
604
677
|
program.command("describe").argument("<file>", "a loop-definition file").description(
|
|
605
678
|
"print a loop's shape (its gate, body, and dag nodes) without running it"
|
|
606
|
-
).action(async (file) => {
|
|
679
|
+
).option("--json", "emit the job shape as JSON").action(async (file, flags) => {
|
|
607
680
|
const { job } = await loadJob(file);
|
|
608
|
-
|
|
681
|
+
const shape = jobMeta(job);
|
|
682
|
+
process.stdout.write(
|
|
683
|
+
flags.json ? `${JSON.stringify(shape, null, 2)}
|
|
684
|
+
` : `${renderPlan(shape).join("\n")}
|
|
685
|
+
`
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
program.command("list").alias("ls").description("list supervised runs (start one with `loops run --supervise`)").action(() => {
|
|
689
|
+
const runs = listRuns();
|
|
690
|
+
if (!runs.length) {
|
|
691
|
+
process.stdout.write(
|
|
692
|
+
`no supervised runs in ${runsHome()}
|
|
693
|
+
(start one with: loops run --supervise <file>)
|
|
694
|
+
`
|
|
695
|
+
);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const r of runs) {
|
|
699
|
+
const state = r.status === "running" ? r.alive ? "running" : "dead" : r.status;
|
|
700
|
+
const age = relAge(Date.now() - (r.endedAt ?? r.updatedAt));
|
|
701
|
+
process.stdout.write(
|
|
702
|
+
`${r.runId.padEnd(26)} ${state.padEnd(9)} iter ${String(r.live.iteration).padStart(3)} ${age.padStart(4)} ${r.title}
|
|
703
|
+
`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
program.command("status").argument("<runId>", "a run id from `loops list`").description("show a supervised run's live state and shape").action((runId) => {
|
|
708
|
+
const r = readRunStatus(runId);
|
|
709
|
+
if (!r) {
|
|
710
|
+
process.stderr.write(`no run "${runId}" in ${runsHome()}
|
|
609
711
|
`);
|
|
712
|
+
process.exitCode = 1;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const state = r.status === "running" ? r.alive ? "running" : "dead (process gone)" : r.status;
|
|
716
|
+
const g = r.live.lastGate;
|
|
717
|
+
const o = r.live.lastOutcome;
|
|
718
|
+
const lines = [
|
|
719
|
+
`${r.runId} [${state}]`,
|
|
720
|
+
` title: ${r.title}`,
|
|
721
|
+
` cwd: ${r.cwd}`,
|
|
722
|
+
` pid: ${r.pid}`,
|
|
723
|
+
r.live.iteration ? ` at: ${r.live.path.join(" \u203A ")} (iteration ${r.live.iteration})` : "",
|
|
724
|
+
g ? ` gate: ${g.which} ${g.met ? "met" : "not met"}${g.confidence != null ? ` @ ${g.confidence.toFixed(2)}` : ""}: ${g.reason}` : "",
|
|
725
|
+
o ? ` last: ${o.status}${o.summary ? `: ${o.summary}` : ""}` : "",
|
|
726
|
+
` tokens: ${r.live.usage.inputTokens} in / ${r.live.usage.outputTokens} out (${r.live.usage.calls} calls)`
|
|
727
|
+
].filter(Boolean);
|
|
728
|
+
process.stdout.write(`${lines.join("\n")}
|
|
729
|
+
`);
|
|
730
|
+
if (r.shape)
|
|
731
|
+
process.stdout.write(
|
|
732
|
+
`
|
|
733
|
+
shape:
|
|
734
|
+
${renderPlan(r.shape).map((l) => ` ${l}`).join("\n")}
|
|
735
|
+
`
|
|
736
|
+
);
|
|
737
|
+
});
|
|
738
|
+
program.command("tail").argument("<runId>", "a run id from `loops list`").description("stream a supervised run's events live (Ctrl-C to stop)").action(async (runId) => {
|
|
739
|
+
const path2 = runEventsPath(runId);
|
|
740
|
+
if (!fs.existsSync(path2)) {
|
|
741
|
+
process.stderr.write(`no run "${runId}" in ${runsHome()}
|
|
742
|
+
`);
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
let offset = 0;
|
|
747
|
+
let stop = false;
|
|
748
|
+
const onSig = () => {
|
|
749
|
+
stop = true;
|
|
750
|
+
};
|
|
751
|
+
process.once("SIGINT", onSig);
|
|
752
|
+
for (; ; ) {
|
|
753
|
+
const buf = fs.readFileSync(path2, "utf8");
|
|
754
|
+
if (buf.length > offset) {
|
|
755
|
+
const chunk = buf.slice(offset);
|
|
756
|
+
const lastNl = chunk.lastIndexOf("\n");
|
|
757
|
+
if (lastNl >= 0) {
|
|
758
|
+
offset += lastNl + 1;
|
|
759
|
+
for (const line of chunk.slice(0, lastNl).split("\n")) {
|
|
760
|
+
if (!line.trim()) continue;
|
|
761
|
+
try {
|
|
762
|
+
process.stdout.write(`${formatEvent(JSON.parse(line))}
|
|
763
|
+
`);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (stop) break;
|
|
770
|
+
const st = readRunStatus(runId);
|
|
771
|
+
if (st && st.status !== "running") {
|
|
772
|
+
process.stdout.write(`\u25C2 ${st.status}
|
|
773
|
+
`);
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
if (st && !st.alive) {
|
|
777
|
+
process.stdout.write("\u25C2 process gone (no terminal status)\n");
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
await new Promise((res) => setTimeout(res, 200));
|
|
781
|
+
}
|
|
782
|
+
process.removeListener("SIGINT", onSig);
|
|
783
|
+
});
|
|
784
|
+
program.command("records").argument("<runId>", "a run id from `loops list`").description("show a supervised run's semantic records").option(
|
|
785
|
+
"--kind <kind>",
|
|
786
|
+
"filter by record kind: dispatch | completion | surfacing | revision-emitted | revision-routed | revision"
|
|
787
|
+
).option("--path <path>", "filter by slash-separated record path prefix").option("--since <time>", "show records at or after an epoch ms or ISO timestamp").option("--last <n>", "show only the last n matching records").option("--json", "emit matching semantic records as JSONL").action((runId, flags) => {
|
|
788
|
+
const records = readSemanticRecords(runId);
|
|
789
|
+
if (!records) {
|
|
790
|
+
process.stderr.write(`no semantic records for run "${runId}" in ${runsHome()}
|
|
791
|
+
`);
|
|
792
|
+
process.exitCode = 1;
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const validKinds = [
|
|
796
|
+
"dispatch",
|
|
797
|
+
"completion",
|
|
798
|
+
"surfacing",
|
|
799
|
+
"revision-emitted",
|
|
800
|
+
"revision-routed",
|
|
801
|
+
"revision"
|
|
802
|
+
];
|
|
803
|
+
if (flags.kind && !validKinds.includes(flags.kind)) {
|
|
804
|
+
process.stderr.write(
|
|
805
|
+
`--kind must be one of ${validKinds.join(" | ")}, got "${flags.kind}"
|
|
806
|
+
`
|
|
807
|
+
);
|
|
808
|
+
process.exitCode = 1;
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
let pathPrefix;
|
|
812
|
+
if (flags.path != null) {
|
|
813
|
+
pathPrefix = normalizeRecordPath(flags.path);
|
|
814
|
+
if (!pathPrefix) {
|
|
815
|
+
process.stderr.write("--path must contain at least one path segment\n");
|
|
816
|
+
process.exitCode = 1;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
let since;
|
|
821
|
+
let last2;
|
|
822
|
+
try {
|
|
823
|
+
if (flags.since != null) since = parseSinceFlag(flags.since);
|
|
824
|
+
if (flags.last != null) last2 = parsePositiveIntFlag(flags.last, "--last");
|
|
825
|
+
} catch (e) {
|
|
826
|
+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}
|
|
827
|
+
`);
|
|
828
|
+
process.exitCode = 1;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
let filtered = flags.kind ? flags.kind === "revision" ? records.filter(
|
|
832
|
+
(r) => r.kind === "revision-emitted" || r.kind === "revision-routed"
|
|
833
|
+
) : records.filter((r) => r.kind === flags.kind) : records;
|
|
834
|
+
if (pathPrefix) filtered = filtered.filter((r) => matchesRecordPath(r, pathPrefix));
|
|
835
|
+
if (since != null) filtered = filtered.filter((r) => r.ts >= since);
|
|
836
|
+
if (last2 != null) filtered = filtered.slice(-last2);
|
|
837
|
+
if (flags.json) {
|
|
838
|
+
for (const record of filtered) {
|
|
839
|
+
process.stdout.write(`${JSON.stringify(record)}
|
|
840
|
+
`);
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
for (const record of filtered) {
|
|
845
|
+
process.stdout.write(`${formatSemanticRecord(record)}
|
|
846
|
+
`);
|
|
847
|
+
}
|
|
610
848
|
});
|
|
611
849
|
await program.parseAsync(argv);
|
|
612
850
|
}
|