background-agents 0.1.1 → 0.1.2
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 +173 -241
- package/dist/agents/claude/index.d.ts +11 -0
- package/dist/agents/claude/index.d.ts.map +1 -0
- package/dist/agents/claude/index.js +78 -0
- package/dist/agents/claude/index.js.map +1 -0
- package/dist/agents/claude/parser.d.ts +16 -0
- package/dist/agents/claude/parser.d.ts.map +1 -0
- package/dist/agents/claude/parser.js +87 -0
- package/dist/agents/claude/parser.js.map +1 -0
- package/dist/agents/claude/tools.d.ts +7 -0
- package/dist/agents/claude/tools.d.ts.map +1 -0
- package/dist/agents/claude/tools.js +15 -0
- package/dist/agents/claude/tools.js.map +1 -0
- package/dist/agents/codex/index.d.ts +11 -0
- package/dist/agents/codex/index.d.ts.map +1 -0
- package/dist/agents/codex/index.js +60 -0
- package/dist/agents/codex/index.js.map +1 -0
- package/dist/agents/codex/parser.d.ts +12 -0
- package/dist/agents/codex/parser.d.ts.map +1 -0
- package/dist/agents/codex/parser.js +108 -0
- package/dist/agents/codex/parser.js.map +1 -0
- package/dist/agents/codex/tools.d.ts +11 -0
- package/dist/agents/codex/tools.d.ts.map +1 -0
- package/dist/agents/codex/tools.js +40 -0
- package/dist/agents/codex/tools.js.map +1 -0
- package/dist/agents/eliza/bundle-content.d.ts +6 -0
- package/dist/agents/eliza/bundle-content.d.ts.map +1 -0
- package/dist/agents/eliza/bundle-content.js +7 -0
- package/dist/agents/eliza/bundle-content.js.map +1 -0
- package/dist/agents/eliza/cli.bundle.js +579 -0
- package/dist/agents/eliza/cli.d.ts +10 -0
- package/dist/agents/eliza/cli.d.ts.map +1 -0
- package/dist/agents/eliza/cli.js +342 -0
- package/dist/agents/eliza/cli.js.map +1 -0
- package/dist/agents/eliza/index.d.ts +22 -0
- package/dist/agents/eliza/index.d.ts.map +1 -0
- package/dist/agents/eliza/index.js +54 -0
- package/dist/agents/eliza/index.js.map +1 -0
- package/dist/agents/eliza/parser.d.ts +16 -0
- package/dist/agents/eliza/parser.d.ts.map +1 -0
- package/dist/agents/eliza/parser.js +67 -0
- package/dist/agents/eliza/parser.js.map +1 -0
- package/dist/agents/eliza/patterns.d.ts +41 -0
- package/dist/agents/eliza/patterns.d.ts.map +1 -0
- package/dist/agents/eliza/patterns.js +259 -0
- package/dist/agents/eliza/patterns.js.map +1 -0
- package/dist/agents/eliza/tools.d.ts +7 -0
- package/dist/agents/eliza/tools.d.ts.map +1 -0
- package/dist/agents/eliza/tools.js +14 -0
- package/dist/agents/eliza/tools.js.map +1 -0
- package/dist/agents/gemini/index.d.ts +11 -0
- package/dist/agents/gemini/index.d.ts.map +1 -0
- package/dist/agents/gemini/index.js +46 -0
- package/dist/agents/gemini/index.js.map +1 -0
- package/dist/agents/gemini/parser.d.ts +31 -0
- package/dist/agents/gemini/parser.d.ts.map +1 -0
- package/dist/agents/gemini/parser.js +106 -0
- package/dist/agents/gemini/parser.js.map +1 -0
- package/dist/agents/gemini/tools.d.ts +7 -0
- package/dist/agents/gemini/tools.d.ts.map +1 -0
- package/dist/agents/gemini/tools.js +23 -0
- package/dist/agents/gemini/tools.js.map +1 -0
- package/dist/agents/goose/index.d.ts +11 -0
- package/dist/agents/goose/index.d.ts.map +1 -0
- package/dist/agents/goose/index.js +73 -0
- package/dist/agents/goose/index.js.map +1 -0
- package/dist/agents/goose/parser.d.ts +24 -0
- package/dist/agents/goose/parser.d.ts.map +1 -0
- package/dist/agents/goose/parser.js +86 -0
- package/dist/agents/goose/parser.js.map +1 -0
- package/dist/agents/goose/tools.d.ts +10 -0
- package/dist/agents/goose/tools.d.ts.map +1 -0
- package/dist/agents/goose/tools.js +30 -0
- package/dist/agents/goose/tools.js.map +1 -0
- package/dist/agents/index.d.ts +27 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +46 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/opencode/index.d.ts +12 -0
- package/dist/agents/opencode/index.d.ts.map +1 -0
- package/dist/agents/opencode/index.js +53 -0
- package/dist/agents/opencode/index.js.map +1 -0
- package/dist/agents/opencode/parser.d.ts +15 -0
- package/dist/agents/opencode/parser.d.ts.map +1 -0
- package/dist/agents/opencode/parser.js +71 -0
- package/dist/agents/opencode/parser.js.map +1 -0
- package/dist/agents/opencode/tools.d.ts +7 -0
- package/dist/agents/opencode/tools.d.ts.map +1 -0
- package/dist/agents/opencode/tools.js +10 -0
- package/dist/agents/opencode/tools.js.map +1 -0
- package/dist/agents/openhands/index.d.ts +17 -0
- package/dist/agents/openhands/index.d.ts.map +1 -0
- package/dist/agents/openhands/index.js +67 -0
- package/dist/agents/openhands/index.js.map +1 -0
- package/dist/agents/openhands/parser.d.ts +16 -0
- package/dist/agents/openhands/parser.d.ts.map +1 -0
- package/dist/agents/openhands/parser.js +93 -0
- package/dist/agents/openhands/parser.js.map +1 -0
- package/dist/agents/openhands/tools.d.ts +7 -0
- package/dist/agents/openhands/tools.d.ts.map +1 -0
- package/dist/agents/openhands/tools.js +24 -0
- package/dist/agents/openhands/tools.js.map +1 -0
- package/dist/agents/pi/index.d.ts +14 -0
- package/dist/agents/pi/index.d.ts.map +1 -0
- package/dist/agents/pi/index.js +54 -0
- package/dist/agents/pi/index.js.map +1 -0
- package/dist/agents/pi/parser.d.ts +21 -0
- package/dist/agents/pi/parser.d.ts.map +1 -0
- package/dist/agents/pi/parser.js +91 -0
- package/dist/agents/pi/parser.js.map +1 -0
- package/dist/agents/pi/tools.d.ts +8 -0
- package/dist/agents/pi/tools.d.ts.map +1 -0
- package/dist/agents/pi/tools.js +16 -0
- package/dist/agents/pi/tools.js.map +1 -0
- package/dist/agents/picocode/index.d.ts +18 -0
- package/dist/agents/picocode/index.d.ts.map +1 -0
- package/dist/agents/picocode/index.js +68 -0
- package/dist/agents/picocode/index.js.map +1 -0
- package/dist/agents/picocode/parser.d.ts +19 -0
- package/dist/agents/picocode/parser.d.ts.map +1 -0
- package/dist/agents/picocode/parser.js +104 -0
- package/dist/agents/picocode/parser.js.map +1 -0
- package/dist/agents/picocode/tools.d.ts +9 -0
- package/dist/agents/picocode/tools.d.ts.map +1 -0
- package/dist/agents/picocode/tools.js +27 -0
- package/dist/agents/picocode/tools.js.map +1 -0
- package/dist/background/index.d.ts +6 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +5 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/session.d.ts +47 -0
- package/dist/background/session.d.ts.map +1 -0
- package/dist/background/session.js +481 -0
- package/dist/background/session.js.map +1 -0
- package/dist/background/types.d.ts +55 -0
- package/dist/background/types.d.ts.map +1 -0
- package/dist/background/types.js +5 -0
- package/dist/background/types.js.map +1 -0
- package/dist/core/agent.d.ts +95 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +8 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/registry.d.ts +48 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +68 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/tools.d.ts +31 -0
- package/dist/core/tools.d.ts.map +1 -0
- package/dist/core/tools.js +82 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/debug.js +1 -1
- package/dist/debug.js.map +1 -1
- package/dist/factory.d.ts +1 -4
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +1 -4
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +29 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -14
- package/dist/index.js.map +1 -1
- package/dist/providers/base.d.ts +45 -18
- package/dist/providers/base.d.ts.map +1 -1
- package/dist/providers/base.js +228 -265
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +18 -8
- package/dist/providers/gemini.js.map +1 -1
- package/dist/sandbox/daytona.d.ts +5 -1
- package/dist/sandbox/daytona.d.ts.map +1 -1
- package/dist/sandbox/daytona.js +157 -214
- package/dist/sandbox/daytona.js.map +1 -1
- package/dist/sandbox/index.d.ts +3 -3
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +2 -2
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session.d.ts +62 -51
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +94 -90
- package/dist/session.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +37 -94
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +3 -0
- package/dist/types/provider.js.map +1 -1
- package/dist/utils/index.d.ts +2 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/install.d.ts +12 -2
- package/dist/utils/install.d.ts.map +1 -1
- package/dist/utils/install.js +40 -4
- package/dist/utils/install.js.map +1 -1
- package/package.json +24 -13
- package/src/index.ts +156 -0
- 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/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
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import * as readline from "node:readline";
|
|
3
1
|
import { randomUUID } from "node:crypto";
|
|
4
2
|
import { debugLog } from "../debug.js";
|
|
5
|
-
import { getDefaultSessionPath, loadSession, storeSession } from "../utils/session.js";
|
|
6
|
-
import { ensureCliInstalled } from "../utils/install.js";
|
|
7
3
|
import { adaptSandbox } from "../sandbox/index.js";
|
|
4
|
+
/** After {@link readSandboxMeta} `startedAt`, ignore "done but no output" briefly (race with wrapper). */
|
|
5
|
+
const BACKGROUND_STARTUP_GRACE_MS = 4000;
|
|
6
|
+
function withinStartupGrace(meta) {
|
|
7
|
+
if (!meta.startedAt)
|
|
8
|
+
return false;
|
|
9
|
+
const t = Date.parse(meta.startedAt);
|
|
10
|
+
if (Number.isNaN(t))
|
|
11
|
+
return false;
|
|
12
|
+
return Date.now() - t < BACKGROUND_STARTUP_GRACE_MS;
|
|
13
|
+
}
|
|
14
|
+
function hasObservableBackgroundProgress(result) {
|
|
15
|
+
for (const e of result.events) {
|
|
16
|
+
if (e.type === "token" || e.type === "tool_start" || e.type === "tool_end" || e.type === "end") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const raw = (result.rawOutput ?? "").trim();
|
|
21
|
+
const nonJsonLines = raw.split("\n").filter((l) => {
|
|
22
|
+
const t = l.trim();
|
|
23
|
+
return t && !(t.startsWith("{") && t.endsWith("}"));
|
|
24
|
+
});
|
|
25
|
+
return nonJsonLines.some((l) => l.trim().length > 0);
|
|
26
|
+
}
|
|
8
27
|
/**
|
|
9
28
|
* Abstract base class for AI coding agent providers
|
|
10
29
|
*/
|
|
@@ -14,44 +33,25 @@ export class Provider {
|
|
|
14
33
|
return this.sessionId;
|
|
15
34
|
}
|
|
16
35
|
/** Sandbox for secure execution */
|
|
17
|
-
sandboxManager
|
|
18
|
-
/** Whether local execution is allowed */
|
|
19
|
-
allowLocalExecution = false;
|
|
36
|
+
sandboxManager;
|
|
20
37
|
/** Resolves when initial setup (install + env) has completed. */
|
|
21
38
|
_readyPromise = null;
|
|
22
|
-
/** Env passed at creation; used for setup and when run() omits env */
|
|
23
|
-
_creationEnv;
|
|
24
39
|
/** Defaults merged into every run (model, timeout, sessionId, env). Set by createSession. */
|
|
25
40
|
_runDefaults = {};
|
|
41
|
+
/** Tracks whether session-level env has been applied */
|
|
42
|
+
_sessionEnvApplied = false;
|
|
26
43
|
/** Tracks whether we've already applied a synthetic system prompt for this session. */
|
|
27
44
|
_systemPromptApplied = false;
|
|
28
45
|
get ready() {
|
|
29
46
|
return this._readyPromise ?? Promise.resolve();
|
|
30
47
|
}
|
|
31
|
-
constructor(options
|
|
32
|
-
this._creationEnv = options.env;
|
|
48
|
+
constructor(options) {
|
|
33
49
|
this._runDefaults = options.runDefaults ?? {};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
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 })");
|
|
50
|
+
this.sandboxManager = adaptSandbox(options.sandbox, { env: options.env });
|
|
51
|
+
if (!options.skipInstall) {
|
|
52
|
+
this._readyPromise = new Promise((resolve, reject) => {
|
|
53
|
+
queueMicrotask(() => this._doSetup().then(resolve).catch(reject));
|
|
54
|
+
});
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
@@ -82,15 +82,7 @@ export class Provider {
|
|
|
82
82
|
: { ...this._runDefaults, ...promptOrOptions };
|
|
83
83
|
options = this._applySystemPrompt(options);
|
|
84
84
|
debugLog(`run start provider=${this.name} promptLength=${options.prompt?.length ?? 0}`, this.sessionId);
|
|
85
|
-
|
|
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
|
-
}
|
|
85
|
+
yield* this.runSandbox(options);
|
|
94
86
|
debugLog(`run end provider=${this.name}`, this.sessionId);
|
|
95
87
|
}
|
|
96
88
|
async _codexLoginIfNeeded(env) {
|
|
@@ -101,32 +93,58 @@ export class Provider {
|
|
|
101
93
|
const safeKey = env.OPENAI_API_KEY.replace(/'/g, "'\\''");
|
|
102
94
|
await this.sandboxManager.executeCommand(`echo '${safeKey}' | codex login --with-api-key 2>&1`, 30);
|
|
103
95
|
}
|
|
104
|
-
/** One-time setup: install CLI and set env. Run in microtask so subclass name is set. */
|
|
96
|
+
/** One-time setup: install CLI and set session-level env. Run in microtask so subclass name is set. */
|
|
105
97
|
async _doSetup() {
|
|
106
|
-
if (!this.sandboxManager)
|
|
107
|
-
return;
|
|
108
98
|
const t = Date.now();
|
|
109
99
|
await this.sandboxManager.ensureProvider(this.name);
|
|
110
100
|
console.log(`[timing] ensureProvider(${this.name}) took ${Date.now() - t}ms`);
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
// Apply session-level env from runDefaults (set by createSession)
|
|
102
|
+
const sessionEnv = this._runDefaults.env;
|
|
103
|
+
if (sessionEnv && !this._sessionEnvApplied) {
|
|
104
|
+
if (this.sandboxManager.setSessionEnvVars) {
|
|
105
|
+
this.sandboxManager.setSessionEnvVars(sessionEnv);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Fallback for backwards compatibility
|
|
109
|
+
this.sandboxManager.setEnvVars(sessionEnv);
|
|
110
|
+
}
|
|
111
|
+
this._sessionEnvApplied = true;
|
|
112
|
+
}
|
|
113
113
|
}
|
|
114
|
-
/** Per-run: set env and Codex login. */
|
|
114
|
+
/** Per-run: clear previous run-level env, set new run-level env, and handle Codex login. */
|
|
115
115
|
async _applyRunEnv(options) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
// Ensure session-level env is applied (idempotent)
|
|
117
|
+
const sessionEnv = this._runDefaults.env;
|
|
118
|
+
if (sessionEnv && !this._sessionEnvApplied) {
|
|
119
|
+
if (this.sandboxManager.setSessionEnvVars) {
|
|
120
|
+
this.sandboxManager.setSessionEnvVars(sessionEnv);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.sandboxManager.setEnvVars(sessionEnv);
|
|
124
|
+
}
|
|
125
|
+
this._sessionEnvApplied = true;
|
|
126
|
+
}
|
|
127
|
+
// Clear previous run-level env
|
|
128
|
+
if (this.sandboxManager.clearRunEnvVars) {
|
|
129
|
+
this.sandboxManager.clearRunEnvVars();
|
|
130
|
+
}
|
|
131
|
+
// Apply new run-level env (if provided)
|
|
132
|
+
const runEnv = options.env;
|
|
133
|
+
if (runEnv) {
|
|
134
|
+
if (this.sandboxManager.setRunEnvVars) {
|
|
135
|
+
this.sandboxManager.setRunEnvVars(runEnv);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Fallback for backwards compatibility
|
|
139
|
+
this.sandboxManager.setEnvVars(runEnv);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await this._codexLoginIfNeeded(runEnv ?? sessionEnv);
|
|
122
143
|
}
|
|
123
144
|
/**
|
|
124
145
|
* Run in a secure Daytona sandbox
|
|
125
146
|
*/
|
|
126
147
|
async *runSandbox(options) {
|
|
127
|
-
if (!this.sandboxManager) {
|
|
128
|
-
throw new Error("Sandbox manager not configured");
|
|
129
|
-
}
|
|
130
148
|
await (this._readyPromise ?? Promise.resolve());
|
|
131
149
|
await this._applyRunEnv(options);
|
|
132
150
|
// Build the command
|
|
@@ -173,81 +191,6 @@ export class Provider {
|
|
|
173
191
|
}
|
|
174
192
|
debugLog(`runSandbox stream ended provider=${this.name}`, this.sessionId);
|
|
175
193
|
}
|
|
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
194
|
/** Meta stored in sandbox for background session (one log per turn, cursor in meta). */
|
|
252
195
|
async readSandboxMeta(sessionDir) {
|
|
253
196
|
if (!this.sandboxManager?.executeCommand)
|
|
@@ -296,6 +239,27 @@ export class Provider {
|
|
|
296
239
|
const b64 = Buffer.from(json, "utf8").toString("base64");
|
|
297
240
|
await this.sandboxManager.executeCommand(`mkdir -p "${sessionDir}" && echo '${b64}' | base64 -d > "${sessionDir}/meta.json"`, 10);
|
|
298
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Best-effort no-op write guard for polling paths. Skips writing meta when no
|
|
244
|
+
* relevant fields changed to reduce sandbox file writes on every poll.
|
|
245
|
+
*/
|
|
246
|
+
async writeSandboxMetaIfChanged(sessionDir, next, prev) {
|
|
247
|
+
if (prev) {
|
|
248
|
+
const unchanged = prev.currentTurn === next.currentTurn &&
|
|
249
|
+
prev.cursor === next.cursor &&
|
|
250
|
+
(prev.rawCursor ?? 0) === (next.rawCursor ?? 0) &&
|
|
251
|
+
prev.pid === next.pid &&
|
|
252
|
+
prev.runId === next.runId &&
|
|
253
|
+
prev.outputFile === next.outputFile &&
|
|
254
|
+
(prev.sawEnd ?? false) === (next.sawEnd ?? false) &&
|
|
255
|
+
prev.startedAt === next.startedAt &&
|
|
256
|
+
prev.provider === next.provider &&
|
|
257
|
+
(prev.sessionId ?? null) === (next.sessionId ?? null);
|
|
258
|
+
if (unchanged)
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await this.writeSandboxMeta(sessionDir, next);
|
|
262
|
+
}
|
|
299
263
|
/**
|
|
300
264
|
* Start a new turn in a session directory: one log file per turn, meta in sandbox.
|
|
301
265
|
* Uses currentTurn for this run; currentTurn is incremented when the turn ends (in getEvents).
|
|
@@ -355,8 +319,8 @@ export class Provider {
|
|
|
355
319
|
}
|
|
356
320
|
/**
|
|
357
321
|
* Cancel the current turn's process in the sandbox (kill pid from meta).
|
|
358
|
-
* Uses
|
|
359
|
-
* Writes the done file after kill so isRunning() becomes false
|
|
322
|
+
* Uses robust multi-step kill: SIGTERM -> SIGKILL -> pkill fallback.
|
|
323
|
+
* Writes the done file after kill so isRunning() becomes false.
|
|
360
324
|
*/
|
|
361
325
|
async cancelSandboxBackground(sessionDir) {
|
|
362
326
|
const meta = await this.readSandboxMeta(sessionDir);
|
|
@@ -364,11 +328,17 @@ export class Provider {
|
|
|
364
328
|
return;
|
|
365
329
|
const mgr = this.sandboxManager;
|
|
366
330
|
if (mgr?.killBackgroundProcess) {
|
|
367
|
-
|
|
331
|
+
// Use the robust kill implementation (includes TERM -> KILL -> pkill)
|
|
332
|
+
await mgr.killBackgroundProcess(meta.pid, this.name);
|
|
368
333
|
}
|
|
369
334
|
else if (mgr?.executeCommand) {
|
|
370
|
-
|
|
335
|
+
// Fallback: manual multi-step kill
|
|
336
|
+
await mgr.executeCommand(`kill -TERM ${meta.pid} 2>/dev/null || true`, 10);
|
|
337
|
+
await new Promise(r => setTimeout(r, 500));
|
|
338
|
+
await mgr.executeCommand(`kill -9 ${meta.pid} 2>/dev/null || true`, 10);
|
|
339
|
+
await mgr.executeCommand(`pkill -9 -f "${this.name}" 2>/dev/null || true`, 10);
|
|
371
340
|
}
|
|
341
|
+
// Write done file so isRunning() returns false
|
|
372
342
|
if (meta.outputFile && mgr?.executeCommand) {
|
|
373
343
|
const donePath = meta.outputFile + ".done";
|
|
374
344
|
const escaped = donePath.replace(/'/g, "'\\''");
|
|
@@ -388,93 +358,132 @@ export class Provider {
|
|
|
388
358
|
debugLog(`isRunning false (no run) sessionDir=${sessionDir}`, this.sessionId);
|
|
389
359
|
return false;
|
|
390
360
|
}
|
|
391
|
-
const
|
|
361
|
+
const running = await this.isSandboxBackgroundOutputRunning(meta.outputFile);
|
|
362
|
+
debugLog(`isRunning ${running} (done file ${running ? "missing" : "exists"}) sessionDir=${sessionDir}`, this.sessionId);
|
|
363
|
+
return running;
|
|
364
|
+
}
|
|
365
|
+
async isSandboxBackgroundOutputRunning(outputFile) {
|
|
366
|
+
if (!this.sandboxManager?.executeCommand)
|
|
367
|
+
return false;
|
|
368
|
+
const donePath = outputFile + ".done";
|
|
392
369
|
const escaped = donePath.replace(/'/g, "'\\''");
|
|
393
370
|
const r = await this.sandboxManager.executeCommand(`test -f '${escaped}' 2>/dev/null; echo $?`, 10);
|
|
394
371
|
const doneExists = Number((r.output ?? "").trim().split(/\s+/).pop()) === 0;
|
|
395
|
-
|
|
396
|
-
debugLog(`isRunning ${running} (done file ${doneExists ? "exists" : "missing"}) sessionDir=${sessionDir}`, this.sessionId);
|
|
397
|
-
return running;
|
|
372
|
+
return !doneExists;
|
|
398
373
|
}
|
|
399
374
|
/**
|
|
400
375
|
* Get new events for the current turn; reads and updates cursor in sandbox meta.
|
|
401
|
-
*
|
|
376
|
+
* Uses optimized polling when available (2 round trips instead of 4).
|
|
402
377
|
*/
|
|
403
378
|
async getEventsSandboxBackgroundFromMeta(sessionDir) {
|
|
404
|
-
|
|
379
|
+
// Optimized path: read meta + output + done status together
|
|
380
|
+
let meta = null;
|
|
381
|
+
let outputContent = null;
|
|
382
|
+
let stillRunning;
|
|
383
|
+
if (this.sandboxManager?.pollBackgroundState) {
|
|
384
|
+
const state = await this.sandboxManager.pollBackgroundState(sessionDir);
|
|
385
|
+
if (state?.meta) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(state.meta);
|
|
388
|
+
if (typeof parsed.currentTurn === "number" && typeof parsed.cursor === "number") {
|
|
389
|
+
meta = parsed;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch { /* invalid JSON */ }
|
|
393
|
+
}
|
|
394
|
+
outputContent = state?.output ?? null;
|
|
395
|
+
stillRunning = !state?.done;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
// Legacy path: separate calls
|
|
399
|
+
meta = await this.readSandboxMeta(sessionDir);
|
|
400
|
+
stillRunning = meta?.outputFile ? await this.isSandboxBackgroundOutputRunning(meta.outputFile) : false;
|
|
401
|
+
}
|
|
405
402
|
if (!meta?.runId || !meta.outputFile) {
|
|
406
403
|
debugLog(`getEventsSandboxBackgroundFromMeta provider=${this.name} sessionDir=${sessionDir} (no turn in progress)`, this.sessionId);
|
|
407
404
|
return {
|
|
408
405
|
sessionId: meta?.sessionId ?? this.sessionId ?? null,
|
|
409
406
|
events: [],
|
|
410
407
|
cursor: String(meta?.cursor ?? 0),
|
|
408
|
+
running: false,
|
|
409
|
+
runPhase: "idle",
|
|
411
410
|
};
|
|
412
411
|
}
|
|
413
|
-
const outputFile = meta.outputFile;
|
|
414
412
|
const cursor = String(meta.cursor);
|
|
415
413
|
debugLog(`getEventsSandboxBackgroundFromMeta provider=${this.name} sessionDir=${sessionDir} turn=${meta.currentTurn} cursor=${cursor}`, this.sessionId);
|
|
416
|
-
|
|
414
|
+
// Poll output (uses pre-fetched content if available)
|
|
415
|
+
const result = await this.pollSandboxBackground(meta.outputFile, cursor, meta.rawCursor != null ? String(meta.rawCursor) : null, outputContent);
|
|
417
416
|
const sawEnd = meta.sawEnd || result.events.some((e) => e.type === "end");
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await this.
|
|
417
|
+
// Handle completion states and update meta
|
|
418
|
+
return this._handlePollResult(sessionDir, meta, result, stillRunning, sawEnd);
|
|
419
|
+
}
|
|
420
|
+
/** Process poll result and update meta. Shared by all polling paths. */
|
|
421
|
+
async _handlePollResult(sessionDir, meta, result, stillRunning, sawEnd) {
|
|
422
|
+
const baseMeta = {
|
|
423
|
+
cursor: Number(result.cursor) || 0,
|
|
424
|
+
rawCursor: Number(result.rawCursor) || meta.rawCursor || 0,
|
|
425
|
+
provider: this.name,
|
|
426
|
+
sessionId: this.sessionId ?? meta.sessionId ?? null,
|
|
427
|
+
};
|
|
428
|
+
// Early poll / wrapper race: done file appears before any JSONL output — stay "active" until grace elapses.
|
|
429
|
+
if (!stillRunning &&
|
|
430
|
+
!sawEnd &&
|
|
431
|
+
withinStartupGrace(meta) &&
|
|
432
|
+
!hasObservableBackgroundProgress(result)) {
|
|
433
|
+
await this.writeSandboxMetaIfChanged(sessionDir, {
|
|
435
434
|
currentTurn: meta.currentTurn,
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
...baseMeta,
|
|
436
|
+
sawEnd: false,
|
|
438
437
|
pid: meta.pid,
|
|
439
438
|
runId: meta.runId,
|
|
440
439
|
outputFile: meta.outputFile,
|
|
441
|
-
sawEnd,
|
|
442
440
|
startedAt: meta.startedAt,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
}, meta);
|
|
442
|
+
return {
|
|
443
|
+
sessionId: result.sessionId,
|
|
444
|
+
events: result.events,
|
|
445
|
+
cursor: result.cursor,
|
|
446
|
+
running: true,
|
|
447
|
+
runPhase: "starting",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (!stillRunning || sawEnd) {
|
|
451
|
+
const nextTurn = (meta.currentTurn ?? 0) + 1;
|
|
452
|
+
await this.writeSandboxMetaIfChanged(sessionDir, {
|
|
453
|
+
currentTurn: nextTurn, ...baseMeta, sawEnd,
|
|
454
|
+
...(sawEnd ? {} : { outputFile: meta.outputFile, runId: meta.runId }),
|
|
455
|
+
}, meta);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
await this.writeSandboxMetaIfChanged(sessionDir, {
|
|
459
|
+
currentTurn: meta.currentTurn, ...baseMeta, sawEnd,
|
|
460
|
+
pid: meta.pid, runId: meta.runId, outputFile: meta.outputFile, startedAt: meta.startedAt,
|
|
461
|
+
}, meta);
|
|
446
462
|
}
|
|
463
|
+
// Crashed: process exited without end event
|
|
447
464
|
if (!stillRunning && !sawEnd) {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const crashEvent = {
|
|
461
|
-
type: "agent_crashed",
|
|
462
|
-
message: "Agent process exited without completing (crashed or killed)",
|
|
463
|
-
output,
|
|
465
|
+
const raw = (result.rawOutput ?? "").trim();
|
|
466
|
+
const nonJsonLines = raw.split("\n").filter((l) => { const t = l.trim(); return t && !(t.startsWith("{") && t.endsWith("}")); });
|
|
467
|
+
const output = nonJsonLines.join("\n").trim().slice(-4096) || undefined;
|
|
468
|
+
const crashEvent = { type: "agent_crashed", message: "Agent process exited without completing (crashed or killed)", output };
|
|
469
|
+
debugLog("session end", this.sessionId ?? meta.sessionId, "reason=crashed", crashEvent.message);
|
|
470
|
+
await this.writeSandboxMetaIfChanged(sessionDir, { currentTurn: (meta.currentTurn ?? 0) + 1, ...baseMeta, sawEnd: true }, meta);
|
|
471
|
+
return {
|
|
472
|
+
sessionId: result.sessionId,
|
|
473
|
+
events: [...result.events, crashEvent],
|
|
474
|
+
cursor: result.cursor,
|
|
475
|
+
running: false,
|
|
476
|
+
runPhase: "stopped",
|
|
464
477
|
};
|
|
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
478
|
}
|
|
477
|
-
|
|
479
|
+
const active = stillRunning && !sawEnd;
|
|
480
|
+
return {
|
|
481
|
+
sessionId: result.sessionId,
|
|
482
|
+
events: result.events,
|
|
483
|
+
cursor: result.cursor,
|
|
484
|
+
running: active,
|
|
485
|
+
runPhase: active ? "running" : "stopped",
|
|
486
|
+
};
|
|
478
487
|
}
|
|
479
488
|
/**
|
|
480
489
|
* Start a background run inside the sandbox.
|
|
@@ -529,98 +538,52 @@ export class Provider {
|
|
|
529
538
|
}
|
|
530
539
|
/**
|
|
531
540
|
* Poll a background sandbox run by reading the JSONL log file.
|
|
532
|
-
*
|
|
541
|
+
* If prefetchedContent is provided, uses that instead of fetching from sandbox.
|
|
533
542
|
*/
|
|
534
|
-
async pollSandboxBackground(outputFile, cursor, rawCursor) {
|
|
535
|
-
if
|
|
536
|
-
|
|
543
|
+
async pollSandboxBackground(outputFile, cursor, rawCursor, prefetchedContent) {
|
|
544
|
+
// Get content: use prefetched if available, otherwise fetch
|
|
545
|
+
let rawOutput;
|
|
546
|
+
if (prefetchedContent != null) {
|
|
547
|
+
rawOutput = prefetchedContent;
|
|
537
548
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
549
|
+
else {
|
|
550
|
+
if (!this.sandboxManager?.executeCommand) {
|
|
551
|
+
throw new Error("Sandbox background mode requires a sandbox with executeCommand support");
|
|
552
|
+
}
|
|
553
|
+
const result = await this.sandboxManager.executeCommand(`cat ${outputFile}`, 30);
|
|
554
|
+
rawOutput = result.output ?? "";
|
|
555
|
+
}
|
|
556
|
+
const startIndex = cursor ? Number(cursor) || 0 : 0;
|
|
557
|
+
void rawCursor; // used for tracking but not filtering in simplified version
|
|
545
558
|
const rawLines = rawOutput.split("\n");
|
|
546
|
-
debugLog(`pollSandboxBackground provider=${this.name} outputFile=${outputFile} cursor=${cursor ?? "null"} rawLines=${rawLines.length}`, this.sessionId);
|
|
547
559
|
const lines = [];
|
|
548
|
-
const
|
|
549
|
-
const trimmed = line.trim();
|
|
550
|
-
return trimmed.startsWith("{") && trimmed.endsWith("}");
|
|
551
|
-
};
|
|
552
|
-
const isRereading = rawStartIndex >= rawLines.length;
|
|
560
|
+
const isJson = (s) => s.startsWith("{") && s.endsWith("}");
|
|
553
561
|
for (let i = 0; i < rawLines.length; i++) {
|
|
554
|
-
const
|
|
555
|
-
if (i >= rawStartIndex) {
|
|
556
|
-
debugLog(`raw file line [${i}]: ${line}`, this.sessionId);
|
|
557
|
-
}
|
|
558
|
-
const trimmed = line.trim();
|
|
562
|
+
const trimmed = rawLines[i].trim();
|
|
559
563
|
if (!trimmed)
|
|
560
564
|
continue;
|
|
561
|
-
if (!
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
if (isJsonLine(trimmed)) {
|
|
565
|
+
if (!isJson(trimmed) && i === rawLines.length - 1)
|
|
566
|
+
continue; // skip partial last line
|
|
567
|
+
if (isJson(trimmed))
|
|
567
568
|
lines.push(trimmed);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
if (!isRereading)
|
|
571
|
-
debugLog(`background poll skipped (not JSONL) [${i}]: ${trimmed}`, this.sessionId);
|
|
572
|
-
}
|
|
573
569
|
}
|
|
574
570
|
if (startIndex >= lines.length) {
|
|
575
|
-
|
|
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);
|
|
571
|
+
return { status: "running", sessionId: this.sessionId, events: [], cursor: String(lines.length), rawCursor: String(rawLines.length), rawOutput };
|
|
591
572
|
}
|
|
592
573
|
const eventsOut = [];
|
|
593
574
|
let status = "running";
|
|
594
|
-
for (const line of slice) {
|
|
575
|
+
for (const line of lines.slice(startIndex)) {
|
|
595
576
|
const raw = this.parse(line);
|
|
596
|
-
if (raw === null) {
|
|
597
|
-
debugLog(`unparsed line (background poll):`, this.sessionId, line);
|
|
598
|
-
}
|
|
599
577
|
const events = raw === null ? [] : Array.isArray(raw) ? raw : [raw];
|
|
600
578
|
for (const event of events) {
|
|
601
|
-
if (event.type === "session")
|
|
579
|
+
if (event.type === "session")
|
|
602
580
|
this.sessionId = event.id;
|
|
603
|
-
|
|
604
|
-
if (event.type === "end") {
|
|
581
|
+
if (event.type === "end")
|
|
605
582
|
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
583
|
eventsOut.push(event);
|
|
612
584
|
}
|
|
613
585
|
}
|
|
614
|
-
|
|
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
|
-
};
|
|
586
|
+
return { status, sessionId: this.sessionId, events: eventsOut, cursor: String(lines.length), rawCursor: String(rawLines.length), rawOutput };
|
|
624
587
|
}
|
|
625
588
|
/**
|
|
626
589
|
* Run the provider with a callback for each event
|