@loops-adk/core 0.1.1 → 0.3.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.
@@ -5,5 +5,5 @@ function isEngine(ref) {
5
5
  }
6
6
 
7
7
  export { SUBAGENT_TOOLS, isEngine };
8
- //# sourceMappingURL=chunk-XC46B4FD.js.map
9
- //# sourceMappingURL=chunk-XC46B4FD.js.map
8
+ //# sourceMappingURL=chunk-MA6NDQMO.js.map
9
+ //# sourceMappingURL=chunk-MA6NDQMO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engines/engine.ts"],"names":[],"mappings":";AAyBO,IAAM,cAAA,GAAiB,CAAC,MAAM;AA6D9B,SAAS,SAAS,GAAA,EAA2C;AAClE,EAAA,OACE,OAAO,GAAA,KAAQ,QAAA,IACf,QAAQ,IAAA,IACR,OAAQ,IAAe,GAAA,KAAQ,UAAA;AAEnC","file":"chunk-MA6NDQMO.js","sourcesContent":["/**\n * The pluggable execution backend. A `Step` asks an `Engine` to run one agent\n * turn with a *fresh context* and stream events back. Each call is independent —\n * that is what gives every loop iteration its clean slate.\n */\n\n/**\n * Built-in, registry-resolvable adapter names. The union is open (`& {}` trick)\n * so callers can name and register their own engines — the core never assumes a\n * fixed provider set. (`mock` is constructed directly in tests/examples, not\n * registered by name, so it is intentionally not listed here.)\n */\nexport type EngineName =\n | 'agent-sdk'\n | 'claude-cli'\n | 'codex'\n | 'anthropic-api'\n | (string & {});\n\nexport interface Usage {\n inputTokens: number;\n outputTokens: number;\n}\n\n/** Tools an agent uses to spawn sub-agents / fan out. A `leaf` request disallows these. */\nexport const SUBAGENT_TOOLS = ['Task'];\n\nexport interface AgentRequest {\n prompt: string;\n system?: string;\n model?: string;\n maxTokens?: number;\n /** Tool allowlist, where the backend supports tools (SDK / CLI). */\n allowedTools?: string[];\n cwd?: string;\n timeoutMs?: number;\n /**\n * Forbid this agent from spawning sub-agents (fanning out). A leaf agent is told to\n * disallow the sub-agent tool (`SUBAGENT_TOOLS`), so a branch of the graph bottoms out\n * here instead of expanding into an uncontrolled swarm — control over where work stops.\n * Authoritative over `allowedTools` (a disallow wins). Engines with no sub-agent tool\n * (anthropic-api, codex, mock) ignore it.\n */\n leaf?: boolean;\n}\n\nexport interface AgentResult {\n /** Final assistant text (concatenated across blocks). */\n text: string;\n usage: Usage;\n model: string;\n stopReason?: string;\n /** Backend-native final payload, for escape-hatch inspection. */\n raw?: unknown;\n}\n\n/** Streamed during a run. The runtime re-tags these as `LoopEvent`s. */\nexport type EngineStreamEvent =\n | { type: 'text'; delta: string }\n | { type: 'thinking'; delta: string }\n | { type: 'tool'; name: string; phase: 'use' | 'result' }\n | { type: 'usage'; usage: Usage; model: string };\n\nexport type EngineEventSink = (event: EngineStreamEvent) => void;\n\nexport interface Engine {\n readonly name: EngineName;\n /**\n * Run one fresh agent turn. Contract for the `usage` stream event: emit it\n * **exactly once, at the end** of the turn — stats sums every `usage` event,\n * so a backend that emits incremental usage mid-stream would inflate totals.\n */\n run(\n req: AgentRequest,\n onEvent: EngineEventSink,\n signal: AbortSignal,\n ): Promise<AgentResult>;\n}\n\n/**\n * Anywhere an engine can be selected, accept either a registered name or a\n * ready-made `Engine`. The latter is the \"bring your own provider/framework\"\n * escape hatch — the runtime treats every backend through this one interface.\n */\nexport type EngineRef = EngineName | Engine;\n\nexport function isEngine(ref: EngineRef | undefined): ref is Engine {\n return (\n typeof ref === 'object' &&\n ref !== null &&\n typeof (ref as Engine).run === 'function'\n );\n}\n\n/**\n * How a tool-using engine treats permission prompts. Mirrors the Claude Code\n * values. `bypassPermissions` lets a headless worker read/write/run without\n * prompting — required for an unattended agent that must touch the filesystem or\n * shell, and to be set deliberately.\n */\nexport type PermissionMode =\n | 'default'\n | 'acceptEdits'\n | 'bypassPermissions'\n | 'plan'\n | 'dontAsk'\n | 'auto';\n\n/** Per-run options that the registry uses to construct engines. */\nexport interface EngineOptions {\n /** Default model when a request/step does not name one. */\n defaultModel?: string;\n apiKey?: string;\n /** For CLI-backed engines: path to the binary. */\n cliBinary?: string;\n /** Extra args appended to CLI-backed engine invocations. */\n cliArgs?: string[];\n /**\n * Permission mode for tool-using engines. Unset = the engine/CLI default\n * where applicable; the Codex adapter stays read-only unless explicitly set\n * to `bypassPermissions`.\n */\n permissionMode?: PermissionMode;\n}\n"]}
@@ -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-XC46B4FD.js';
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-U7WEVAOL.js.map
124
- //# sourceMappingURL=claude-cli-U7WEVAOL.js.map
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 prompt = req.system ? `${req.system}
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-6I5UZ2HM.js.map
60
- //# sourceMappingURL=codex-6I5UZ2HM.js.map
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":";;;;;;;AAyBO,SAAS,cAAA,CACd,GAAA,EACA,IAAA,EACA,OAAA,EACU;AACV,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,IAAS,IAAA,CAAK,YAAA;AAChC,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,MAAA,GAAS,CAAA,EAAG,IAAI,MAAM;;AAAA;;AAAA,EAAc,GAAA,CAAI,MAAM,CAAA,CAAA,GAAK,GAAA,CAAI,MAAA;AAC1E,EAAA,MAAM,OAAO,CAAC,MAAA,EAAQ,aAAA,EAAe,uBAAA,EAAyB,WAAW,OAAO,CAAA;AAEhF,EAAA,IAAI,IAAA,CAAK,mBAAmB,mBAAA,EAAqB;AAC/C,IAAA,IAAA,CAAK,KAAK,4CAA4C,CAAA;AAAA,EACxD,CAAA,MAAO;AACL,IAAA,IAAA,CAAK,IAAA,CAAK,MAAM,WAAW,CAAA;AAAA,EAC7B;AAEA,EAAA,IAAI,IAAI,GAAA,EAAK,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,IAAI,GAAG,CAAA;AACpC,EAAA,IAAI,KAAA,EAAO,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,KAAK,CAAA;AAChC,EAAA,IAAI,KAAK,OAAA,EAAS,MAAA,OAAa,IAAA,CAAK,GAAG,KAAK,OAAO,CAAA;AACnD,EAAA,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,OAAA,EAAS,MAAM,CAAA;AAC/B,EAAA,OAAO,IAAA;AACT;AAEO,IAAM,cAAN,MAAoC;AAAA,EAEzC,WAAA,CAA6B,IAAA,GAAsB,EAAC,EAAG;AAA1B,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAA2B;AAAA,EAA3B,IAAA;AAAA,EADpB,IAAA,GAAO,OAAA;AAAA,EAGhB,MAAM,GAAA,CACJ,GAAA,EACA,OAAA,EACA,MAAA,EACsB;AACtB,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,IAAS,IAAA,CAAK,IAAA,CAAK,YAAA;AACrC,IAAA,MAAM,MAAM,WAAA,CAAY,IAAA,CAAK,MAAA,EAAO,EAAG,cAAc,CAAC,CAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,EAAK,UAAU,CAAA;AACpC,IAAA,MAAM,IAAA,GAAO,cAAA,CAAe,GAAA,EAAK,IAAA,CAAK,MAAM,OAAO,CAAA;AAEnD,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,MAAM,KAAA,CAAM,KAAK,IAAA,CAAK,SAAA,IAAa,SAAS,IAAA,EAAM;AAAA,QAC5D,KAAA,EAAO,QAAA;AAAA;AAAA,QACP,YAAA,EAAc,MAAA;AAAA,QACd,mBAAA,EAAqB,GAAA;AAAA,QACrB,MAAA,EAAQ,KAAA;AAAA,QACR,SAAS,GAAA,CAAI;AAAA,OACd,CAAA;AACD,MAAA,IAAI,MAAA,CAAO,OAAA;AACT,QAAA,MAAM,IAAI,UAAU,EAAE,IAAA,EAAM,WAAW,KAAA,EAAO,QAAA,EAAU,OAAA,EAAS,mBAAA,EAAqB,CAAA;AAExF,MAAA,IAAI,IAAA,GAAO,EAAA;AACX,MAAA,IAAI;AACF,QAAA,IAAA,GAAO,YAAA,CAAa,OAAA,EAAS,MAAM,CAAA,CAAE,IAAA,EAAK;AAAA,MAC5C,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,IAAI,CAAC,QAAQ,GAAA,CAAI,MAAA;AACf,QAAA,MAAM,IAAI,SAAA,CAAU;AAAA,UAClB,IAAA,EAAM,GAAA,CAAI,QAAA,GAAW,SAAA,GAAY,QAAA;AAAA,UACjC,KAAA,EAAO,QAAA;AAAA,UACP,OAAA,EAAS,gBAAgB,GAAA,CAAI,QAAA,IAAY,GAAG,CAAA,EAC1C,OAAO,IAAI,MAAA,KAAW,QAAA,GAAW,KAAK,aAAA,CAAc,GAAA,CAAI,OAAO,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAC,KAAK,EACpF,CAAA;AAAA,SACD,CAAA;AAIH,MAAA,MAAM,KAAA,GAAQ,EAAE,WAAA,EAAa,CAAA,EAAG,cAAc,CAAA,EAAE;AAChD,MAAA,IAAI,MAAM,OAAA,CAAQ,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,MAAM,CAAA;AAC/C,MAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,KAAA,EAAO,KAAA,IAAS,SAAS,CAAA;AACzD,MAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,OAAO,KAAA,IAAS,OAAA,EAAS,YAAY,UAAA,EAAW;AAAA,IACxE,CAAA,SAAE;AACA,MAAA,MAAA,CAAO,KAAK,EAAE,SAAA,EAAW,IAAA,EAAM,KAAA,EAAO,MAAM,CAAA;AAAA,IAC9C;AAAA,EACF;AACF","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"]}
@@ -1,4 +1,4 @@
1
- import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
1
+ import { W as Workspace, E as Environment } from '../types-CpB03Jj4.js';
2
2
 
3
3
  /**
4
4
  * `commandEnvironment` — a generic, CLI-driven Environment. Every IaC tool
@@ -1,4 +1,4 @@
1
- import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
1
+ import { W as Workspace, E as Environment } from '../types-CpB03Jj4.js';
2
2
 
3
3
  /**
4
4
  * `dockerEnvironment` — a local environment via Docker Compose, a thin preset
package/dist/env/sst.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
1
+ import { W as Workspace, E as Environment } from '../types-CpB03Jj4.js';
2
2
  import { Cmd } from './command.js';
3
3
 
4
4
  /**
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { renderPlan, jobMeta, listRuns, runsHome, readRunStatus, runEventsPath, formatEvent, run, exitCodeFor, loop, agentJob, agentCheck, bodyPassed, gateJob } from './chunk-6BDWTFOS.js';
2
+ import { jobMeta, renderPlan, listRuns, runsHome, readRunStatus, runEventsPath, formatEvent, run, exitCodeFor, loop, runSemanticRecordsPath, agentJob, agentCheck, bodyPassed, gateJob } from './chunk-3PMVII43.js';
3
3
  import './chunk-JFTXJ7I2.js';
4
- import './chunk-XC46B4FD.js';
4
+ import './chunk-MA6NDQMO.js';
5
5
  import './chunk-Y2SD7GBL.js';
6
6
  import './chunk-I3STY7U6.js';
7
7
  import fs from 'fs';
@@ -190,6 +190,12 @@ function plainReporter() {
190
190
  `${indent(event.path)} ${pc.dim(`tool ${event.phase}: ${event.name}`)}`
191
191
  );
192
192
  return;
193
+ case "loop:stall":
194
+ endStream();
195
+ console.log(
196
+ `${indent(event.path)} ${pc.red("\u23F9 stalled")}: no progress across iterations ${event.report.iterations.join(", ")} ${pc.dim(`(${event.report.reason})`)}`
197
+ );
198
+ return;
193
199
  case "limit:wait":
194
200
  endStream();
195
201
  console.log(
@@ -288,7 +294,8 @@ var FlagSpec = z.object({
288
294
  review: z.string().optional(),
289
295
  reviewThreshold: z.number().min(0).max(1).default(0.85),
290
296
  interval: z.number().int().nonnegative().optional(),
291
- maxTokens: z.number().int().positive().optional()
297
+ maxTokens: z.number().int().positive().optional(),
298
+ stallAfter: z.number().int().positive().optional()
292
299
  });
293
300
  function parseDuration(value) {
294
301
  if (/^\d+$/.test(value)) return Number(value);
@@ -340,6 +347,7 @@ Your previous attempt was REJECTED in review: ${ctx.lastReview.summary ?? ctx.la
340
347
  until,
341
348
  review,
342
349
  max: spec.max,
350
+ noProgress: spec.stallAfter,
343
351
  delayMs: spec.interval
344
352
  });
345
353
  }
@@ -403,7 +411,8 @@ function buildFromFlags(flags) {
403
411
  review: flags.review,
404
412
  reviewThreshold: num(flags.reviewThreshold),
405
413
  interval: flags.interval != null ? parseDuration(flags.interval) : void 0,
406
- maxTokens: num(flags.maxTokens)
414
+ maxTokens: num(flags.maxTokens),
415
+ stallAfter: num(flags.stallAfter)
407
416
  });
408
417
  } catch (e) {
409
418
  if (e instanceof z.ZodError) {
@@ -550,6 +559,58 @@ function relAge(ms2) {
550
559
  if (h < 48) return `${h}h`;
551
560
  return `${Math.round(h / 24)}d`;
552
561
  }
562
+ function readSemanticRecords(runId) {
563
+ const path2 = runSemanticRecordsPath(runId);
564
+ if (!fs.existsSync(path2)) return void 0;
565
+ const raw = fs.readFileSync(path2, "utf8").trim();
566
+ if (!raw) return [];
567
+ const records = [];
568
+ for (const line of raw.split("\n")) {
569
+ try {
570
+ records.push(JSON.parse(line));
571
+ } catch {
572
+ }
573
+ }
574
+ return records;
575
+ }
576
+ function parsePositiveIntFlag(value, flag) {
577
+ const parsed = Number(value);
578
+ if (!Number.isInteger(parsed) || parsed <= 0) {
579
+ throw new Error(`${flag} must be a positive integer, got "${value}"`);
580
+ }
581
+ return parsed;
582
+ }
583
+ function parseSinceFlag(value) {
584
+ const trimmed = value.trim();
585
+ if (/^\d+$/.test(trimmed)) return Number(trimmed);
586
+ const parsed = Date.parse(trimmed);
587
+ if (!Number.isFinite(parsed)) {
588
+ throw new Error(`--since must be epoch ms or an ISO timestamp, got "${value}"`);
589
+ }
590
+ return parsed;
591
+ }
592
+ function normalizeRecordPath(value) {
593
+ return value.split(/[\/›>]+/).map((part) => part.trim()).filter(Boolean).join("/");
594
+ }
595
+ function matchesRecordPath(record, prefix) {
596
+ const path2 = record.path.join("/");
597
+ return path2 === prefix || path2.startsWith(`${prefix}/`);
598
+ }
599
+ function formatSemanticRecord(record) {
600
+ const at = record.path.length ? `${record.path.join(" \u203A ")} ` : "";
601
+ switch (record.kind) {
602
+ case "dispatch":
603
+ return `${at}dispatch ${record.unit}${record.label ? ` ${record.label}` : ""}${record.node ? ` ${record.node}` : ""}`;
604
+ case "completion":
605
+ return `${at}completion ${record.unit}${record.label ? ` ${record.label}` : ""}: ${record.outcome.status}${record.outcome.summary ? ` \u2014 ${record.outcome.summary}` : ""}`;
606
+ case "surfacing":
607
+ return `${at}surfacing ${record.source} ${record.decision}${record.severity ? ` [${record.severity}]` : ""}: ${record.reason}`;
608
+ case "revision-emitted":
609
+ return `${at}revision emitted ${record.sourceEvent}${record.revision.target ? ` -> ${record.revision.target}` : ""}: ${record.revision.reason}`;
610
+ case "revision-routed":
611
+ return `${at}revision routed ${record.sourceEvent} ${record.decision}${record.revision.target ? ` -> ${record.revision.target}` : ""}: ${record.revision.reason}`;
612
+ }
613
+ }
553
614
  async function main(argv = process.argv) {
554
615
  const program = new Command();
555
616
  program.name("loops").description(
@@ -563,7 +624,7 @@ async function main(argv = process.argv) {
563
624
  "read the worker prompt from a file (no-file mode)"
564
625
  ).option(
565
626
  "-e, --engine <name>",
566
- "default engine: agent-sdk | claude-cli | anthropic-api"
627
+ "default engine: codex | agent-sdk | claude-cli | anthropic-api"
567
628
  ).option("--default-model <id>", "fallback model id for engines").option("--worker-model <id>", "model for the worker job").option(
568
629
  "--validator-model <id>",
569
630
  "small model for agent-validated conditions"
@@ -574,15 +635,18 @@ async function main(argv = process.argv) {
574
635
  "--review-threshold <0..1>",
575
636
  "confidence threshold for --review",
576
637
  "0.85"
577
- ).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(
638
+ ).option("-i, --interval <dur>", "delay between iterations (e.g. 30s, 5m)").option("--max-tokens <n>", "max output tokens per agent turn").option(
639
+ "--stall-after <n>",
640
+ "end exhausted after n consecutive iterations with no observable progress"
641
+ ).option("--api-key <key>", "Anthropic API key (anthropic-api engine)").option(
578
642
  "--cli-binary <path>",
579
- "path to the claude binary (claude-cli engine)"
643
+ "path to a CLI engine binary"
580
644
  ).option(
581
645
  "--permission-mode <mode>",
582
- "tool permission mode for claude-cli/agent-sdk (default | acceptEdits | bypassPermissions | plan | dontAsk | auto)"
646
+ "tool permission mode for CLI/SDK engines (default | acceptEdits | bypassPermissions | plan | dontAsk | auto)"
583
647
  ).option(
584
648
  "--engine-arg <arg>",
585
- "extra arg forwarded to the claude-cli engine (repeatable)",
649
+ "extra arg forwarded to CLI-backed engines (repeatable)",
586
650
  (v, acc) => acc.concat(v),
587
651
  []
588
652
  ).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(
@@ -605,9 +669,17 @@ async function main(argv = process.argv) {
605
669
  );
606
670
  program.command("validate").argument("<file>", "a loop-definition file to check").description(
607
671
  "load a .loop.ts and print its shape without running it: the cheap, no-model pre-flight an agent runs before `loops run`"
608
- ).action(async (file) => {
672
+ ).option("--json", "emit JSON with the loaded job shape").action(async (file, flags) => {
609
673
  const { job } = await loadJob(file);
610
- const plan = renderPlan(jobMeta(job));
674
+ const shape = jobMeta(job);
675
+ if (flags.json) {
676
+ process.stdout.write(
677
+ `${JSON.stringify({ file, ok: true, executed: false, shape }, null, 2)}
678
+ `
679
+ );
680
+ return;
681
+ }
682
+ const plan = renderPlan(shape);
611
683
  process.stdout.write(
612
684
  `\u2713 ${file} loads (not executed)
