@agentstep/agent-sdk 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/package.json +45 -0
- package/src/auth/middleware.ts +38 -0
- package/src/backends/claude/args.ts +88 -0
- package/src/backends/claude/index.ts +193 -0
- package/src/backends/claude/permission-hook.ts +152 -0
- package/src/backends/claude/tool-bridge.ts +211 -0
- package/src/backends/claude/translator.ts +209 -0
- package/src/backends/claude/wrapper-script.ts +45 -0
- package/src/backends/codex/args.ts +69 -0
- package/src/backends/codex/auth.ts +35 -0
- package/src/backends/codex/index.ts +57 -0
- package/src/backends/codex/setup.ts +37 -0
- package/src/backends/codex/translator.ts +223 -0
- package/src/backends/codex/wrapper-script.ts +26 -0
- package/src/backends/factory/args.ts +45 -0
- package/src/backends/factory/auth.ts +30 -0
- package/src/backends/factory/index.ts +56 -0
- package/src/backends/factory/setup.ts +34 -0
- package/src/backends/factory/translator.ts +139 -0
- package/src/backends/factory/wrapper-script.ts +33 -0
- package/src/backends/gemini/args.ts +44 -0
- package/src/backends/gemini/auth.ts +30 -0
- package/src/backends/gemini/index.ts +53 -0
- package/src/backends/gemini/setup.ts +34 -0
- package/src/backends/gemini/translator.ts +139 -0
- package/src/backends/gemini/wrapper-script.ts +26 -0
- package/src/backends/opencode/args.ts +53 -0
- package/src/backends/opencode/auth.ts +53 -0
- package/src/backends/opencode/index.ts +70 -0
- package/src/backends/opencode/mcp.ts +67 -0
- package/src/backends/opencode/setup.ts +54 -0
- package/src/backends/opencode/translator.ts +168 -0
- package/src/backends/opencode/wrapper-script.ts +46 -0
- package/src/backends/registry.ts +38 -0
- package/src/backends/shared/ndjson.ts +29 -0
- package/src/backends/shared/translator-types.ts +69 -0
- package/src/backends/shared/wrap-prompt.ts +17 -0
- package/src/backends/types.ts +85 -0
- package/src/config/index.ts +95 -0
- package/src/db/agents.ts +185 -0
- package/src/db/api_keys.ts +78 -0
- package/src/db/batch.ts +142 -0
- package/src/db/client.ts +81 -0
- package/src/db/environments.ts +127 -0
- package/src/db/events.ts +208 -0
- package/src/db/memory.ts +143 -0
- package/src/db/migrations.ts +295 -0
- package/src/db/proxy.ts +37 -0
- package/src/db/sessions.ts +295 -0
- package/src/db/vaults.ts +110 -0
- package/src/errors.ts +53 -0
- package/src/handlers/agents.ts +194 -0
- package/src/handlers/batch.ts +41 -0
- package/src/handlers/docs.ts +87 -0
- package/src/handlers/environments.ts +154 -0
- package/src/handlers/events.ts +234 -0
- package/src/handlers/index.ts +12 -0
- package/src/handlers/memory.ts +141 -0
- package/src/handlers/openapi.ts +14 -0
- package/src/handlers/sessions.ts +223 -0
- package/src/handlers/stream.ts +76 -0
- package/src/handlers/threads.ts +26 -0
- package/src/handlers/ui/app.js +984 -0
- package/src/handlers/ui/index.html +112 -0
- package/src/handlers/ui/style.css +164 -0
- package/src/handlers/ui.ts +1281 -0
- package/src/handlers/vaults.ts +99 -0
- package/src/http.ts +35 -0
- package/src/index.ts +104 -0
- package/src/init.ts +227 -0
- package/src/openapi/registry.ts +8 -0
- package/src/openapi/schemas.ts +625 -0
- package/src/openapi/spec.ts +691 -0
- package/src/providers/apple.ts +220 -0
- package/src/providers/daytona.ts +217 -0
- package/src/providers/docker.ts +264 -0
- package/src/providers/e2b.ts +203 -0
- package/src/providers/fly.ts +276 -0
- package/src/providers/modal.ts +222 -0
- package/src/providers/podman.ts +206 -0
- package/src/providers/registry.ts +28 -0
- package/src/providers/shared.ts +11 -0
- package/src/providers/sprites.ts +55 -0
- package/src/providers/types.ts +73 -0
- package/src/providers/vercel.ts +208 -0
- package/src/proxy/forward.ts +111 -0
- package/src/queue/index.ts +111 -0
- package/src/sessions/actor.ts +53 -0
- package/src/sessions/bus.ts +155 -0
- package/src/sessions/driver.ts +818 -0
- package/src/sessions/grader.ts +120 -0
- package/src/sessions/interrupt.ts +14 -0
- package/src/sessions/sweeper.ts +136 -0
- package/src/sessions/threads.ts +126 -0
- package/src/sessions/tools.ts +50 -0
- package/src/shutdown.ts +78 -0
- package/src/sprite/client.ts +294 -0
- package/src/sprite/exec.ts +161 -0
- package/src/sprite/lifecycle.ts +339 -0
- package/src/sprite/pool.ts +65 -0
- package/src/sprite/setup.ts +159 -0
- package/src/state.ts +61 -0
- package/src/types.ts +339 -0
- package/src/util/clock.ts +7 -0
- package/src/util/ids.ts +11 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful translator: opencode NDJSON → Managed Agents events.
|
|
3
|
+
*
|
|
4
|
+
* Ported from
|
|
5
|
+
*
|
|
6
|
+
*
|
|
7
|
+
* Opencode's event model (emitted by `opencode run --format json`):
|
|
8
|
+
* - `step_start` — an agent step is starting. Carries `sessionID` on the
|
|
9
|
+
* root event object. First occurrence marks the first turn.
|
|
10
|
+
* - `text` — assistant text content, in `part.text`
|
|
11
|
+
* - `tool_use` — assistant is calling a tool, in `part.tool`,
|
|
12
|
+
* `part.callID`, `part.state.input`
|
|
13
|
+
* - `step_finish` — end of a step. Carries `part.cost`, `part.tokens`,
|
|
14
|
+
* `part.reason`. `reason === "stop"` signals turn end.
|
|
15
|
+
*
|
|
16
|
+
* Note: opencode does NOT emit tool_result events in its NDJSON stream —
|
|
17
|
+
* tool execution is handled by opencode's own plugins/built-ins, and the
|
|
18
|
+
* results are folded into subsequent assistant content. This translator
|
|
19
|
+
* does not need a tool_result case.
|
|
20
|
+
*
|
|
21
|
+
* The Managed Agents side (our taxonomy) this maps to:
|
|
22
|
+
* - system.init (internal state only — driver emits session.status_running)
|
|
23
|
+
* - agent.message for text
|
|
24
|
+
* - agent.tool_use / agent.custom_tool_use based on customToolNames
|
|
25
|
+
* - result → TurnResult with stop_reason "end_turn" + usage aggregation
|
|
26
|
+
*/
|
|
27
|
+
import type {
|
|
28
|
+
ToolClass,
|
|
29
|
+
TranslatedEvent,
|
|
30
|
+
Translator,
|
|
31
|
+
TranslatorOptions,
|
|
32
|
+
TurnResult,
|
|
33
|
+
TurnUsage,
|
|
34
|
+
} from "../shared/translator-types";
|
|
35
|
+
|
|
36
|
+
interface OpencodePart {
|
|
37
|
+
text?: string;
|
|
38
|
+
tool?: string;
|
|
39
|
+
callID?: string;
|
|
40
|
+
id?: string;
|
|
41
|
+
state?: { input?: unknown };
|
|
42
|
+
cost?: number;
|
|
43
|
+
tokens?: { input?: number; output?: number };
|
|
44
|
+
reason?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createOpencodeTranslator(opts: TranslatorOptions): Translator {
|
|
48
|
+
const toolClass = new Map<string, ToolClass>();
|
|
49
|
+
let sessionId: string | null = null;
|
|
50
|
+
let seenFirstStep = false;
|
|
51
|
+
let sawCustom = false;
|
|
52
|
+
let totalCostUsd = 0;
|
|
53
|
+
let totalInputTokens = 0;
|
|
54
|
+
let totalOutputTokens = 0;
|
|
55
|
+
let stepCount = 0;
|
|
56
|
+
let turnResult: TurnResult | null = null;
|
|
57
|
+
|
|
58
|
+
function classify(name: string): ToolClass {
|
|
59
|
+
if (opts.customToolNames.has(name)) return "custom";
|
|
60
|
+
// Opencode tools don't use the `mcp__` prefix convention; treat
|
|
61
|
+
// everything else as a built-in tool for now. (Future: if opencode
|
|
62
|
+
// exposes MCP tools with a distinct prefix, classify them here.)
|
|
63
|
+
return "builtin";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function translate(raw: Record<string, unknown>): TranslatedEvent[] {
|
|
67
|
+
const out: TranslatedEvent[] = [];
|
|
68
|
+
if (!raw || typeof raw !== "object") return out;
|
|
69
|
+
|
|
70
|
+
// Opencode puts sessionID on the root of every event that has session
|
|
71
|
+
// context (step_start, text, tool_use, step_finish). We track it in
|
|
72
|
+
// state and expose via getBackendSessionId.
|
|
73
|
+
if (typeof raw.sessionID === "string" && raw.sessionID) {
|
|
74
|
+
sessionId = raw.sessionID;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const type = String(raw.type ?? "");
|
|
78
|
+
|
|
79
|
+
switch (type) {
|
|
80
|
+
case "step_start": {
|
|
81
|
+
stepCount++;
|
|
82
|
+
if (!seenFirstStep) {
|
|
83
|
+
seenFirstStep = true;
|
|
84
|
+
// session.status_running is emitted by the driver, not the translator.
|
|
85
|
+
// We only track state here.
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "text": {
|
|
91
|
+
const part = (raw.part as OpencodePart | undefined) ?? {};
|
|
92
|
+
const text = part.text ?? "";
|
|
93
|
+
if (text) {
|
|
94
|
+
out.push({
|
|
95
|
+
type: "agent.message",
|
|
96
|
+
payload: {
|
|
97
|
+
content: [{ type: "text", text }],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "tool_use": {
|
|
105
|
+
const part = (raw.part as OpencodePart | undefined) ?? {};
|
|
106
|
+
const toolUseId = part.callID ?? part.id ?? "";
|
|
107
|
+
const name = part.tool ?? "unknown";
|
|
108
|
+
const input = part.state?.input ?? {};
|
|
109
|
+
if (!toolUseId) return out;
|
|
110
|
+
|
|
111
|
+
const cls = classify(name);
|
|
112
|
+
toolClass.set(toolUseId, cls);
|
|
113
|
+
if (cls === "custom") {
|
|
114
|
+
sawCustom = true;
|
|
115
|
+
out.push({
|
|
116
|
+
type: "agent.custom_tool_use",
|
|
117
|
+
payload: { tool_use_id: toolUseId, name, input },
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
out.push({
|
|
121
|
+
type: "agent.tool_use",
|
|
122
|
+
payload: { tool_use_id: toolUseId, name, input },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "step_finish": {
|
|
129
|
+
const part = (raw.part as OpencodePart | undefined) ?? {};
|
|
130
|
+
totalCostUsd += part.cost ?? 0;
|
|
131
|
+
if (part.tokens) {
|
|
132
|
+
totalInputTokens += part.tokens.input ?? 0;
|
|
133
|
+
totalOutputTokens += part.tokens.output ?? 0;
|
|
134
|
+
}
|
|
135
|
+
if (part.reason === "stop") {
|
|
136
|
+
const usage: TurnUsage = {
|
|
137
|
+
input_tokens: totalInputTokens,
|
|
138
|
+
output_tokens: totalOutputTokens,
|
|
139
|
+
cache_read_input_tokens: 0,
|
|
140
|
+
cache_creation_input_tokens: 0,
|
|
141
|
+
cost_usd: totalCostUsd,
|
|
142
|
+
};
|
|
143
|
+
const stopReason: TurnResult["stopReason"] = sawCustom
|
|
144
|
+
? "custom_tool_call"
|
|
145
|
+
: "end_turn";
|
|
146
|
+
turnResult = {
|
|
147
|
+
stopReason,
|
|
148
|
+
usage,
|
|
149
|
+
num_turns: stepCount,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Non-"stop" step_finish: just accumulate, no turn result yet
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
// Unrecognized — drop silently, translator is forward-compatible.
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
translate,
|
|
164
|
+
getBackendSessionId: () => sessionId,
|
|
165
|
+
getTurnResult: () => turnResult,
|
|
166
|
+
sawCustomToolUse: () => sawCustom,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite wrapper script for opencode.
|
|
3
|
+
*
|
|
4
|
+
* Opencode (unlike claude) does NOT accept the prompt on stdin — it takes
|
|
5
|
+
* the prompt as a positional argv. But the prompt is too large/unsafe to
|
|
6
|
+
* transport via sprites.dev's `?cmd=...` HTTP query params. So the wrapper:
|
|
7
|
+
*
|
|
8
|
+
* 1. Reads env vars from stdin until a blank line (same protocol as
|
|
9
|
+
* claude's wrapper — KEY=value per line, blank line terminator)
|
|
10
|
+
* 2. Captures the remaining stdin into a `$PROMPT` shell variable
|
|
11
|
+
* 3. Execs `opencode <argv> "$PROMPT"` — injecting the prompt as a
|
|
12
|
+
* trailing positional argv entry
|
|
13
|
+
*
|
|
14
|
+
* This keeps the prompt on the HTTP request body (not the URL) and still
|
|
15
|
+
* lets opencode receive it in the argv shape it expects.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: `buildOpencodeArgs` must never end argv with a flag that expects
|
|
18
|
+
* its own value (e.g. a dangling `--model` without the model name) —
|
|
19
|
+
* otherwise `"$PROMPT"` would bind to the wrong flag. All current
|
|
20
|
+
* `buildOpencodeArgs` output is safe (flag + value pairs, no trailing
|
|
21
|
+
* dangling flag).
|
|
22
|
+
*
|
|
23
|
+
* NOTE 2: `PROMPT=$(cat)` strips trailing newlines from the captured body.
|
|
24
|
+
* For normal prompts this is a harmless quirk; very prompt-sensitive
|
|
25
|
+
* consumers should be aware.
|
|
26
|
+
*/
|
|
27
|
+
import type { ContainerProvider } from "../../providers/types";
|
|
28
|
+
|
|
29
|
+
export const OPENCODE_WRAPPER_PATH = "/tmp/.opencode-wrapper";
|
|
30
|
+
|
|
31
|
+
const SPRITE_WRAPPER_SCRIPT = [
|
|
32
|
+
"#!/bin/bash",
|
|
33
|
+
"set -e",
|
|
34
|
+
'while IFS= read -r line; do [ -z "$line" ] && break; export "$line"; done',
|
|
35
|
+
"PROMPT=$(cat)",
|
|
36
|
+
'exec opencode "$@" "$PROMPT"',
|
|
37
|
+
].join("\n");
|
|
38
|
+
|
|
39
|
+
export async function installOpencodeWrapper(spriteName: string, provider: ContainerProvider): Promise<void> {
|
|
40
|
+
const escaped = SPRITE_WRAPPER_SCRIPT.replace(/'/g, "'\\''");
|
|
41
|
+
await provider.exec(spriteName, [
|
|
42
|
+
"bash",
|
|
43
|
+
"-c",
|
|
44
|
+
`printf '%s' '${escaped}' > ${OPENCODE_WRAPPER_PATH} && chmod +x ${OPENCODE_WRAPPER_PATH}`,
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend registry: maps a backend name to its concrete implementation.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own file so types.ts and the concrete backends can import the
|
|
5
|
+
* type definitions without circular dependencies. The driver imports only
|
|
6
|
+
* from here.
|
|
7
|
+
*/
|
|
8
|
+
import type { Backend, BackendName } from "./types";
|
|
9
|
+
import { claudeBackend } from "./claude";
|
|
10
|
+
import { opencodeBackend } from "./opencode";
|
|
11
|
+
import { codexBackend } from "./codex";
|
|
12
|
+
import { geminiBackend } from "./gemini";
|
|
13
|
+
import { factoryBackend } from "./factory";
|
|
14
|
+
|
|
15
|
+
const BACKENDS: Record<BackendName, Backend> = {
|
|
16
|
+
claude: claudeBackend,
|
|
17
|
+
opencode: opencodeBackend,
|
|
18
|
+
codex: codexBackend,
|
|
19
|
+
gemini: geminiBackend,
|
|
20
|
+
factory: factoryBackend,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a backend by name, defaulting to claude for undefined input.
|
|
25
|
+
* Throws if the name is set but unknown.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveBackend(
|
|
28
|
+
name: BackendName | string | null | undefined,
|
|
29
|
+
): Backend {
|
|
30
|
+
const key = (name ?? "claude") as BackendName;
|
|
31
|
+
const b = BACKENDS[key];
|
|
32
|
+
if (!b) throw new Error(`unknown backend: ${name}`);
|
|
33
|
+
return b;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listBackends(): BackendName[] {
|
|
37
|
+
return Object.keys(BACKENDS) as BackendName[];
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON line parser for streaming CLI output (backend-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Returns the incomplete trailing portion so the caller can keep buffering.
|
|
5
|
+
* Lifted in spirit from
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* Note: the driver strips sprites.dev HTTP exec framing bytes (0x00-0x1F)
|
|
9
|
+
* from the raw stream BEFORE calling this parser — this parser does not
|
|
10
|
+
* mangle bytes itself.
|
|
11
|
+
*/
|
|
12
|
+
export function parseNDJSONLines(
|
|
13
|
+
buffer: string,
|
|
14
|
+
onLine: (parsed: Record<string, unknown>) => void,
|
|
15
|
+
): string {
|
|
16
|
+
const lines = buffer.split("\n");
|
|
17
|
+
const remainder = lines.pop() ?? "";
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
23
|
+
onLine(parsed);
|
|
24
|
+
} catch {
|
|
25
|
+
// Skip non-JSON lines (progress noise, stray log output)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return remainder;
|
|
29
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared translator contract — implemented by every backend's translator
|
|
3
|
+
* (claude, opencode, ...). Decoupled from any particular CLI event shape.
|
|
4
|
+
*
|
|
5
|
+
* A Translator consumes raw NDJSON events from a backend CLI and produces
|
|
6
|
+
* Managed Agents event payloads (unwrapped — the bus adds id/seq/etc).
|
|
7
|
+
*
|
|
8
|
+
* Implementations are stateful per-turn: they track the backend session id
|
|
9
|
+
* so the driver can persist it and use it on the next `--resume`/`--session`
|
|
10
|
+
* turn, and they track whether any custom tool was emitted so the driver can
|
|
11
|
+
* flip the turn's `stop_reason` to "custom_tool_call".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ToolClass = "builtin" | "mcp" | "custom";
|
|
15
|
+
|
|
16
|
+
export interface TranslatedEvent {
|
|
17
|
+
type: string;
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TurnUsage {
|
|
22
|
+
input_tokens: number;
|
|
23
|
+
output_tokens: number;
|
|
24
|
+
cache_read_input_tokens: number;
|
|
25
|
+
cache_creation_input_tokens: number;
|
|
26
|
+
cost_usd: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TurnResult {
|
|
30
|
+
stopReason: "end_turn" | "max_turns" | "error" | "custom_tool_call";
|
|
31
|
+
usage: TurnUsage;
|
|
32
|
+
num_turns: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Translator {
|
|
36
|
+
/**
|
|
37
|
+
* Consume a single raw CLI NDJSON event and return zero or more Managed
|
|
38
|
+
* Agents event payloads. These are appended to the session via
|
|
39
|
+
* `bus.appendEventsBatch` by the driver.
|
|
40
|
+
*/
|
|
41
|
+
translate(raw: Record<string, unknown>): TranslatedEvent[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Latest backend session id observed so far (claude `system.init.session_id`
|
|
45
|
+
* or opencode `sessionID`). Exposed so the driver can persist it and use
|
|
46
|
+
* it on the next turn's resume flag.
|
|
47
|
+
*/
|
|
48
|
+
getBackendSessionId(): string | null;
|
|
49
|
+
|
|
50
|
+
/** Summarize the turn from the most recent `result` event for the driver */
|
|
51
|
+
getTurnResult(): TurnResult | null;
|
|
52
|
+
|
|
53
|
+
/** True if any custom tool was emitted during this turn */
|
|
54
|
+
sawCustomToolUse(): boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TranslatorOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Names of custom tools defined on the agent. Used to classify tool_use
|
|
60
|
+
* events the translator doesn't otherwise recognize.
|
|
61
|
+
*/
|
|
62
|
+
customToolNames: Set<string>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* True if this is the first turn of the session. Controls whether
|
|
66
|
+
* `system.init`-equivalent events trigger status_running (first turn only).
|
|
67
|
+
*/
|
|
68
|
+
isFirstTurn: boolean;
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt wrapper for backends that lack a `--system-prompt` CLI flag
|
|
3
|
+
* (opencode, codex). If a system prompt is set on the agent, prepend it to
|
|
4
|
+
* the user prompt with a separator. If no system prompt, return the prompt
|
|
5
|
+
* unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Pattern from
|
|
8
|
+
*
|
|
9
|
+
* (opencode) and 253-256 (codex) — both use the identical wrapping format.
|
|
10
|
+
*/
|
|
11
|
+
export function wrapPromptWithSystem(
|
|
12
|
+
prompt: string,
|
|
13
|
+
systemPrompt: string | null | undefined,
|
|
14
|
+
): string {
|
|
15
|
+
if (!systemPrompt) return prompt;
|
|
16
|
+
return `Instructions: ${systemPrompt}\n\n---\n\n${prompt}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend abstraction: a pluggable CLI engine that powers a session turn.
|
|
3
|
+
*
|
|
4
|
+
* Concrete backends (claude, opencode) implement this interface. The driver
|
|
5
|
+
* resolves a Backend from the agent's `backend` field via the registry and
|
|
6
|
+
* delegates argv/env/stdin construction and stream translation to it.
|
|
7
|
+
*
|
|
8
|
+
* Design notes:
|
|
9
|
+
* - argv, env, stdin are returned as three separate primitives so the driver
|
|
10
|
+
* owns the stdin framing composition (env KEY=value lines + blank line +
|
|
11
|
+
* prompt body). This prevents each backend from reinventing the glue and
|
|
12
|
+
* silently diverging.
|
|
13
|
+
* - stdin is the PROMPT BODY, not the full wrapper body. The driver composes
|
|
14
|
+
* the final wrapper stdin as `envLines + "\n\n" + stdin`. The prompt rides
|
|
15
|
+
* the HTTP request body (not the URL) — URL length caps on sprites.dev's
|
|
16
|
+
* `?cmd=...` query params are the reason we can't just put it in argv.
|
|
17
|
+
* Claude's wrapper pipes the prompt to stdin of claude; opencode's wrapper
|
|
18
|
+
* captures it into $PROMPT and re-passes it as argv to opencode (which
|
|
19
|
+
* doesn't accept stdin prompts).
|
|
20
|
+
*/
|
|
21
|
+
import type { Agent } from "../types";
|
|
22
|
+
import type { ContainerProvider } from "../providers/types";
|
|
23
|
+
import type { Translator, TranslatorOptions } from "./shared/translator-types";
|
|
24
|
+
|
|
25
|
+
/** CLI backends that have a Backend implementation in the registry */
|
|
26
|
+
export type CliBackendName = "claude" | "opencode" | "codex" | "gemini" | "factory";
|
|
27
|
+
/** All backend names including proxy-only backends */
|
|
28
|
+
export type AnyBackendName = CliBackendName | "anthropic";
|
|
29
|
+
|
|
30
|
+
/** @deprecated Use CliBackendName or AnyBackendName depending on context */
|
|
31
|
+
export type BackendName = CliBackendName;
|
|
32
|
+
|
|
33
|
+
export interface BuildTurnInput {
|
|
34
|
+
agent: Agent;
|
|
35
|
+
/** session id from a prior turn; null on turn 1 */
|
|
36
|
+
backendSessionId: string | null;
|
|
37
|
+
/** plain-text prompt text (concatenated from all pending text inputs) */
|
|
38
|
+
promptText: string;
|
|
39
|
+
/** tool_result inputs for backends that support mid-turn re-entry */
|
|
40
|
+
toolResults: Array<{ custom_tool_use_id: string; content: unknown[] }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuildTurnResult {
|
|
44
|
+
/** argv to append after the wrapper path — does NOT include the prompt text */
|
|
45
|
+
argv: string[];
|
|
46
|
+
/** env vars to inject via the wrapper's stdin env-read loop (auth + MCP + etc.) */
|
|
47
|
+
env: Record<string, string>;
|
|
48
|
+
/**
|
|
49
|
+
* prompt body to send on stdin after the env block. For claude this is the
|
|
50
|
+
* raw prompt OR the stream-json user frame (when toolResults is non-empty).
|
|
51
|
+
* For opencode this is the wrapped prompt (system prompt prepended if set)
|
|
52
|
+
* that the opencode wrapper captures via `PROMPT=$(cat)` and re-passes as
|
|
53
|
+
* argv to opencode.
|
|
54
|
+
*/
|
|
55
|
+
stdin: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Backend {
|
|
59
|
+
name: BackendName;
|
|
60
|
+
/** Absolute path to this backend's wrapper script on the sprite */
|
|
61
|
+
wrapperPath: string;
|
|
62
|
+
/**
|
|
63
|
+
* Build argv + env + stdin primitives for one turn of this backend.
|
|
64
|
+
* The driver composes the final wrapper stdin as `envLines \n\n stdin`.
|
|
65
|
+
*/
|
|
66
|
+
buildTurn(input: BuildTurnInput): BuildTurnResult;
|
|
67
|
+
/** Stateful translator, created fresh per turn */
|
|
68
|
+
createTranslator(opts: TranslatorOptions): Translator;
|
|
69
|
+
/**
|
|
70
|
+
* Install / verify the backend binary + wrapper on a freshly-created sprite.
|
|
71
|
+
* Safe to call multiple times (idempotent via sentinels).
|
|
72
|
+
*/
|
|
73
|
+
prepareOnSprite(spriteName: string, provider: ContainerProvider): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Agent-create-time validation: return an error message if this backend
|
|
76
|
+
* cannot run with the current config (e.g. opencode + no API key).
|
|
77
|
+
*/
|
|
78
|
+
validateAgentCreation?(): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* First-turn runtime validation: belt-and-braces check that the backend
|
|
81
|
+
* can run NOW (config may have changed since agent create). Called from
|
|
82
|
+
* the driver before acquireForFirstTurn.
|
|
83
|
+
*/
|
|
84
|
+
validateRuntime?(): string | null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config cascade: env → settings table → defaults, 30s cache.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by
|
|
5
|
+
*/
|
|
6
|
+
import { getDb } from "../db/client";
|
|
7
|
+
import { nowMs } from "../util/clock";
|
|
8
|
+
|
|
9
|
+
export interface Config {
|
|
10
|
+
spriteToken: string | undefined;
|
|
11
|
+
spriteApi: string;
|
|
12
|
+
anthropicApiKey: string | undefined;
|
|
13
|
+
claudeToken: string | undefined;
|
|
14
|
+
openAiApiKey: string | undefined;
|
|
15
|
+
geminiApiKey: string | undefined;
|
|
16
|
+
factoryApiKey: string | undefined;
|
|
17
|
+
defaultModel: string;
|
|
18
|
+
agentMaxTurns: number;
|
|
19
|
+
agentTimeoutMs: number;
|
|
20
|
+
spriteTimeoutMs: number;
|
|
21
|
+
concurrency: number;
|
|
22
|
+
maxSpritesPerEnv: number;
|
|
23
|
+
sessionMaxAgeMs: number;
|
|
24
|
+
sweeperIntervalMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type GlobalCache = typeof globalThis & {
|
|
28
|
+
__caConfigCache?: { at: number; value: Config };
|
|
29
|
+
};
|
|
30
|
+
const g = globalThis as GlobalCache;
|
|
31
|
+
|
|
32
|
+
const CACHE_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
function readSetting(key: string): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const db = getDb();
|
|
37
|
+
const row = db
|
|
38
|
+
.prepare(
|
|
39
|
+
`SELECT value FROM settings WHERE key = ?`,
|
|
40
|
+
)
|
|
41
|
+
.get(key) as { value: string | null } | undefined;
|
|
42
|
+
return row?.value ?? undefined;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function num(env: string | undefined, fallback: number): number {
|
|
49
|
+
if (env == null) return fallback;
|
|
50
|
+
const n = Number(env);
|
|
51
|
+
return Number.isFinite(n) ? n : fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadConfig(): Config {
|
|
55
|
+
return {
|
|
56
|
+
spriteToken: process.env.SPRITE_TOKEN || readSetting("sprite_token"),
|
|
57
|
+
spriteApi:
|
|
58
|
+
process.env.SPRITE_API || readSetting("sprite_api") || "https://api.sprites.dev",
|
|
59
|
+
anthropicApiKey:
|
|
60
|
+
process.env.ANTHROPIC_API_KEY || readSetting("anthropic_api_key"),
|
|
61
|
+
claudeToken:
|
|
62
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN || readSetting("claude_token"),
|
|
63
|
+
openAiApiKey:
|
|
64
|
+
process.env.OPENAI_API_KEY || readSetting("openai_api_key"),
|
|
65
|
+
geminiApiKey:
|
|
66
|
+
process.env.GEMINI_API_KEY || readSetting("gemini_api_key") || undefined,
|
|
67
|
+
factoryApiKey:
|
|
68
|
+
process.env.FACTORY_API_KEY || readSetting("factory_api_key") || undefined,
|
|
69
|
+
defaultModel:
|
|
70
|
+
process.env.DEFAULT_MODEL ||
|
|
71
|
+
readSetting("default_model") ||
|
|
72
|
+
"claude-sonnet-4-6",
|
|
73
|
+
agentMaxTurns: num(process.env.AGENT_MAX_TURNS, 10),
|
|
74
|
+
agentTimeoutMs: num(process.env.AGENT_TIMEOUT_MS, 600_000),
|
|
75
|
+
spriteTimeoutMs: num(process.env.SPRITE_TIMEOUT_MS, 30_000),
|
|
76
|
+
concurrency: num(process.env.CONCURRENCY, 4),
|
|
77
|
+
maxSpritesPerEnv: num(process.env.MAX_SPRITES_PER_ENV, 8),
|
|
78
|
+
sessionMaxAgeMs: num(process.env.SESSION_MAX_AGE_MS, 7 * 24 * 3600 * 1000),
|
|
79
|
+
sweeperIntervalMs: num(process.env.SWEEPER_INTERVAL_MS, 60_000),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getConfig(): Config {
|
|
84
|
+
const now = nowMs();
|
|
85
|
+
if (g.__caConfigCache && now - g.__caConfigCache.at < CACHE_MS) {
|
|
86
|
+
return g.__caConfigCache.value;
|
|
87
|
+
}
|
|
88
|
+
const value = loadConfig();
|
|
89
|
+
g.__caConfigCache = { at: now, value };
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function invalidateConfigCache(): void {
|
|
94
|
+
g.__caConfigCache = undefined;
|
|
95
|
+
}
|