@loops-adk/core 0.1.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/LICENSE +21 -0
- package/README.md +486 -0
- package/bin/loops.mjs +16 -0
- package/dist/App-3YQS6DXA.js +461 -0
- package/dist/App-3YQS6DXA.js.map +1 -0
- package/dist/agent-sdk-RF5VJZAT.js +95 -0
- package/dist/agent-sdk-RF5VJZAT.js.map +1 -0
- package/dist/anthropic-api-XJY6Y4T2.js +131 -0
- package/dist/anthropic-api-XJY6Y4T2.js.map +1 -0
- package/dist/api.d.ts +949 -0
- package/dist/api.js +898 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-33YIGWNU.js +63 -0
- package/dist/chunk-33YIGWNU.js.map +1 -0
- package/dist/chunk-3BPU34DE.js +2163 -0
- package/dist/chunk-3BPU34DE.js.map +1 -0
- package/dist/chunk-CXEPZHSR.js +86 -0
- package/dist/chunk-CXEPZHSR.js.map +1 -0
- package/dist/chunk-I3STY7U6.js +61 -0
- package/dist/chunk-I3STY7U6.js.map +1 -0
- package/dist/chunk-JFTXJ7I2.js +18 -0
- package/dist/chunk-JFTXJ7I2.js.map +1 -0
- package/dist/chunk-XC46B4FD.js +9 -0
- package/dist/chunk-XC46B4FD.js.map +1 -0
- package/dist/chunk-Y2SD7GBL.js +30 -0
- package/dist/chunk-Y2SD7GBL.js.map +1 -0
- package/dist/claude-cli-U7WEVAOL.js +124 -0
- package/dist/claude-cli-U7WEVAOL.js.map +1 -0
- package/dist/codex-6I5UZ2HM.js +60 -0
- package/dist/codex-6I5UZ2HM.js.map +1 -0
- package/dist/env/command.d.ts +53 -0
- package/dist/env/command.js +3 -0
- package/dist/env/command.js.map +1 -0
- package/dist/env/docker.d.ts +38 -0
- package/dist/env/docker.js +33 -0
- package/dist/env/docker.js.map +1 -0
- package/dist/env/sst.d.ts +39 -0
- package/dist/env/sst.js +20 -0
- package/dist/env/sst.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +620 -0
- package/dist/index.js.map +1 -0
- package/dist/types-B4wGVpqo.d.ts +898 -0
- package/package.json +100 -0
- package/skills/author-loop/SKILL.md +121 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/engines/message-map.ts
|
|
2
|
+
function newAccumulator(model) {
|
|
3
|
+
return {
|
|
4
|
+
text: "",
|
|
5
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
6
|
+
model,
|
|
7
|
+
sawDelta: false
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function asArray(value) {
|
|
11
|
+
return Array.isArray(value) ? value : [];
|
|
12
|
+
}
|
|
13
|
+
function mapMessage(message, acc, onEvent) {
|
|
14
|
+
const msg = message ?? {};
|
|
15
|
+
switch (msg.type) {
|
|
16
|
+
case "assistant": {
|
|
17
|
+
const inner = msg.message ?? {};
|
|
18
|
+
if (typeof inner.model === "string") acc.model = inner.model;
|
|
19
|
+
if (typeof inner.stop_reason === "string")
|
|
20
|
+
acc.stopReason = inner.stop_reason;
|
|
21
|
+
for (const block of asArray(inner.content)) {
|
|
22
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
23
|
+
acc.text += block.text;
|
|
24
|
+
if (!acc.sawDelta) onEvent({ type: "text", delta: block.text });
|
|
25
|
+
} else if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
26
|
+
if (!acc.sawDelta)
|
|
27
|
+
onEvent({ type: "thinking", delta: block.thinking });
|
|
28
|
+
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
29
|
+
onEvent({ type: "tool", name: block.name, phase: "use" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const usage = inner.usage;
|
|
33
|
+
if (usage) {
|
|
34
|
+
acc.usage.inputTokens += num(usage.input_tokens);
|
|
35
|
+
acc.usage.outputTokens += num(usage.output_tokens);
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "user": {
|
|
40
|
+
const inner = msg.message ?? {};
|
|
41
|
+
for (const block of asArray(inner.content)) {
|
|
42
|
+
if (block.type === "tool_result") {
|
|
43
|
+
onEvent({
|
|
44
|
+
type: "tool",
|
|
45
|
+
name: typeof block.name === "string" ? block.name : "tool",
|
|
46
|
+
phase: "result"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "stream_event": {
|
|
53
|
+
const event = msg.event ?? {};
|
|
54
|
+
if (event.type === "content_block_delta") {
|
|
55
|
+
const delta = event.delta ?? {};
|
|
56
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
57
|
+
acc.sawDelta = true;
|
|
58
|
+
onEvent({ type: "text", delta: delta.text });
|
|
59
|
+
} else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
60
|
+
acc.sawDelta = true;
|
|
61
|
+
onEvent({ type: "thinking", delta: delta.thinking });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "result": {
|
|
67
|
+
if (typeof msg.stop_reason === "string") acc.stopReason = msg.stop_reason;
|
|
68
|
+
const usage = msg.usage;
|
|
69
|
+
if (usage) {
|
|
70
|
+
const i = num(usage.input_tokens);
|
|
71
|
+
const o = num(usage.output_tokens);
|
|
72
|
+
if (i) acc.usage.inputTokens = i;
|
|
73
|
+
if (o) acc.usage.outputTokens = o;
|
|
74
|
+
}
|
|
75
|
+
if (!acc.text && typeof msg.result === "string") acc.text = msg.result;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function num(value) {
|
|
81
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { mapMessage, newAccumulator };
|
|
85
|
+
//# sourceMappingURL=chunk-CXEPZHSR.js.map
|
|
86
|
+
//# sourceMappingURL=chunk-CXEPZHSR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engines/message-map.ts"],"names":[],"mappings":"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","file":"chunk-CXEPZHSR.js","sourcesContent":["/**\n * Shared mapping from the Claude stream-json message schema to our neutral\n * `EngineStreamEvent`s. Both the Agent SDK and the `claude` CLI emit this same\n * schema, so the two adapters share this one function and stay tiny.\n *\n * The boundary with an external schema is the right place for a little `any`:\n * we read defensively so a minor upstream shape change doesn't crash a run.\n */\n\nimport type { EngineEventSink, Usage } from './engine.ts';\n\nexport interface Accumulator {\n text: string;\n usage: Usage;\n model: string;\n stopReason?: string;\n /** Set once we have seen token deltas, so we don't double-emit full blocks. */\n sawDelta: boolean;\n}\n\nexport function newAccumulator(model: string): Accumulator {\n return {\n text: '',\n usage: { inputTokens: 0, outputTokens: 0 },\n model,\n sawDelta: false,\n };\n}\n\ntype AnyRecord = Record<string, unknown>;\n\nfunction asArray(value: unknown): AnyRecord[] {\n return Array.isArray(value) ? (value as AnyRecord[]) : [];\n}\n\nexport function mapMessage(\n message: unknown,\n acc: Accumulator,\n onEvent: EngineEventSink,\n): void {\n const msg = (message ?? {}) as AnyRecord;\n switch (msg.type) {\n case 'assistant': {\n const inner = (msg.message ?? {}) as AnyRecord;\n if (typeof inner.model === 'string') acc.model = inner.model;\n if (typeof inner.stop_reason === 'string')\n acc.stopReason = inner.stop_reason;\n for (const block of asArray(inner.content)) {\n if (block.type === 'text' && typeof block.text === 'string') {\n acc.text += block.text;\n if (!acc.sawDelta) onEvent({ type: 'text', delta: block.text });\n } else if (\n block.type === 'thinking' &&\n typeof block.thinking === 'string'\n ) {\n if (!acc.sawDelta)\n onEvent({ type: 'thinking', delta: block.thinking });\n } else if (\n block.type === 'tool_use' &&\n typeof block.name === 'string'\n ) {\n onEvent({ type: 'tool', name: block.name, phase: 'use' });\n }\n }\n const usage = inner.usage as AnyRecord | undefined;\n if (usage) {\n acc.usage.inputTokens += num(usage.input_tokens);\n acc.usage.outputTokens += num(usage.output_tokens);\n }\n break;\n }\n case 'user': {\n const inner = (msg.message ?? {}) as AnyRecord;\n for (const block of asArray(inner.content)) {\n if (block.type === 'tool_result') {\n onEvent({\n type: 'tool',\n name: typeof block.name === 'string' ? block.name : 'tool',\n phase: 'result',\n });\n }\n }\n break;\n }\n case 'stream_event': {\n const event = (msg.event ?? {}) as AnyRecord;\n if (event.type === 'content_block_delta') {\n const delta = (event.delta ?? {}) as AnyRecord;\n if (delta.type === 'text_delta' && typeof delta.text === 'string') {\n acc.sawDelta = true;\n onEvent({ type: 'text', delta: delta.text });\n } else if (\n delta.type === 'thinking_delta' &&\n typeof delta.thinking === 'string'\n ) {\n acc.sawDelta = true;\n onEvent({ type: 'thinking', delta: delta.thinking });\n }\n }\n break;\n }\n case 'result': {\n // `subtype` is the result *classification* (success / error_max_turns …),\n // not the model stop reason — that is the sibling `stop_reason` field.\n if (typeof msg.stop_reason === 'string') acc.stopReason = msg.stop_reason;\n const usage = msg.usage as AnyRecord | undefined;\n if (usage) {\n // result usage is authoritative for the turn\n const i = num(usage.input_tokens);\n const o = num(usage.output_tokens);\n if (i) acc.usage.inputTokens = i;\n if (o) acc.usage.outputTokens = o;\n }\n if (!acc.text && typeof msg.result === 'string') acc.text = msg.result;\n break;\n }\n }\n}\n\nfunction num(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var LoopError = class _LoopError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
phase;
|
|
5
|
+
path;
|
|
6
|
+
iteration;
|
|
7
|
+
retryable;
|
|
8
|
+
/** Suggested wait before retry, in ms (e.g. a `retry-after` header). */
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
/** When the limit resets, as epoch ms. The wait policy prefers this. */
|
|
11
|
+
resetAt;
|
|
12
|
+
constructor(init) {
|
|
13
|
+
super(
|
|
14
|
+
init.message,
|
|
15
|
+
init.cause !== void 0 ? { cause: init.cause } : void 0
|
|
16
|
+
);
|
|
17
|
+
this.name = "LoopError";
|
|
18
|
+
this.code = init.code;
|
|
19
|
+
this.phase = init.phase;
|
|
20
|
+
this.path = init.path;
|
|
21
|
+
this.iteration = init.iteration;
|
|
22
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
23
|
+
this.resetAt = init.resetAt;
|
|
24
|
+
this.retryable = init.retryable ?? defaultRetryable(init);
|
|
25
|
+
}
|
|
26
|
+
/** Wrap an arbitrary thrown value, preserving a `LoopError` as-is. */
|
|
27
|
+
static from(value, fallback) {
|
|
28
|
+
if (value instanceof _LoopError) return value;
|
|
29
|
+
const message = value instanceof Error ? value.message : String(value);
|
|
30
|
+
return new _LoopError({ ...fallback, message, cause: value });
|
|
31
|
+
}
|
|
32
|
+
toJSON() {
|
|
33
|
+
return {
|
|
34
|
+
name: this.name,
|
|
35
|
+
code: this.code,
|
|
36
|
+
message: this.message,
|
|
37
|
+
phase: this.phase,
|
|
38
|
+
path: this.path,
|
|
39
|
+
iteration: this.iteration,
|
|
40
|
+
retryable: this.retryable,
|
|
41
|
+
retryAfterMs: this.retryAfterMs,
|
|
42
|
+
resetAt: this.resetAt
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function defaultRetryable(init) {
|
|
47
|
+
switch (init.code) {
|
|
48
|
+
case "ENGINE":
|
|
49
|
+
case "TIMEOUT":
|
|
50
|
+
case "RATE_LIMIT":
|
|
51
|
+
return true;
|
|
52
|
+
case "QUOTA":
|
|
53
|
+
return init.resetAt != null || init.retryAfterMs != null;
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { LoopError };
|
|
60
|
+
//# sourceMappingURL=chunk-I3STY7U6.js.map
|
|
61
|
+
//# sourceMappingURL=chunk-I3STY7U6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts"],"names":[],"mappings":";AAuCO,IAAM,SAAA,GAAN,MAAM,UAAA,SAAkB,KAAA,CAAM;AAAA,EAC1B,IAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA;AAAA,EAEA,YAAA;AAAA;AAAA,EAEA,OAAA;AAAA,EAET,YAAY,IAAA,EAAqB;AAC/B,IAAA,KAAA;AAAA,MACE,IAAA,CAAK,OAAA;AAAA,MACL,KAAK,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,IAAA,CAAK,OAAM,GAAI;AAAA,KACrD;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,WAAA;AACZ,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,QAAQ,IAAA,CAAK,KAAA;AAClB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,YAAY,IAAA,CAAK,SAAA;AACtB,IAAA,IAAA,CAAK,eAAe,IAAA,CAAK,YAAA;AACzB,IAAA,IAAA,CAAK,UAAU,IAAA,CAAK,OAAA;AACpB,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,SAAA,IAAa,gBAAA,CAAiB,IAAI,CAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,OAAO,IAAA,CACL,KAAA,EACA,QAAA,EACW;AACX,IAAA,IAAI,KAAA,YAAiB,YAAW,OAAO,KAAA;AACvC,IAAA,MAAM,UAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACrE,IAAA,OAAO,IAAI,WAAU,EAAE,GAAG,UAAU,OAAA,EAAS,KAAA,EAAO,OAAO,CAAA;AAAA,EAC7D;AAAA,EAEA,MAAA,GAAS;AACP,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,WAAW,IAAA,CAAK,SAAA;AAAA,MAChB,WAAW,IAAA,CAAK,SAAA;AAAA,MAChB,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,SAAS,IAAA,CAAK;AAAA,KAChB;AAAA,EACF;AACF;AAQA,SAAS,iBAAiB,IAAA,EAA8B;AACtD,EAAA,QAAQ,KAAK,IAAA;AAAM,IACjB,KAAK,QAAA;AAAA,IACL,KAAK,SAAA;AAAA,IACL,KAAK,YAAA;AACH,MAAA,OAAO,IAAA;AAAA,IACT,KAAK,OAAA;AACH,MAAA,OAAO,IAAA,CAAK,OAAA,IAAW,IAAA,IAAQ,IAAA,CAAK,YAAA,IAAgB,IAAA;AAAA,IACtD;AACE,MAAA,OAAO,KAAA;AAAA;AAEb","file":"chunk-I3STY7U6.js","sourcesContent":["/**\n * Structured, classified errors so the exit report can say *what* failed,\n * *where* in the loop tree, and *why* — instead of dumping a stack.\n */\n\nexport type LoopErrorCode =\n | 'ENGINE' // the backend (SDK/CLI/API) threw or returned an error\n | 'TIMEOUT' // a step exceeded its time budget\n | 'ABORTED' // an early-exit signal interrupted the work\n | 'VALIDATION' // a condition/validator could not produce a verdict\n | 'CONFIG' // the loop definition or CLI input was invalid\n | 'BUDGET' // the run's token budget was exhausted\n | 'RATE_LIMIT' // the provider throttled the call (resets on its own)\n | 'QUOTA' // an account/usage allowance was hit (may or may not reset)\n | 'BODY' // the step body threw\n | 'UNKNOWN';\n\nexport type LoopPhase =\n | 'start'\n | 'body'\n | 'until'\n | 'stopOn'\n | 'review'\n | 'engine';\n\nexport interface LoopErrorInit {\n code: LoopErrorCode;\n message: string;\n phase?: LoopPhase;\n path?: readonly string[];\n iteration?: number;\n cause?: unknown;\n retryable?: boolean;\n /** Suggested wait before retry, in ms (e.g. a `retry-after` header). */\n retryAfterMs?: number;\n /** When the limit resets, as epoch ms. The wait policy prefers this. */\n resetAt?: number;\n}\n\nexport class LoopError extends Error {\n readonly code: LoopErrorCode;\n readonly phase?: LoopPhase;\n readonly path?: readonly string[];\n readonly iteration?: number;\n readonly retryable: boolean;\n /** Suggested wait before retry, in ms (e.g. a `retry-after` header). */\n readonly retryAfterMs?: number;\n /** When the limit resets, as epoch ms. The wait policy prefers this. */\n readonly resetAt?: number;\n\n constructor(init: LoopErrorInit) {\n super(\n init.message,\n init.cause !== undefined ? { cause: init.cause } : undefined,\n );\n this.name = 'LoopError';\n this.code = init.code;\n this.phase = init.phase;\n this.path = init.path;\n this.iteration = init.iteration;\n this.retryAfterMs = init.retryAfterMs;\n this.resetAt = init.resetAt;\n this.retryable = init.retryable ?? defaultRetryable(init);\n }\n\n /** Wrap an arbitrary thrown value, preserving a `LoopError` as-is. */\n static from(\n value: unknown,\n fallback: Omit<LoopErrorInit, 'message' | 'cause'>,\n ): LoopError {\n if (value instanceof LoopError) return value;\n const message = value instanceof Error ? value.message : String(value);\n return new LoopError({ ...fallback, message, cause: value });\n }\n\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n phase: this.phase,\n path: this.path,\n iteration: this.iteration,\n retryable: this.retryable,\n retryAfterMs: this.retryAfterMs,\n resetAt: this.resetAt,\n };\n }\n}\n\n/**\n * Default `retryable` by code. ENGINE/TIMEOUT and RATE_LIMIT always refresh on\n * their own, so they retry. QUOTA retries only when a reset is known (a reset\n * the wait policy can act on); a quota with no parseable reset is fatal until\n * the allowance refreshes out of band. BUDGET never refreshes within a run.\n */\nfunction defaultRetryable(init: LoopErrorInit): boolean {\n switch (init.code) {\n case 'ENGINE':\n case 'TIMEOUT':\n case 'RATE_LIMIT':\n return true;\n case 'QUOTA':\n return init.resetAt != null || init.retryAfterMs != null;\n default:\n return false;\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/core/redact.ts
|
|
2
|
+
var PATTERNS = [
|
|
3
|
+
/sk-ant-[A-Za-z0-9_-]{8,}/g,
|
|
4
|
+
// Anthropic keys
|
|
5
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
6
|
+
// emails
|
|
7
|
+
/\b(api[_-]?key|token|secret|password|authorization|bearer)\b\s*[=:]\s*\S+/gi
|
|
8
|
+
// key=value creds
|
|
9
|
+
];
|
|
10
|
+
function redactSecrets(text) {
|
|
11
|
+
let out = text;
|
|
12
|
+
for (const pattern of PATTERNS) out = out.replace(pattern, "[redacted]");
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { redactSecrets };
|
|
17
|
+
//# sourceMappingURL=chunk-JFTXJ7I2.js.map
|
|
18
|
+
//# sourceMappingURL=chunk-JFTXJ7I2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/redact.ts"],"names":[],"mappings":";AAMA,IAAM,QAAA,GAAqB;AAAA,EACzB,2BAAA;AAAA;AAAA,EACA,qDAAA;AAAA;AAAA,EACA;AAAA;AACF,CAAA;AAEO,SAAS,cAAc,IAAA,EAAsB;AAClD,EAAA,IAAI,GAAA,GAAM,IAAA;AACV,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,SAAS,YAAY,CAAA;AACvE,EAAA,OAAO,GAAA;AACT","file":"chunk-JFTXJ7I2.js","sourcesContent":["/**\n * Best-effort secret scrubbing for text that flows into events / logs / the\n * exit summary (e.g. a subprocess's stderr, which we don't control). Not a\n * security boundary — a guard against accidental credential echo.\n */\n\nconst PATTERNS: RegExp[] = [\n /sk-ant-[A-Za-z0-9_-]{8,}/g, // Anthropic keys\n /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g, // emails\n /\\b(api[_-]?key|token|secret|password|authorization|bearer)\\b\\s*[=:]\\s*\\S+/gi, // key=value creds\n];\n\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const pattern of PATTERNS) out = out.replace(pattern, '[redacted]');\n return out;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/engines/engine.ts
|
|
2
|
+
var SUBAGENT_TOOLS = ["Task"];
|
|
3
|
+
function isEngine(ref) {
|
|
4
|
+
return typeof ref === "object" && ref !== null && typeof ref.run === "function";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export { SUBAGENT_TOOLS, isEngine };
|
|
8
|
+
//# sourceMappingURL=chunk-XC46B4FD.js.map
|
|
9
|
+
//# sourceMappingURL=chunk-XC46B4FD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engines/engine.ts"],"names":[],"mappings":";AAwBO,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-XC46B4FD.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 | '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, 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 (claude-cli / agent-sdk) treats permission prompts.\n * Mirrors the Claude Code values. `bypassPermissions` lets a headless worker\n * read/write/run without prompting — required for an unattended agent that must\n * touch the filesystem or 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 `claude-cli`: path to the binary (defaults to `claude` on PATH). */\n cliBinary?: string;\n /** Extra args appended to the `claude` invocation. */\n cliArgs?: string[];\n /**\n * Permission mode for tool-using engines (claude-cli `--permission-mode`,\n * agent-sdk `permissionMode`). Unset = the engine/CLI default (prompts).\n */\n permissionMode?: PermissionMode;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/core/limits.ts
|
|
2
|
+
var LIMIT_CODES = /* @__PURE__ */ new Set([
|
|
3
|
+
"RATE_LIMIT",
|
|
4
|
+
"QUOTA",
|
|
5
|
+
"BUDGET"
|
|
6
|
+
]);
|
|
7
|
+
function isLimitError(error) {
|
|
8
|
+
return !!error && LIMIT_CODES.has(error.code);
|
|
9
|
+
}
|
|
10
|
+
function waitMsFor(error, now = Date.now()) {
|
|
11
|
+
if (error.code === "BUDGET") return void 0;
|
|
12
|
+
if (typeof error.retryAfterMs === "number" && error.retryAfterMs >= 0)
|
|
13
|
+
return error.retryAfterMs;
|
|
14
|
+
if (typeof error.resetAt === "number")
|
|
15
|
+
return Math.max(0, error.resetAt - now);
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
function retryAfterHeaderToMs(value, now = Date.now()) {
|
|
19
|
+
if (value == null) return void 0;
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (trimmed === "") return void 0;
|
|
22
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
23
|
+
const when = Date.parse(trimmed);
|
|
24
|
+
if (!Number.isNaN(when)) return Math.max(0, when - now);
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { isLimitError, retryAfterHeaderToMs, waitMsFor };
|
|
29
|
+
//# sourceMappingURL=chunk-Y2SD7GBL.js.map
|
|
30
|
+
//# sourceMappingURL=chunk-Y2SD7GBL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/limits.ts"],"names":[],"mappings":";AAeA,IAAM,WAAA,uBAA8C,GAAA,CAAI;AAAA,EACtD,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAGM,SAAS,aAAa,KAAA,EAAkD;AAC7E,EAAA,OAAO,CAAC,CAAC,KAAA,IAAS,WAAA,CAAY,GAAA,CAAI,MAAM,IAAI,CAAA;AAC9C;AAQO,SAAS,SAAA,CACd,KAAA,EACA,GAAA,GAAc,IAAA,CAAK,KAAI,EACH;AACpB,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,QAAA,EAAU,OAAO,MAAA;AACpC,EAAA,IAAI,OAAO,KAAA,CAAM,YAAA,KAAiB,QAAA,IAAY,MAAM,YAAA,IAAgB,CAAA;AAClE,IAAA,OAAO,KAAA,CAAM,YAAA;AACf,EAAA,IAAI,OAAO,MAAM,OAAA,KAAY,QAAA;AAC3B,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,UAAU,GAAG,CAAA;AACxC,EAAA,OAAO,MAAA;AACT;AAOO,SAAS,oBAAA,CACd,KAAA,EACA,GAAA,GAAc,IAAA,CAAK,KAAI,EACH;AACpB,EAAA,IAAI,KAAA,IAAS,MAAM,OAAO,MAAA;AAC1B,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,OAAA,KAAY,IAAI,OAAO,MAAA;AAC3B,EAAA,IAAI,QAAQ,IAAA,CAAK,OAAO,GAAG,OAAO,MAAA,CAAO,OAAO,CAAA,GAAI,GAAA;AACpD,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAC/B,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,SAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,GAAO,GAAG,CAAA;AACtD,EAAA,OAAO,MAAA;AACT","file":"chunk-Y2SD7GBL.js","sourcesContent":["/**\n * Provider-limit plumbing shared by the engines and the runner.\n *\n * Engines classify a backend throttle/allowance signal into a `RATE_LIMIT` or\n * `QUOTA` `LoopError` carrying the reset hint they could extract (`retryAfterMs`\n * or `resetAt`). The runner reads that hint back through `waitMsFor` to decide\n * whether to wait-and-continue or checkpoint-and-pause (see `onLimit`).\n *\n * Keeping the reset-time math in one place means every engine and the policy\n * agree on what \"a known, bounded wait\" means.\n */\n\nimport type { LoopError, LoopErrorCode } from './errors.ts';\n\n/** The error codes the limit policy reacts to: provider limits + the budget. */\nconst LIMIT_CODES: ReadonlySet<LoopErrorCode> = new Set([\n 'RATE_LIMIT',\n 'QUOTA',\n 'BUDGET',\n]);\n\n/** True when an error is one the `onLimit` policy governs. */\nexport function isLimitError(error: LoopError | undefined): error is LoopError {\n return !!error && LIMIT_CODES.has(error.code);\n}\n\n/**\n * The wait a limit error implies, in ms, or `undefined` when no reset is known.\n * Prefers an explicit `retryAfterMs`; falls back to `resetAt - now` (floored at\n * 0 so an already-passed reset waits nothing rather than going negative). BUDGET\n * never refreshes within a run, so it never yields a wait.\n */\nexport function waitMsFor(\n error: LoopError,\n now: number = Date.now(),\n): number | undefined {\n if (error.code === 'BUDGET') return undefined;\n if (typeof error.retryAfterMs === 'number' && error.retryAfterMs >= 0)\n return error.retryAfterMs;\n if (typeof error.resetAt === 'number')\n return Math.max(0, error.resetAt - now);\n return undefined;\n}\n\n/**\n * Parse a `Retry-After` header value to ms. The HTTP spec allows two forms: a\n * number of seconds, or an HTTP-date. Returns `undefined` for anything we can't\n * read, so the caller falls back to other reset hints.\n */\nexport function retryAfterHeaderToMs(\n value: string | null | undefined,\n now: number = Date.now(),\n): number | undefined {\n if (value == null) return undefined;\n const trimmed = value.trim();\n if (trimmed === '') return undefined;\n if (/^\\d+$/.test(trimmed)) return Number(trimmed) * 1000;\n const when = Date.parse(trimmed);\n if (!Number.isNaN(when)) return Math.max(0, when - now);\n return undefined;\n}\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { redactSecrets } from './chunk-JFTXJ7I2.js';
|
|
2
|
+
import { newAccumulator, mapMessage } from './chunk-CXEPZHSR.js';
|
|
3
|
+
import { SUBAGENT_TOOLS } from './chunk-XC46B4FD.js';
|
|
4
|
+
import { LoopError } from './chunk-I3STY7U6.js';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
|
|
7
|
+
function classifyCliLimit(text) {
|
|
8
|
+
const lower = text.toLowerCase();
|
|
9
|
+
const isUsage = /usage limit|out of credits|insufficient credits|quota|billing/.test(lower);
|
|
10
|
+
const isRate = /rate limit|rate-limit|too many requests|429/.test(lower);
|
|
11
|
+
if (!isUsage && !isRate) return void 0;
|
|
12
|
+
const resetAt = parseResetAt(text);
|
|
13
|
+
if (isUsage) {
|
|
14
|
+
return new LoopError({
|
|
15
|
+
code: "QUOTA",
|
|
16
|
+
phase: "engine",
|
|
17
|
+
message: `claude usage limit: ${text}`,
|
|
18
|
+
resetAt
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return new LoopError({
|
|
22
|
+
code: "RATE_LIMIT",
|
|
23
|
+
phase: "engine",
|
|
24
|
+
message: `claude rate limited: ${text}`,
|
|
25
|
+
resetAt
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function parseResetAt(text) {
|
|
29
|
+
const m = /(?:reset|resets|retry|available)\D{0,20}(\d{10,13})/i.exec(text);
|
|
30
|
+
if (!m) return void 0;
|
|
31
|
+
const n = Number(m[1]);
|
|
32
|
+
if (!Number.isFinite(n)) return void 0;
|
|
33
|
+
return m[1].length <= 10 ? n * 1e3 : n;
|
|
34
|
+
}
|
|
35
|
+
function buildClaudeArgs(req, opts) {
|
|
36
|
+
const model = req.model ?? opts.defaultModel;
|
|
37
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
38
|
+
if (model) args.push("--model", model);
|
|
39
|
+
if (req.system) args.push("--append-system-prompt", req.system);
|
|
40
|
+
if (req.allowedTools?.length)
|
|
41
|
+
args.push("--allowedTools", req.allowedTools.join(","));
|
|
42
|
+
if (req.leaf) args.push("--disallowedTools", SUBAGENT_TOOLS.join(","));
|
|
43
|
+
if (opts.permissionMode) args.push("--permission-mode", opts.permissionMode);
|
|
44
|
+
if (opts.cliArgs?.length) args.push(...opts.cliArgs);
|
|
45
|
+
args.push("--", req.prompt);
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
var ClaudeCliEngine = class {
|
|
49
|
+
constructor(opts = {}) {
|
|
50
|
+
this.opts = opts;
|
|
51
|
+
}
|
|
52
|
+
opts;
|
|
53
|
+
name = "claude-cli";
|
|
54
|
+
async run(req, onEvent, signal) {
|
|
55
|
+
const bin = this.opts.cliBinary ?? "claude";
|
|
56
|
+
const model = req.model ?? this.opts.defaultModel;
|
|
57
|
+
const args = buildClaudeArgs(req, this.opts);
|
|
58
|
+
const acc = newAccumulator(model ?? "claude-cli");
|
|
59
|
+
const sub = execa(bin, args, {
|
|
60
|
+
cwd: req.cwd,
|
|
61
|
+
cancelSignal: signal,
|
|
62
|
+
// The prompt is passed as an argument, not piped — don't let `claude -p`
|
|
63
|
+
// stall waiting on stdin.
|
|
64
|
+
stdin: "ignore",
|
|
65
|
+
// If the child ignores the SIGTERM from an abort/timeout, escalate to
|
|
66
|
+
// SIGKILL so a wedged subprocess can't make Ctrl-C hang.
|
|
67
|
+
forceKillAfterDelay: 5e3,
|
|
68
|
+
reject: false,
|
|
69
|
+
timeout: req.timeoutMs,
|
|
70
|
+
stripFinalNewline: false
|
|
71
|
+
});
|
|
72
|
+
let buffer = "";
|
|
73
|
+
const flush = (line) => {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed) return;
|
|
76
|
+
try {
|
|
77
|
+
mapMessage(JSON.parse(trimmed), acc, onEvent);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
sub.stdout?.setEncoding("utf8");
|
|
82
|
+
sub.stdout?.on("data", (chunk) => {
|
|
83
|
+
buffer += chunk;
|
|
84
|
+
let idx;
|
|
85
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
86
|
+
flush(buffer.slice(0, idx));
|
|
87
|
+
buffer = buffer.slice(idx + 1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
const result = await sub;
|
|
91
|
+
if (buffer) flush(buffer);
|
|
92
|
+
if (signal.aborted)
|
|
93
|
+
throw new LoopError({
|
|
94
|
+
code: "ABORTED",
|
|
95
|
+
phase: "engine",
|
|
96
|
+
message: "claude-cli run aborted"
|
|
97
|
+
});
|
|
98
|
+
if (result.failed) {
|
|
99
|
+
const stderr = typeof result.stderr === "string" ? redactSecrets(result.stderr.slice(0, 400)) : "";
|
|
100
|
+
if (!result.timedOut) {
|
|
101
|
+
const stdout = typeof result.stdout === "string" ? redactSecrets(result.stdout.slice(0, 400)) : "";
|
|
102
|
+
const limit = classifyCliLimit(`${stderr}
|
|
103
|
+
${stdout}`);
|
|
104
|
+
if (limit) throw limit;
|
|
105
|
+
}
|
|
106
|
+
throw new LoopError({
|
|
107
|
+
code: result.timedOut ? "TIMEOUT" : "ENGINE",
|
|
108
|
+
phase: "engine",
|
|
109
|
+
message: `claude exited ${result.exitCode ?? "?"}${stderr ? `: ${stderr}` : ""}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
onEvent({ type: "usage", usage: acc.usage, model: acc.model });
|
|
113
|
+
return {
|
|
114
|
+
text: acc.text,
|
|
115
|
+
usage: acc.usage,
|
|
116
|
+
model: acc.model,
|
|
117
|
+
stopReason: acc.stopReason
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { ClaudeCliEngine, buildClaudeArgs, classifyCliLimit };
|
|
123
|
+
//# sourceMappingURL=claude-cli-U7WEVAOL.js.map
|
|
124
|
+
//# sourceMappingURL=claude-cli-U7WEVAOL.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LoopError } from './chunk-I3STY7U6.js';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
|
|
7
|
+
var CodexEngine = class {
|
|
8
|
+
constructor(opts = {}) {
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
}
|
|
11
|
+
opts;
|
|
12
|
+
name = "codex";
|
|
13
|
+
async run(req, onEvent, signal) {
|
|
14
|
+
const model = req.model ?? this.opts.defaultModel;
|
|
15
|
+
const dir = mkdtempSync(join(tmpdir(), "loops-codex-"));
|
|
16
|
+
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);
|
|
26
|
+
try {
|
|
27
|
+
const sub = await execa(this.opts.cliBinary ?? "codex", args, {
|
|
28
|
+
stdin: "ignore",
|
|
29
|
+
// codex exec stalls on an open stdin
|
|
30
|
+
cancelSignal: signal,
|
|
31
|
+
forceKillAfterDelay: 5e3,
|
|
32
|
+
reject: false,
|
|
33
|
+
timeout: req.timeoutMs
|
|
34
|
+
});
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
throw new LoopError({ code: "ABORTED", phase: "engine", message: "codex run aborted" });
|
|
37
|
+
let text = "";
|
|
38
|
+
try {
|
|
39
|
+
text = readFileSync(outFile, "utf8").trim();
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
if (!text && sub.failed)
|
|
43
|
+
throw new LoopError({
|
|
44
|
+
code: sub.timedOut ? "TIMEOUT" : "ENGINE",
|
|
45
|
+
phase: "engine",
|
|
46
|
+
message: `codex exited ${sub.exitCode ?? "?"}${typeof sub.stderr === "string" ? `: ${sub.stderr.slice(0, 300)}` : ""}`
|
|
47
|
+
});
|
|
48
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
49
|
+
if (text) onEvent({ type: "text", delta: text });
|
|
50
|
+
onEvent({ type: "usage", usage, model: model ?? "codex" });
|
|
51
|
+
return { text, usage, model: model ?? "codex", stopReason: "end_turn" };
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export { CodexEngine };
|
|
59
|
+
//# sourceMappingURL=codex-6I5UZ2HM.js.map
|
|
60
|
+
//# sourceMappingURL=codex-6I5UZ2HM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engines/codex.ts"],"names":[],"mappings":"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","file":"codex-6I5UZ2HM.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';\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 // codex exec has no system-prompt flag; fold any system text into the prompt.\n const prompt = req.system ? `${req.system}\\n\\n---\\n\\n${req.prompt}` : req.prompt;\n const args = ['exec', '--ephemeral', '-s', 'read-only', '--skip-git-repo-check'];\n if (req.cwd) args.push('-C', req.cwd);\n if (model) args.push('-m', model);\n args.push('-o', outFile, prompt);\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' ? `: ${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"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `commandEnvironment` — a generic, CLI-driven Environment. Every IaC tool
|
|
5
|
+
* (sst, terraform, pulumi, cloudformation-via-aws-cli) has the same shape: a
|
|
6
|
+
* command to deploy a stage, a command to read its outputs, a command to tear it
|
|
7
|
+
* down. This factory captures that shape, so a concrete adapter is a thin preset
|
|
8
|
+
* (see ./sst.ts) rather than bespoke code.
|
|
9
|
+
*
|
|
10
|
+
* It drives the CLIs through `execa` — no SDK, no new dependency — so it stays in
|
|
11
|
+
* the loops package as an OPT-IN subpath (`loops/env/command`) without coupling
|
|
12
|
+
* the core to any deploy tool. An SDK-bound adapter (e.g. @aws-sdk) adds a real
|
|
13
|
+
* dependency and belongs in a separate package or the consumer instead.
|
|
14
|
+
*
|
|
15
|
+
* The consumer supplies the tool-specific bits: how a stage name is derived, the
|
|
16
|
+
* argv for each phase, and `map` — which parsed output is the URL and how the
|
|
17
|
+
* outputs become the env vars the gate reads. The factory stays tool-agnostic.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** A command to run: a binary and its args. */
|
|
21
|
+
interface Cmd {
|
|
22
|
+
cmd: string;
|
|
23
|
+
args?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface CommandEnvConfig {
|
|
26
|
+
/** Adapter name (surfaced in errors). Default 'command'. */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** Working dir for the commands. Default: the workspace dir (the worktree). */
|
|
29
|
+
cwd?: (ws: Workspace) => string;
|
|
30
|
+
/** Stage/stack/workspace identity. Default: a slug of the workspace branch. */
|
|
31
|
+
stage?: (ws: Workspace) => string;
|
|
32
|
+
/** Argv to deploy the stage. */
|
|
33
|
+
deploy: (stage: string, ws: Workspace) => Cmd;
|
|
34
|
+
/** Argv to read outputs as JSON on stdout. Optional (no URL/env if omitted). */
|
|
35
|
+
outputs?: (stage: string, ws: Workspace) => Cmd;
|
|
36
|
+
/** Argv to tear the stage down. */
|
|
37
|
+
destroy: (stage: string, ws: Workspace) => Cmd;
|
|
38
|
+
/**
|
|
39
|
+
* Turn the outputs into the handle's `url` + `env`. This is where the tool's
|
|
40
|
+
* specific shape is normalised: `outputs` is the best-effort JSON parse (for
|
|
41
|
+
* terraform/pulumi/sst), `raw` is the verbatim stdout (for tools whose output
|
|
42
|
+
* is not JSON, e.g. `docker compose port` → `0.0.0.0:49153`).
|
|
43
|
+
*/
|
|
44
|
+
map?: (outputs: Record<string, unknown>, stage: string, raw: string) => {
|
|
45
|
+
url?: string;
|
|
46
|
+
env?: Record<string, string>;
|
|
47
|
+
};
|
|
48
|
+
/** Per-command timeout (ms). */
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
declare function commandEnvironment(config: CommandEnvConfig): Environment;
|
|
52
|
+
|
|
53
|
+
export { type Cmd, type CommandEnvConfig, commandEnvironment };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"command.js"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `dockerEnvironment` — a local environment via Docker Compose, a thin preset
|
|
5
|
+
* over `commandEnvironment`. The compose project name is the stage (a slug of
|
|
6
|
+
* the branch), so concurrent worktree-teams get isolated stacks that never
|
|
7
|
+
* collide. `docker compose -p <stage> up -d` on up; the service's published host
|
|
8
|
+
* port is discovered with `docker compose -p <stage> port` and turned into a
|
|
9
|
+
* URL; `docker compose -p <stage> down -v` on down.
|
|
10
|
+
*
|
|
11
|
+
* Publish the service to an EPHEMERAL host port in the compose file (e.g.
|
|
12
|
+
* `ports: ["3000"]`) so parallel branches do not fight over a fixed port — the
|
|
13
|
+
* adapter discovers whichever port Docker assigned. No dependency: it shells out
|
|
14
|
+
* to the `docker` CLI on PATH.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface DockerEnvConfig {
|
|
18
|
+
/** Dir containing the compose file. Default: the workspace dir. */
|
|
19
|
+
cwd?: (ws: Workspace) => string;
|
|
20
|
+
/** Compose project name (isolates per-branch stacks). Default slug(branch). */
|
|
21
|
+
project?: (ws: Workspace) => string;
|
|
22
|
+
/** Service whose published port becomes the URL. */
|
|
23
|
+
service: string;
|
|
24
|
+
/** The container port to resolve to a host port (e.g. 3000). */
|
|
25
|
+
port: number;
|
|
26
|
+
/** Build the URL from the discovered `host:port`. Default `http://<hostPort>`. */
|
|
27
|
+
url?: (hostPort: string) => string;
|
|
28
|
+
/** Extra env injected alongside `BASE_URL`. */
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
/** The docker binary. Default 'docker'. */
|
|
31
|
+
binary?: string;
|
|
32
|
+
/** Compose subcommand argv. Default `['compose']`; e.g. `['compose','-f','x.yml']`. */
|
|
33
|
+
composeArgs?: string[];
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
declare function dockerEnvironment(config: DockerEnvConfig): Environment;
|
|
37
|
+
|
|
38
|
+
export { type DockerEnvConfig, dockerEnvironment };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { commandEnvironment } from '../chunk-33YIGWNU.js';
|
|
2
|
+
|
|
3
|
+
// src/env/docker.ts
|
|
4
|
+
function dockerEnvironment(config) {
|
|
5
|
+
const bin = config.binary ?? "docker";
|
|
6
|
+
const compose = config.composeArgs ?? ["compose"];
|
|
7
|
+
const argv = (project, ...rest) => ({
|
|
8
|
+
cmd: bin,
|
|
9
|
+
args: [...compose, "-p", project, ...rest]
|
|
10
|
+
});
|
|
11
|
+
return commandEnvironment({
|
|
12
|
+
name: "docker",
|
|
13
|
+
cwd: config.cwd,
|
|
14
|
+
stage: config.project,
|
|
15
|
+
deploy: (p) => argv(p, "up", "-d"),
|
|
16
|
+
outputs: (p) => argv(p, "port", config.service, String(config.port)),
|
|
17
|
+
destroy: (p) => argv(p, "down", "-v"),
|
|
18
|
+
// `docker compose port` prints `0.0.0.0:49153` (not JSON), so read the raw
|
|
19
|
+
// stdout and normalise the bind address to localhost.
|
|
20
|
+
map: (_outputs, _stage, raw) => {
|
|
21
|
+
const hostPort = raw.trim().split("\n")[0]?.trim();
|
|
22
|
+
if (!hostPort) return {};
|
|
23
|
+
const normalized = hostPort.replace(/^0\.0\.0\.0:/, "localhost:").replace(/^\[?::\]?:/, "localhost:");
|
|
24
|
+
const url = config.url ? config.url(normalized) : `http://${normalized}`;
|
|
25
|
+
return { url, env: { BASE_URL: url, ...config.env } };
|
|
26
|
+
},
|
|
27
|
+
timeoutMs: config.timeoutMs
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { dockerEnvironment };
|
|
32
|
+
//# sourceMappingURL=docker.js.map
|
|
33
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/env/docker.ts"],"names":[],"mappings":";;;AAsCO,SAAS,kBAAkB,MAAA,EAAsC;AACtE,EAAA,MAAM,GAAA,GAAM,OAAO,MAAA,IAAU,QAAA;AAC7B,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,WAAA,IAAe,CAAC,SAAS,CAAA;AAChD,EAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAAA,GAAoB,IAAA,MAAoB;AAAA,IACpD,GAAA,EAAK,GAAA;AAAA,IACL,MAAM,CAAC,GAAG,SAAS,IAAA,EAAM,OAAA,EAAS,GAAG,IAAI;AAAA,GAC3C,CAAA;AAEA,EAAA,OAAO,kBAAA,CAAmB;AAAA,IACxB,IAAA,EAAM,QAAA;AAAA,IACN,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ,OAAO,MAAA,CAAO,OAAA;AAAA,IACd,QAAQ,CAAC,CAAA,KAAM,IAAA,CAAK,CAAA,EAAG,MAAM,IAAI,CAAA;AAAA,IACjC,OAAA,EAAS,CAAC,CAAA,KAAM,IAAA,CAAK,CAAA,EAAG,MAAA,EAAQ,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,IAAI,CAAC,CAAA;AAAA,IACnE,SAAS,CAAC,CAAA,KAAM,IAAA,CAAK,CAAA,EAAG,QAAQ,IAAI,CAAA;AAAA;AAAA;AAAA,IAGpC,GAAA,EAAK,CAAC,QAAA,EAAU,MAAA,EAAQ,GAAA,KAAQ;AAC9B,MAAA,MAAM,QAAA,GAAW,IAAI,IAAA,EAAK,CAAE,MAAM,IAAI,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK;AACjD,MAAA,IAAI,CAAC,QAAA,EAAU,OAAO,EAAC;AACvB,MAAA,MAAM,UAAA,GAAa,SAChB,OAAA,CAAQ,cAAA,EAAgB,YAAY,CAAA,CACpC,OAAA,CAAQ,cAAc,YAAY,CAAA;AACrC,MAAA,MAAM,GAAA,GAAM,OAAO,GAAA,GAAM,MAAA,CAAO,IAAI,UAAU,CAAA,GAAI,UAAU,UAAU,CAAA,CAAA;AACtE,MAAA,OAAO,EAAE,KAAK,GAAA,EAAK,EAAE,UAAU,GAAA,EAAK,GAAG,MAAA,CAAO,GAAA,EAAI,EAAE;AAAA,IACtD,CAAA;AAAA,IACA,WAAW,MAAA,CAAO;AAAA,GACnB,CAAA;AACH","file":"docker.js","sourcesContent":["/**\n * `dockerEnvironment` — a local environment via Docker Compose, a thin preset\n * over `commandEnvironment`. The compose project name is the stage (a slug of\n * the branch), so concurrent worktree-teams get isolated stacks that never\n * collide. `docker compose -p <stage> up -d` on up; the service's published host\n * port is discovered with `docker compose -p <stage> port` and turned into a\n * URL; `docker compose -p <stage> down -v` on down.\n *\n * Publish the service to an EPHEMERAL host port in the compose file (e.g.\n * `ports: [\"3000\"]`) so parallel branches do not fight over a fixed port — the\n * adapter discovers whichever port Docker assigned. No dependency: it shells out\n * to the `docker` CLI on PATH.\n */\n\nimport type { Workspace } from '../core/types.ts';\nimport type { Environment } from './environment.ts';\nimport { commandEnvironment } from './command.ts';\n\nexport interface DockerEnvConfig {\n /** Dir containing the compose file. Default: the workspace dir. */\n cwd?: (ws: Workspace) => string;\n /** Compose project name (isolates per-branch stacks). Default slug(branch). */\n project?: (ws: Workspace) => string;\n /** Service whose published port becomes the URL. */\n service: string;\n /** The container port to resolve to a host port (e.g. 3000). */\n port: number;\n /** Build the URL from the discovered `host:port`. Default `http://<hostPort>`. */\n url?: (hostPort: string) => string;\n /** Extra env injected alongside `BASE_URL`. */\n env?: Record<string, string>;\n /** The docker binary. Default 'docker'. */\n binary?: string;\n /** Compose subcommand argv. Default `['compose']`; e.g. `['compose','-f','x.yml']`. */\n composeArgs?: string[];\n timeoutMs?: number;\n}\n\nexport function dockerEnvironment(config: DockerEnvConfig): Environment {\n const bin = config.binary ?? 'docker';\n const compose = config.composeArgs ?? ['compose'];\n const argv = (project: string, ...rest: string[]) => ({\n cmd: bin,\n args: [...compose, '-p', project, ...rest],\n });\n\n return commandEnvironment({\n name: 'docker',\n cwd: config.cwd,\n stage: config.project,\n deploy: (p) => argv(p, 'up', '-d'),\n outputs: (p) => argv(p, 'port', config.service, String(config.port)),\n destroy: (p) => argv(p, 'down', '-v'),\n // `docker compose port` prints `0.0.0.0:49153` (not JSON), so read the raw\n // stdout and normalise the bind address to localhost.\n map: (_outputs, _stage, raw) => {\n const hostPort = raw.trim().split('\\n')[0]?.trim();\n if (!hostPort) return {};\n const normalized = hostPort\n .replace(/^0\\.0\\.0\\.0:/, 'localhost:')\n .replace(/^\\[?::\\]?:/, 'localhost:');\n const url = config.url ? config.url(normalized) : `http://${normalized}`;\n return { url, env: { BASE_URL: url, ...config.env } };\n },\n timeoutMs: config.timeoutMs,\n });\n}\n"]}
|