background-agents 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -364
- package/app/assets/icon.png +0 -0
- package/app/assets/tray-icon.png +0 -0
- package/app/assets/tray-icon.svg +4 -0
- package/app/dist/main.js +932 -0
- package/app/dist/preload.cjs +70 -0
- package/bin/background-agents.js +298 -0
- package/lib/ui.js +110 -0
- package/package.json +32 -47
- package/dist/debug.d.ts +0 -7
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js +0 -19
- package/dist/debug.js.map +0 -1
- package/dist/factory.d.ts +0 -31
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -47
- package/dist/factory.js.map +0 -1
- package/dist/index.d.ts +0 -30
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -35
- package/dist/index.js.map +0 -1
- package/dist/providers/base.d.ts +0 -160
- package/dist/providers/base.d.ts.map +0 -1
- package/dist/providers/base.js +0 -656
- package/dist/providers/base.js.map +0 -1
- package/dist/providers/claude.d.ts +0 -14
- package/dist/providers/claude.d.ts.map +0 -1
- package/dist/providers/claude.js +0 -122
- package/dist/providers/claude.js.map +0 -1
- package/dist/providers/codex.d.ts +0 -14
- package/dist/providers/codex.d.ts.map +0 -1
- package/dist/providers/codex.js +0 -160
- package/dist/providers/codex.js.map +0 -1
- package/dist/providers/gemini.d.ts +0 -16
- package/dist/providers/gemini.d.ts.map +0 -1
- package/dist/providers/gemini.js +0 -91
- package/dist/providers/gemini.js.map +0 -1
- package/dist/providers/index.d.ts +0 -6
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -6
- package/dist/providers/index.js.map +0 -1
- package/dist/providers/opencode.d.ts +0 -14
- package/dist/providers/opencode.d.ts.map +0 -1
- package/dist/providers/opencode.js +0 -100
- package/dist/providers/opencode.js.map +0 -1
- package/dist/sandbox/daytona-ssh.d.ts +0 -9
- package/dist/sandbox/daytona-ssh.d.ts.map +0 -1
- package/dist/sandbox/daytona-ssh.js +0 -113
- package/dist/sandbox/daytona-ssh.js.map +0 -1
- package/dist/sandbox/daytona.d.ts +0 -4
- package/dist/sandbox/daytona.d.ts.map +0 -1
- package/dist/sandbox/daytona.js +0 -238
- package/dist/sandbox/daytona.js.map +0 -1
- package/dist/sandbox/index.d.ts +0 -14
- package/dist/sandbox/index.d.ts.map +0 -1
- package/dist/sandbox/index.js +0 -15
- package/dist/sandbox/index.js.map +0 -1
- package/dist/session.d.ts +0 -64
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -113
- package/dist/session.js.map +0 -1
- package/dist/types/events.d.ts +0 -114
- package/dist/types/events.d.ts.map +0 -1
- package/dist/types/events.js +0 -50
- package/dist/types/events.js.map +0 -1
- package/dist/types/index.d.ts +0 -3
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/types/provider.d.ts +0 -124
- package/dist/types/provider.d.ts.map +0 -1
- package/dist/types/provider.js +0 -2
- package/dist/types/provider.js.map +0 -1
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -4
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/install.d.ts +0 -27
- package/dist/utils/install.d.ts.map +0 -1
- package/dist/utils/install.js +0 -86
- package/dist/utils/install.js.map +0 -1
- package/dist/utils/json.d.ts +0 -8
- package/dist/utils/json.d.ts.map +0 -1
- package/dist/utils/json.js +0 -15
- package/dist/utils/json.js.map +0 -1
- package/dist/utils/session.d.ts +0 -17
- package/dist/utils/session.d.ts.map +0 -1
- package/dist/utils/session.js +0 -59
- package/dist/utils/session.js.map +0 -1
- package/next.config.codeagentsdk.cjs +0 -22
package/dist/providers/base.js
DELETED
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import * as readline from "node:readline";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { debugLog } from "../debug.js";
|
|
5
|
-
import { getDefaultSessionPath, loadSession, storeSession } from "../utils/session.js";
|
|
6
|
-
import { ensureCliInstalled } from "../utils/install.js";
|
|
7
|
-
import { adaptSandbox } from "../sandbox/index.js";
|
|
8
|
-
/**
|
|
9
|
-
* Abstract base class for AI coding agent providers
|
|
10
|
-
*/
|
|
11
|
-
export class Provider {
|
|
12
|
-
sessionId = null;
|
|
13
|
-
getSessionId() {
|
|
14
|
-
return this.sessionId;
|
|
15
|
-
}
|
|
16
|
-
/** Sandbox for secure execution */
|
|
17
|
-
sandboxManager = null;
|
|
18
|
-
/** Whether local execution is allowed */
|
|
19
|
-
allowLocalExecution = false;
|
|
20
|
-
/** Resolves when initial setup (install + env) has completed. */
|
|
21
|
-
_readyPromise = null;
|
|
22
|
-
/** Env passed at creation; used for setup and when run() omits env */
|
|
23
|
-
_creationEnv;
|
|
24
|
-
/** Defaults merged into every run (model, timeout, sessionId, env). Set by createSession. */
|
|
25
|
-
_runDefaults = {};
|
|
26
|
-
/** Tracks whether we've already applied a synthetic system prompt for this session. */
|
|
27
|
-
_systemPromptApplied = false;
|
|
28
|
-
get ready() {
|
|
29
|
-
return this._readyPromise ?? Promise.resolve();
|
|
30
|
-
}
|
|
31
|
-
constructor(options = {}) {
|
|
32
|
-
this._creationEnv = options.env;
|
|
33
|
-
this._runDefaults = options.runDefaults ?? {};
|
|
34
|
-
if (options.sandbox) {
|
|
35
|
-
this.sandboxManager = adaptSandbox(options.sandbox, { env: options.env });
|
|
36
|
-
if (!options.skipInstall) {
|
|
37
|
-
this._readyPromise = new Promise((resolve, reject) => {
|
|
38
|
-
queueMicrotask(() => this._doSetup().then(resolve).catch(reject));
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else if (options.dangerouslyAllowLocalExecution) {
|
|
43
|
-
this.allowLocalExecution = true;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
throw new Error("Provider requires either a sandbox or dangerouslyAllowLocalExecution: true. " +
|
|
47
|
-
"For secure execution, create a sandbox with @daytonaio/sdk and pass it in:\n\n" +
|
|
48
|
-
" import { Daytona } from '@daytonaio/sdk'\n" +
|
|
49
|
-
" import { createProvider } from 'background-agents'\n" +
|
|
50
|
-
" const daytona = new Daytona({ apiKey: '...' })\n" +
|
|
51
|
-
" const sandbox = await daytona.create({ envVars: { ANTHROPIC_API_KEY: '...' } })\n" +
|
|
52
|
-
" const provider = createProvider('claude', { sandbox })\n\n" +
|
|
53
|
-
"For local execution (dangerous), use:\n\n" +
|
|
54
|
-
" const provider = createProvider('claude', { dangerouslyAllowLocalExecution: true })");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Apply a synthetic system prompt for providers without native support by
|
|
59
|
-
* prepending it to the first user prompt in the session. Claude uses its
|
|
60
|
-
* native CLI flag instead, so we leave the prompt unchanged there.
|
|
61
|
-
*/
|
|
62
|
-
_applySystemPrompt(options) {
|
|
63
|
-
if (options.systemPrompt && !this._systemPromptApplied) {
|
|
64
|
-
const supportsNativeSystemPrompt = this.name === "claude";
|
|
65
|
-
if (!supportsNativeSystemPrompt) {
|
|
66
|
-
const basePrompt = options.prompt ?? "";
|
|
67
|
-
options.prompt = basePrompt
|
|
68
|
-
? `${options.systemPrompt}\n\n${basePrompt}`
|
|
69
|
-
: options.systemPrompt;
|
|
70
|
-
}
|
|
71
|
-
this._systemPromptApplied = true;
|
|
72
|
-
}
|
|
73
|
-
return options;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Run the provider and yield events. Pass a prompt string or full RunOptions.
|
|
77
|
-
* When created via createSession, runDefaults are merged in (e.g. model, timeout).
|
|
78
|
-
*/
|
|
79
|
-
async *run(promptOrOptions = {}) {
|
|
80
|
-
let options = typeof promptOrOptions === "string"
|
|
81
|
-
? { ...this._runDefaults, prompt: promptOrOptions }
|
|
82
|
-
: { ...this._runDefaults, ...promptOrOptions };
|
|
83
|
-
options = this._applySystemPrompt(options);
|
|
84
|
-
debugLog(`run start provider=${this.name} promptLength=${options.prompt?.length ?? 0}`, this.sessionId);
|
|
85
|
-
if (this.sandboxManager) {
|
|
86
|
-
yield* this.runSandbox(options);
|
|
87
|
-
}
|
|
88
|
-
else if (this.allowLocalExecution) {
|
|
89
|
-
yield* this.runLocal(options);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
throw new Error("No execution mode configured");
|
|
93
|
-
}
|
|
94
|
-
debugLog(`run end provider=${this.name}`, this.sessionId);
|
|
95
|
-
}
|
|
96
|
-
async _codexLoginIfNeeded(env) {
|
|
97
|
-
if (this.name !== "codex" ||
|
|
98
|
-
!env?.OPENAI_API_KEY ||
|
|
99
|
-
!this.sandboxManager?.executeCommand)
|
|
100
|
-
return;
|
|
101
|
-
const safeKey = env.OPENAI_API_KEY.replace(/'/g, "'\\''");
|
|
102
|
-
await this.sandboxManager.executeCommand(`echo '${safeKey}' | codex login --with-api-key 2>&1`, 30);
|
|
103
|
-
}
|
|
104
|
-
/** One-time setup: install CLI and set env. Run in microtask so subclass name is set. */
|
|
105
|
-
async _doSetup() {
|
|
106
|
-
if (!this.sandboxManager)
|
|
107
|
-
return;
|
|
108
|
-
const t = Date.now();
|
|
109
|
-
await this.sandboxManager.ensureProvider(this.name);
|
|
110
|
-
console.log(`[timing] ensureProvider(${this.name}) took ${Date.now() - t}ms`);
|
|
111
|
-
if (this._creationEnv)
|
|
112
|
-
this.sandboxManager.setEnvVars(this._creationEnv);
|
|
113
|
-
}
|
|
114
|
-
/** Per-run: set env and Codex login. */
|
|
115
|
-
async _applyRunEnv(options) {
|
|
116
|
-
if (!this.sandboxManager)
|
|
117
|
-
return;
|
|
118
|
-
const env = options.env ?? this._creationEnv;
|
|
119
|
-
if (env)
|
|
120
|
-
this.sandboxManager.setEnvVars(env);
|
|
121
|
-
await this._codexLoginIfNeeded(env);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Run in a secure Daytona sandbox
|
|
125
|
-
*/
|
|
126
|
-
async *runSandbox(options) {
|
|
127
|
-
if (!this.sandboxManager) {
|
|
128
|
-
throw new Error("Sandbox manager not configured");
|
|
129
|
-
}
|
|
130
|
-
await (this._readyPromise ?? Promise.resolve());
|
|
131
|
-
await this._applyRunEnv(options);
|
|
132
|
-
// Build the command
|
|
133
|
-
const { cmd, args, env: cmdEnv } = this.getCommand(options);
|
|
134
|
-
// Set command-specific env vars
|
|
135
|
-
if (cmdEnv) {
|
|
136
|
-
this.sandboxManager.setEnvVars(cmdEnv);
|
|
137
|
-
}
|
|
138
|
-
// Build full command string
|
|
139
|
-
const fullCommand = [cmd, ...args.map(arg => arg.includes(" ") || arg.includes('"') || arg.includes("'")
|
|
140
|
-
? `'${arg.replace(/'/g, "'\\''")}'`
|
|
141
|
-
: arg)].join(" ");
|
|
142
|
-
const timeout = options.timeout ?? 120;
|
|
143
|
-
debugLog(`runSandbox command start provider=${this.name} timeout=${timeout}`, this.sessionId);
|
|
144
|
-
debugLog("runSandbox cli", this.sessionId, fullCommand);
|
|
145
|
-
let pendingToolEnd = false;
|
|
146
|
-
for await (const line of this.sandboxManager.executeCommandStream(fullCommand, timeout)) {
|
|
147
|
-
debugLog(`raw line (sandbox): ${line.length > 300 ? line.slice(0, 300) + "…" : line}`, this.sessionId);
|
|
148
|
-
const raw = this.parse(line);
|
|
149
|
-
if (raw === null) {
|
|
150
|
-
debugLog(`unparsed line (sandbox):`, this.sessionId, line);
|
|
151
|
-
}
|
|
152
|
-
const events = raw === null ? [] : Array.isArray(raw) ? raw : [raw];
|
|
153
|
-
for (const event of events) {
|
|
154
|
-
if (event.type === "session") {
|
|
155
|
-
this.sessionId = event.id;
|
|
156
|
-
}
|
|
157
|
-
if (event.type === "tool_start")
|
|
158
|
-
pendingToolEnd = true;
|
|
159
|
-
if (event.type === "tool_end")
|
|
160
|
-
pendingToolEnd = false;
|
|
161
|
-
if (event.type === "end" && pendingToolEnd) {
|
|
162
|
-
yield { type: "tool_end" };
|
|
163
|
-
pendingToolEnd = false;
|
|
164
|
-
}
|
|
165
|
-
if (event.type === "end") {
|
|
166
|
-
debugLog("session end", this.sessionId, event.error ? `reason=error ${event.error}` : "reason=completed");
|
|
167
|
-
}
|
|
168
|
-
else if (event.type === "agent_crashed") {
|
|
169
|
-
debugLog("session end", this.sessionId, "reason=crashed", event.message ?? event.output ?? "");
|
|
170
|
-
}
|
|
171
|
-
yield event;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
debugLog(`runSandbox stream ended provider=${this.name}`, this.sessionId);
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Run directly on local machine (dangerous - use with caution)
|
|
178
|
-
*/
|
|
179
|
-
async *runLocal(options) {
|
|
180
|
-
// Ensure CLI is installed locally
|
|
181
|
-
ensureCliInstalled(this.name, !(options.skipInstall ?? false));
|
|
182
|
-
// Load session from file if not provided and persistence is enabled
|
|
183
|
-
const sessionFile = options.sessionFile ?? getDefaultSessionPath(this.name);
|
|
184
|
-
if (options.sessionId) {
|
|
185
|
-
this.sessionId = options.sessionId;
|
|
186
|
-
}
|
|
187
|
-
else if (options.persistSession !== false) {
|
|
188
|
-
this.sessionId = loadSession(sessionFile);
|
|
189
|
-
}
|
|
190
|
-
const { cmd, args, env: cmdEnv } = this.getCommand(options);
|
|
191
|
-
const cliCommand = [cmd, ...args].join(" ");
|
|
192
|
-
debugLog("runLocal cli", this.sessionId, cliCommand);
|
|
193
|
-
const proc = spawn(cmd, args, {
|
|
194
|
-
stdio: ["inherit", "pipe", "inherit"],
|
|
195
|
-
cwd: options.cwd,
|
|
196
|
-
env: {
|
|
197
|
-
...process.env,
|
|
198
|
-
...cmdEnv,
|
|
199
|
-
...options.env,
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
const rl = readline.createInterface({ input: proc.stdout });
|
|
203
|
-
let pendingToolEnd = false;
|
|
204
|
-
debugLog(`runLocal started provider=${this.name}`, this.sessionId);
|
|
205
|
-
for await (const line of rl) {
|
|
206
|
-
debugLog(`raw line (local): ${line.length > 300 ? line.slice(0, 300) + "…" : line}`, this.sessionId);
|
|
207
|
-
const raw = this.parse(line);
|
|
208
|
-
if (raw === null) {
|
|
209
|
-
debugLog(`unparsed line (local):`, this.sessionId, line);
|
|
210
|
-
}
|
|
211
|
-
const events = raw === null ? [] : Array.isArray(raw) ? raw : [raw];
|
|
212
|
-
for (const event of events) {
|
|
213
|
-
if (event.type === "session") {
|
|
214
|
-
this.sessionId = event.id;
|
|
215
|
-
if (options.persistSession !== false) {
|
|
216
|
-
storeSession(sessionFile, event.id);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (event.type === "tool_start")
|
|
220
|
-
pendingToolEnd = true;
|
|
221
|
-
if (event.type === "tool_end")
|
|
222
|
-
pendingToolEnd = false;
|
|
223
|
-
if (event.type === "end" && pendingToolEnd) {
|
|
224
|
-
yield { type: "tool_end" };
|
|
225
|
-
pendingToolEnd = false;
|
|
226
|
-
}
|
|
227
|
-
if (event.type === "end") {
|
|
228
|
-
debugLog("session end", this.sessionId, event.error ? `reason=error ${event.error}` : "reason=completed");
|
|
229
|
-
}
|
|
230
|
-
else if (event.type === "agent_crashed") {
|
|
231
|
-
debugLog("session end", this.sessionId, "reason=crashed", event.message ?? event.output ?? "");
|
|
232
|
-
}
|
|
233
|
-
yield event;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Wait for process to close
|
|
237
|
-
await new Promise((resolve, reject) => {
|
|
238
|
-
proc.on("close", (code) => {
|
|
239
|
-
debugLog(`runLocal process closed provider=${this.name} code=${code}`, this.sessionId);
|
|
240
|
-
if (code != null && code !== 0) {
|
|
241
|
-
debugLog("session end", this.sessionId, `reason=process exited with code ${code}`);
|
|
242
|
-
reject(new Error(`Provider process exited with code ${code}`));
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
resolve();
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
proc.on("error", reject);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
/** Meta stored in sandbox for background session (one log per turn, cursor in meta). */
|
|
252
|
-
async readSandboxMeta(sessionDir) {
|
|
253
|
-
if (!this.sandboxManager?.executeCommand)
|
|
254
|
-
return null;
|
|
255
|
-
const result = await this.sandboxManager.executeCommand(`cat "${sessionDir}/meta.json" 2>/dev/null || true`, 10);
|
|
256
|
-
const raw = (result.output ?? "").trim();
|
|
257
|
-
if (!raw)
|
|
258
|
-
return null;
|
|
259
|
-
try {
|
|
260
|
-
const o = JSON.parse(raw);
|
|
261
|
-
if (typeof o.currentTurn !== "number" || typeof o.cursor !== "number")
|
|
262
|
-
return null;
|
|
263
|
-
return {
|
|
264
|
-
currentTurn: o.currentTurn,
|
|
265
|
-
cursor: o.cursor,
|
|
266
|
-
rawCursor: o.rawCursor,
|
|
267
|
-
pid: o.pid,
|
|
268
|
-
runId: o.runId,
|
|
269
|
-
outputFile: o.outputFile,
|
|
270
|
-
sawEnd: o.sawEnd,
|
|
271
|
-
startedAt: o.startedAt,
|
|
272
|
-
provider: o.provider,
|
|
273
|
-
sessionId: o.sessionId ?? null,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
/** Write initial session meta at creation so getBackgroundSession can reattach before first start(). */
|
|
281
|
-
async writeInitialSessionMeta(sessionDir) {
|
|
282
|
-
if (!this.sandboxManager?.executeCommand)
|
|
283
|
-
return;
|
|
284
|
-
await this.writeSandboxMeta(sessionDir, {
|
|
285
|
-
currentTurn: 0,
|
|
286
|
-
cursor: 0,
|
|
287
|
-
provider: this.name,
|
|
288
|
-
sessionId: this.sessionId ?? null,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
async writeSandboxMeta(sessionDir, meta) {
|
|
292
|
-
if (!this.sandboxManager?.executeCommand) {
|
|
293
|
-
throw new Error("Sandbox background mode requires a sandbox with executeCommand support");
|
|
294
|
-
}
|
|
295
|
-
const json = JSON.stringify(meta);
|
|
296
|
-
const b64 = Buffer.from(json, "utf8").toString("base64");
|
|
297
|
-
await this.sandboxManager.executeCommand(`mkdir -p "${sessionDir}" && echo '${b64}' | base64 -d > "${sessionDir}/meta.json"`, 10);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Start a new turn in a session directory: one log file per turn, meta in sandbox.
|
|
301
|
-
* Uses currentTurn for this run; currentTurn is incremented when the turn ends (in getEvents).
|
|
302
|
-
* Writes meta with runId/outputFile before starting so reattaching clients see "running" immediately.
|
|
303
|
-
*/
|
|
304
|
-
async startSandboxBackgroundTurn(sessionDir, options) {
|
|
305
|
-
const t0 = Date.now();
|
|
306
|
-
if (!this.sandboxManager?.executeCommand) {
|
|
307
|
-
throw new Error("Sandbox background mode requires a sandbox with executeCommand support");
|
|
308
|
-
}
|
|
309
|
-
await this.sandboxManager.executeCommand(`mkdir -p "${sessionDir}"`, 10);
|
|
310
|
-
console.log(`[timing] mkdir took ${Date.now() - t0}ms (elapsed ${Date.now() - t0}ms)`);
|
|
311
|
-
let t = Date.now();
|
|
312
|
-
const meta = await this.readSandboxMeta(sessionDir);
|
|
313
|
-
console.log(`[timing] readSandboxMeta took ${Date.now() - t}ms (elapsed ${Date.now() - t0}ms)`);
|
|
314
|
-
const currentTurn = meta?.currentTurn ?? 0;
|
|
315
|
-
const outputFile = `${sessionDir}/${currentTurn}.jsonl`;
|
|
316
|
-
const runId = randomUUID().slice(0, 8);
|
|
317
|
-
debugLog(`background turn start provider=${this.name} sessionDir=${sessionDir} turn=${currentTurn} outputFile=${outputFile}`, this.sessionId);
|
|
318
|
-
t = Date.now();
|
|
319
|
-
await this.writeSandboxMeta(sessionDir, {
|
|
320
|
-
currentTurn,
|
|
321
|
-
cursor: 0,
|
|
322
|
-
runId,
|
|
323
|
-
outputFile,
|
|
324
|
-
startedAt: new Date().toISOString(),
|
|
325
|
-
provider: this.name,
|
|
326
|
-
sessionId: this.sessionId ?? options.sessionId ?? meta?.sessionId ?? null,
|
|
327
|
-
});
|
|
328
|
-
console.log(`[timing] writeSandboxMeta(1) took ${Date.now() - t}ms (elapsed ${Date.now() - t0}ms)`);
|
|
329
|
-
t = Date.now();
|
|
330
|
-
const result = await this.startSandboxBackground({ ...options, outputFile, runId });
|
|
331
|
-
console.log(`[timing] startSandboxBackground took ${Date.now() - t}ms (elapsed ${Date.now() - t0}ms)`);
|
|
332
|
-
debugLog(`background turn started provider=${this.name} pid=${result.pid} executionId=${result.executionId}`, this.sessionId);
|
|
333
|
-
t = Date.now();
|
|
334
|
-
await this.writeSandboxMeta(sessionDir, {
|
|
335
|
-
currentTurn,
|
|
336
|
-
cursor: 0,
|
|
337
|
-
pid: result.pid,
|
|
338
|
-
runId: result.runId,
|
|
339
|
-
outputFile,
|
|
340
|
-
startedAt: new Date().toISOString(),
|
|
341
|
-
provider: this.name,
|
|
342
|
-
sessionId: this.sessionId ?? options.sessionId ?? meta?.sessionId ?? null,
|
|
343
|
-
});
|
|
344
|
-
console.log(`[timing] writeSandboxMeta(2) took ${Date.now() - t}ms (elapsed ${Date.now() - t0}ms)`);
|
|
345
|
-
return { executionId: result.executionId, pid: result.pid, outputFile };
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Get the current turn's process id from sandbox meta, or null if no run in progress.
|
|
349
|
-
*/
|
|
350
|
-
async getSandboxBackgroundPid(sessionDir) {
|
|
351
|
-
const meta = await this.readSandboxMeta(sessionDir);
|
|
352
|
-
if (meta?.pid == null || meta.pid < 1)
|
|
353
|
-
return null;
|
|
354
|
-
return meta.pid;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Cancel the current turn's process in the sandbox (kill pid from meta).
|
|
358
|
-
* Uses killBackgroundProcess when available (same channel as start, e.g. SSH); else executeCommand.
|
|
359
|
-
* Writes the done file after kill so isRunning() becomes false (the wrapper never gets to write it).
|
|
360
|
-
*/
|
|
361
|
-
async cancelSandboxBackground(sessionDir) {
|
|
362
|
-
const meta = await this.readSandboxMeta(sessionDir);
|
|
363
|
-
if (meta?.pid == null)
|
|
364
|
-
return;
|
|
365
|
-
const mgr = this.sandboxManager;
|
|
366
|
-
if (mgr?.killBackgroundProcess) {
|
|
367
|
-
await mgr.killBackgroundProcess(meta.pid);
|
|
368
|
-
}
|
|
369
|
-
else if (mgr?.executeCommand) {
|
|
370
|
-
await mgr.executeCommand(`kill ${meta.pid} 2>/dev/null || true`, 10);
|
|
371
|
-
}
|
|
372
|
-
if (meta.outputFile && mgr?.executeCommand) {
|
|
373
|
-
const donePath = meta.outputFile + ".done";
|
|
374
|
-
const escaped = donePath.replace(/'/g, "'\\''");
|
|
375
|
-
await mgr.executeCommand(`echo 1 > '${escaped}' 2>/dev/null || true`, 10);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Check if the current turn's process is still running in the sandbox.
|
|
380
|
-
* True only while a turn is in progress; false until the next turn starts.
|
|
381
|
-
* Uses the done file (outputFile.done): the wrapper writes it when the command exits.
|
|
382
|
-
* We don't use kill -0 / ps here because the process is started over SSH; the process API
|
|
383
|
-
* runs in a different context and can't see that pid. Kill would only work over the same SSH.
|
|
384
|
-
*/
|
|
385
|
-
async isSandboxBackgroundProcessRunning(sessionDir) {
|
|
386
|
-
const meta = await this.readSandboxMeta(sessionDir);
|
|
387
|
-
if (!meta?.runId || !meta.outputFile || !this.sandboxManager?.executeCommand) {
|
|
388
|
-
debugLog(`isRunning false (no run) sessionDir=${sessionDir}`, this.sessionId);
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
const donePath = meta.outputFile + ".done";
|
|
392
|
-
const escaped = donePath.replace(/'/g, "'\\''");
|
|
393
|
-
const r = await this.sandboxManager.executeCommand(`test -f '${escaped}' 2>/dev/null; echo $?`, 10);
|
|
394
|
-
const doneExists = Number((r.output ?? "").trim().split(/\s+/).pop()) === 0;
|
|
395
|
-
const running = !doneExists;
|
|
396
|
-
debugLog(`isRunning ${running} (done file ${doneExists ? "exists" : "missing"}) sessionDir=${sessionDir}`, this.sessionId);
|
|
397
|
-
return running;
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Get new events for the current turn; reads and updates cursor in sandbox meta.
|
|
401
|
-
* Use isSandboxBackgroundProcessRunning() to check if the agent is still running.
|
|
402
|
-
*/
|
|
403
|
-
async getEventsSandboxBackgroundFromMeta(sessionDir) {
|
|
404
|
-
const meta = await this.readSandboxMeta(sessionDir);
|
|
405
|
-
if (!meta?.runId || !meta.outputFile) {
|
|
406
|
-
debugLog(`getEventsSandboxBackgroundFromMeta provider=${this.name} sessionDir=${sessionDir} (no turn in progress)`, this.sessionId);
|
|
407
|
-
return {
|
|
408
|
-
sessionId: meta?.sessionId ?? this.sessionId ?? null,
|
|
409
|
-
events: [],
|
|
410
|
-
cursor: String(meta?.cursor ?? 0),
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const outputFile = meta.outputFile;
|
|
414
|
-
const cursor = String(meta.cursor);
|
|
415
|
-
debugLog(`getEventsSandboxBackgroundFromMeta provider=${this.name} sessionDir=${sessionDir} turn=${meta.currentTurn} cursor=${cursor}`, this.sessionId);
|
|
416
|
-
const result = await this.pollSandboxBackground(outputFile, cursor, meta.rawCursor != null ? String(meta.rawCursor) : null);
|
|
417
|
-
const sawEnd = meta.sawEnd || result.events.some((e) => e.type === "end");
|
|
418
|
-
const stillRunning = await this.isSandboxBackgroundProcessRunning(sessionDir);
|
|
419
|
-
// Clear run when process stopped or we've seen end event (sawEnd can be true before .done file exists)
|
|
420
|
-
if (!stillRunning || sawEnd) {
|
|
421
|
-
const nextTurn = (meta.currentTurn ?? 0) + 1;
|
|
422
|
-
const metaUpdate = {
|
|
423
|
-
currentTurn: nextTurn,
|
|
424
|
-
cursor: Number(result.cursor) || 0,
|
|
425
|
-
rawCursor: Number(result.rawCursor) || meta.rawCursor || 0,
|
|
426
|
-
sawEnd,
|
|
427
|
-
provider: this.name,
|
|
428
|
-
sessionId: this.sessionId ?? meta.sessionId ?? null,
|
|
429
|
-
...(sawEnd ? {} : { outputFile: meta.outputFile, runId: meta.runId }),
|
|
430
|
-
};
|
|
431
|
-
await this.writeSandboxMeta(sessionDir, metaUpdate);
|
|
432
|
-
}
|
|
433
|
-
if (stillRunning && !sawEnd) {
|
|
434
|
-
await this.writeSandboxMeta(sessionDir, {
|
|
435
|
-
currentTurn: meta.currentTurn,
|
|
436
|
-
cursor: Number(result.cursor) || 0,
|
|
437
|
-
rawCursor: Number(result.rawCursor) || meta.rawCursor || 0,
|
|
438
|
-
pid: meta.pid,
|
|
439
|
-
runId: meta.runId,
|
|
440
|
-
outputFile: meta.outputFile,
|
|
441
|
-
sawEnd,
|
|
442
|
-
startedAt: meta.startedAt,
|
|
443
|
-
provider: this.name,
|
|
444
|
-
sessionId: this.sessionId,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
if (!stillRunning && !sawEnd) {
|
|
448
|
-
const maxOutputChars = 4096;
|
|
449
|
-
let output;
|
|
450
|
-
if (this.sandboxManager?.executeCommand) {
|
|
451
|
-
const outResult = await this.sandboxManager.executeCommand(`cat ${outputFile} 2>/dev/null || true`, 10);
|
|
452
|
-
const raw = (outResult.output ?? "").trim();
|
|
453
|
-
const nonJsonLines = raw.split("\n").filter((line) => {
|
|
454
|
-
const t = line.trim();
|
|
455
|
-
return t && !(t.startsWith("{") && t.endsWith("}"));
|
|
456
|
-
});
|
|
457
|
-
const filtered = nonJsonLines.join("\n").trim();
|
|
458
|
-
output = filtered.length > maxOutputChars ? filtered.slice(-maxOutputChars) : filtered || undefined;
|
|
459
|
-
}
|
|
460
|
-
const crashEvent = {
|
|
461
|
-
type: "agent_crashed",
|
|
462
|
-
message: "Agent process exited without completing (crashed or killed)",
|
|
463
|
-
output,
|
|
464
|
-
};
|
|
465
|
-
debugLog("session end", this.sessionId ?? meta.sessionId, "reason=crashed", crashEvent.message, output ? `output=${output.slice(0, 200)}${output.length > 200 ? "…" : ""}` : "");
|
|
466
|
-
const nextTurn = (meta.currentTurn ?? 0) + 1;
|
|
467
|
-
await this.writeSandboxMeta(sessionDir, {
|
|
468
|
-
currentTurn: nextTurn,
|
|
469
|
-
cursor: Number(result.cursor) || 0,
|
|
470
|
-
rawCursor: Number(result.rawCursor) || meta.rawCursor || 0,
|
|
471
|
-
sawEnd: true,
|
|
472
|
-
provider: this.name,
|
|
473
|
-
sessionId: this.sessionId ?? meta.sessionId ?? null,
|
|
474
|
-
});
|
|
475
|
-
return { sessionId: result.sessionId, events: [...result.events, crashEvent], cursor: result.cursor };
|
|
476
|
-
}
|
|
477
|
-
return { sessionId: result.sessionId, events: result.events, cursor: result.cursor };
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Start a background run inside the sandbox.
|
|
481
|
-
* The CLI is run with stdout redirected to an append-only JSONL log file.
|
|
482
|
-
* Later you can call pollSandboxBackground(outputFile, cursor) to consume new events.
|
|
483
|
-
* If options.runId is provided (e.g. from startSandboxBackgroundTurn), it is used so meta can be written before this returns.
|
|
484
|
-
*/
|
|
485
|
-
async startSandboxBackground(options) {
|
|
486
|
-
if (!this.sandboxManager || !this.sandboxManager.executeCommand) {
|
|
487
|
-
throw new Error("Sandbox background mode requires a sandbox with executeCommand support");
|
|
488
|
-
}
|
|
489
|
-
const t0 = Date.now();
|
|
490
|
-
let t = Date.now();
|
|
491
|
-
await (this._readyPromise ?? Promise.resolve());
|
|
492
|
-
console.log(`[timing] _readyPromise took ${Date.now() - t}ms`);
|
|
493
|
-
t = Date.now();
|
|
494
|
-
const optsWithSystem = this._applySystemPrompt(options);
|
|
495
|
-
await this._applyRunEnv(optsWithSystem);
|
|
496
|
-
console.log(`[timing] _applyRunEnv took ${Date.now() - t}ms`);
|
|
497
|
-
const { cmd, args, env: cmdEnv } = this.getCommand(optsWithSystem);
|
|
498
|
-
if (cmdEnv) {
|
|
499
|
-
this.sandboxManager.setEnvVars(cmdEnv);
|
|
500
|
-
}
|
|
501
|
-
const fullCommand = [cmd, ...args.map(arg => arg.includes(" ") || arg.includes('"') || arg.includes("'")
|
|
502
|
-
? `'${arg.replace(/'/g, "'\\''")}'`
|
|
503
|
-
: arg)].join(" ");
|
|
504
|
-
const runId = options.runId ?? randomUUID().slice(0, 8);
|
|
505
|
-
const timeout = options.timeout ?? 30;
|
|
506
|
-
if (typeof this.sandboxManager.executeBackground !== "function") {
|
|
507
|
-
throw new Error("Background sessions require a sandbox with executeBackground (e.g. Daytona sandbox with createSshAccess()).");
|
|
508
|
-
}
|
|
509
|
-
debugLog(`startSandboxBackground executing provider=${this.name} outputFile=${options.outputFile} runId=${runId}`, this.sessionId);
|
|
510
|
-
debugLog("startSandboxBackground cli", this.sessionId, fullCommand);
|
|
511
|
-
t = Date.now();
|
|
512
|
-
const result = await this.sandboxManager.executeBackground({
|
|
513
|
-
command: fullCommand,
|
|
514
|
-
outputFile: options.outputFile,
|
|
515
|
-
runId,
|
|
516
|
-
timeout,
|
|
517
|
-
});
|
|
518
|
-
console.log(`[timing] executeBackground took ${Date.now() - t}ms (total startSandboxBackground ${Date.now() - t0}ms)`);
|
|
519
|
-
const pid = result.pid;
|
|
520
|
-
debugLog(`startSandboxBackground done provider=${this.name} pid=${pid}`, this.sessionId);
|
|
521
|
-
const executionId = randomUUID();
|
|
522
|
-
return {
|
|
523
|
-
executionId,
|
|
524
|
-
pid,
|
|
525
|
-
runId,
|
|
526
|
-
outputFile: options.outputFile,
|
|
527
|
-
cursor: "0",
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Poll a background sandbox run by reading the JSONL log file.
|
|
532
|
-
* Cursor is an opaque string representing the last processed line index.
|
|
533
|
-
*/
|
|
534
|
-
async pollSandboxBackground(outputFile, cursor, rawCursor) {
|
|
535
|
-
if (!this.sandboxManager || !this.sandboxManager.executeCommand) {
|
|
536
|
-
throw new Error("Sandbox background mode requires a sandbox with executeCommand support");
|
|
537
|
-
}
|
|
538
|
-
const decodeCursor = (c) => (c ? Number(c) || 0 : 0);
|
|
539
|
-
const encodeCursor = (index) => String(index);
|
|
540
|
-
const startIndex = decodeCursor(cursor);
|
|
541
|
-
const rawStartIndex = decodeCursor(rawCursor);
|
|
542
|
-
const catCommand = `cat ${outputFile}`;
|
|
543
|
-
const result = await this.sandboxManager.executeCommand(catCommand, 30);
|
|
544
|
-
const rawOutput = result.output ?? "";
|
|
545
|
-
const rawLines = rawOutput.split("\n");
|
|
546
|
-
debugLog(`pollSandboxBackground provider=${this.name} outputFile=${outputFile} cursor=${cursor ?? "null"} rawLines=${rawLines.length}`, this.sessionId);
|
|
547
|
-
const lines = [];
|
|
548
|
-
const isJsonLine = (line) => {
|
|
549
|
-
const trimmed = line.trim();
|
|
550
|
-
return trimmed.startsWith("{") && trimmed.endsWith("}");
|
|
551
|
-
};
|
|
552
|
-
const isRereading = rawStartIndex >= rawLines.length;
|
|
553
|
-
for (let i = 0; i < rawLines.length; i++) {
|
|
554
|
-
const line = rawLines[i];
|
|
555
|
-
if (i >= rawStartIndex) {
|
|
556
|
-
debugLog(`raw file line [${i}]: ${line}`, this.sessionId);
|
|
557
|
-
}
|
|
558
|
-
const trimmed = line.trim();
|
|
559
|
-
if (!trimmed)
|
|
560
|
-
continue;
|
|
561
|
-
if (!isJsonLine(trimmed) && i === rawLines.length - 1) {
|
|
562
|
-
if (!isRereading)
|
|
563
|
-
debugLog(`background poll skipped (partial last line) [${i}]: ${trimmed}`, this.sessionId);
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
if (isJsonLine(trimmed)) {
|
|
567
|
-
lines.push(trimmed);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
if (!isRereading)
|
|
571
|
-
debugLog(`background poll skipped (not JSONL) [${i}]: ${trimmed}`, this.sessionId);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (startIndex >= lines.length) {
|
|
575
|
-
// No new JSONL events since the last cursor; keep status as running
|
|
576
|
-
// and only advance the cursor based on JSON lines, not raw lines.
|
|
577
|
-
const nextCursor = lines.length;
|
|
578
|
-
const nextRawCursor = encodeCursor(rawLines.length);
|
|
579
|
-
return {
|
|
580
|
-
status: "running",
|
|
581
|
-
sessionId: this.sessionId,
|
|
582
|
-
events: [],
|
|
583
|
-
cursor: encodeCursor(nextCursor),
|
|
584
|
-
rawCursor: nextRawCursor,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
const slice = lines.slice(startIndex);
|
|
588
|
-
for (let i = 0; i < slice.length; i++) {
|
|
589
|
-
const l = slice[i];
|
|
590
|
-
debugLog(`raw line (background poll) [${startIndex + i}]: ${l ?? ""}`, this.sessionId);
|
|
591
|
-
}
|
|
592
|
-
const eventsOut = [];
|
|
593
|
-
let status = "running";
|
|
594
|
-
for (const line of slice) {
|
|
595
|
-
const raw = this.parse(line);
|
|
596
|
-
if (raw === null) {
|
|
597
|
-
debugLog(`unparsed line (background poll):`, this.sessionId, line);
|
|
598
|
-
}
|
|
599
|
-
const events = raw === null ? [] : Array.isArray(raw) ? raw : [raw];
|
|
600
|
-
for (const event of events) {
|
|
601
|
-
if (event.type === "session") {
|
|
602
|
-
this.sessionId = event.id;
|
|
603
|
-
}
|
|
604
|
-
if (event.type === "end") {
|
|
605
|
-
status = "completed";
|
|
606
|
-
debugLog("session end", this.sessionId, event.error ? `reason=error ${event.error}` : "reason=completed");
|
|
607
|
-
}
|
|
608
|
-
else if (event.type === "agent_crashed") {
|
|
609
|
-
debugLog("session end", this.sessionId, "reason=crashed", event.message ?? event.output ?? "");
|
|
610
|
-
}
|
|
611
|
-
eventsOut.push(event);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const newCursor = encodeCursor(lines.length);
|
|
615
|
-
const newRawCursor = encodeCursor(rawLines.length);
|
|
616
|
-
debugLog(`pollSandboxBackground result provider=${this.name} status=${status} events=${eventsOut.length} newCursor=${newCursor} newRawCursor=${newRawCursor}`, this.sessionId);
|
|
617
|
-
return {
|
|
618
|
-
status,
|
|
619
|
-
sessionId: this.sessionId,
|
|
620
|
-
events: eventsOut,
|
|
621
|
-
cursor: newCursor,
|
|
622
|
-
rawCursor: newRawCursor,
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Run the provider with a callback for each event
|
|
627
|
-
*/
|
|
628
|
-
async runWithCallback(callback, promptOrOptions = {}) {
|
|
629
|
-
for await (const event of this.run(promptOrOptions)) {
|
|
630
|
-
await callback(event);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Collect all events from a run into an array
|
|
635
|
-
*/
|
|
636
|
-
async collectEvents(promptOrOptions = {}) {
|
|
637
|
-
const events = [];
|
|
638
|
-
for await (const event of this.run(promptOrOptions)) {
|
|
639
|
-
events.push(event);
|
|
640
|
-
}
|
|
641
|
-
return events;
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Collect the full text response from a run
|
|
645
|
-
*/
|
|
646
|
-
async collectText(promptOrOptions = {}) {
|
|
647
|
-
let text = "";
|
|
648
|
-
for await (const event of this.run(promptOrOptions)) {
|
|
649
|
-
if (event.type === "token") {
|
|
650
|
-
text += event.text;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return text;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
//# sourceMappingURL=base.js.map
|