613
685
  ${plan.map((l) => ` ${l}`).join("\n")}
@@ -616,10 +688,14 @@ ${plan.map((l) => ` ${l}`).join("\n")}
616
688
  });
617
689
  program.command("describe").argument("<file>", "a loop-definition file").description(
618
690
  "print a loop's shape (its gate, body, and dag nodes) without running it"
619
- ).action(async (file) => {
691
+ ).option("--json", "emit the job shape as JSON").action(async (file, flags) => {
620
692
  const { job } = await loadJob(file);
621
- process.stdout.write(`${renderPlan(jobMeta(job)).join("\n")}
622
- `);
693
+ const shape = jobMeta(job);
694
+ process.stdout.write(
695
+ flags.json ? `${JSON.stringify(shape, null, 2)}
696
+ ` : `${renderPlan(shape).join("\n")}
697
+ `
698
+ );
623
699
  });
624
700
  program.command("list").alias("ls").description("list supervised runs (start one with `loops run --supervise`)").action(() => {
625
701
  const runs = listRuns();
@@ -717,6 +793,71 @@ ${renderPlan(r.shape).map((l) => ` ${l}`).join("\n")}
717
793
  }
718
794
  process.removeListener("SIGINT", onSig);
719
795
  });
796
+ program.command("records").argument("<runId>", "a run id from `loops list`").description("show a supervised run's semantic records").option(
797
+ "--kind <kind>",
798
+ "filter by record kind: dispatch | completion | surfacing | revision-emitted | revision-routed | revision"
799
+ ).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) => {
800
+ const records = readSemanticRecords(runId);
801
+ if (!records) {
802
+ process.stderr.write(`no semantic records for run "${runId}" in ${runsHome()}
803
+ `);
804
+ process.exitCode = 1;
805
+ return;
806
+ }
807
+ const validKinds = [
808
+ "dispatch",
809
+ "completion",
810
+ "surfacing",
811
+ "revision-emitted",
812
+ "revision-routed",
813
+ "revision"
814
+ ];
815
+ if (flags.kind && !validKinds.includes(flags.kind)) {
816
+ process.stderr.write(
817
+ `--kind must be one of ${validKinds.join(" | ")}, got "${flags.kind}"
818
+ `
819
+ );
820
+ process.exitCode = 1;
821
+ return;
822
+ }
823
+ let pathPrefix;
824
+ if (flags.path != null) {
825
+ pathPrefix = normalizeRecordPath(flags.path);
826
+ if (!pathPrefix) {
827
+ process.stderr.write("--path must contain at least one path segment\n");
828
+ process.exitCode = 1;
829
+ return;
830
+ }
831
+ }
832
+ let since;
833
+ let last2;
834
+ try {
835
+ if (flags.since != null) since = parseSinceFlag(flags.since);
836
+ if (flags.last != null) last2 = parsePositiveIntFlag(flags.last, "--last");
837
+ } catch (e) {
838
+ process.stderr.write(`${e instanceof Error ? e.message : String(e)}
839
+ `);
840
+ process.exitCode = 1;
841
+ return;
842
+ }
843
+ let filtered = flags.kind ? flags.kind === "revision" ? records.filter(
844
+ (r) => r.kind === "revision-emitted" || r.kind === "revision-routed"
845
+ ) : records.filter((r) => r.kind === flags.kind) : records;
846
+ if (pathPrefix) filtered = filtered.filter((r) => matchesRecordPath(r, pathPrefix));
847
+ if (since != null) filtered = filtered.filter((r) => r.ts >= since);
848
+ if (last2 != null) filtered = filtered.slice(-last2);
849
+ if (flags.json) {
850
+ for (const record of filtered) {
851
+ process.stdout.write(`${JSON.stringify(record)}
852
+ `);
853
+ }
854
+ return;
855
+ }
856
+ for (const record of filtered) {
857
+ process.stdout.write(`${formatSemanticRecord(record)}
858
+ `);
859
+ }
860
+ });
720
861
  await program.parseAsync(argv);
721
862
  }
722
863