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.
Files changed (210) hide show
  1. package/README.md +173 -241
  2. package/dist/agents/claude/index.d.ts +11 -0
  3. package/dist/agents/claude/index.d.ts.map +1 -0
  4. package/dist/agents/claude/index.js +78 -0
  5. package/dist/agents/claude/index.js.map +1 -0
  6. package/dist/agents/claude/parser.d.ts +16 -0
  7. package/dist/agents/claude/parser.d.ts.map +1 -0
  8. package/dist/agents/claude/parser.js +87 -0
  9. package/dist/agents/claude/parser.js.map +1 -0
  10. package/dist/agents/claude/tools.d.ts +7 -0
  11. package/dist/agents/claude/tools.d.ts.map +1 -0
  12. package/dist/agents/claude/tools.js +15 -0
  13. package/dist/agents/claude/tools.js.map +1 -0
  14. package/dist/agents/codex/index.d.ts +11 -0
  15. package/dist/agents/codex/index.d.ts.map +1 -0
  16. package/dist/agents/codex/index.js +60 -0
  17. package/dist/agents/codex/index.js.map +1 -0
  18. package/dist/agents/codex/parser.d.ts +12 -0
  19. package/dist/agents/codex/parser.d.ts.map +1 -0
  20. package/dist/agents/codex/parser.js +108 -0
  21. package/dist/agents/codex/parser.js.map +1 -0
  22. package/dist/agents/codex/tools.d.ts +11 -0
  23. package/dist/agents/codex/tools.d.ts.map +1 -0
  24. package/dist/agents/codex/tools.js +40 -0
  25. package/dist/agents/codex/tools.js.map +1 -0
  26. package/dist/agents/eliza/bundle-content.d.ts +6 -0
  27. package/dist/agents/eliza/bundle-content.d.ts.map +1 -0
  28. package/dist/agents/eliza/bundle-content.js +7 -0
  29. package/dist/agents/eliza/bundle-content.js.map +1 -0
  30. package/dist/agents/eliza/cli.bundle.js +579 -0
  31. package/dist/agents/eliza/cli.d.ts +10 -0
  32. package/dist/agents/eliza/cli.d.ts.map +1 -0
  33. package/dist/agents/eliza/cli.js +342 -0
  34. package/dist/agents/eliza/cli.js.map +1 -0
  35. package/dist/agents/eliza/index.d.ts +22 -0
  36. package/dist/agents/eliza/index.d.ts.map +1 -0
  37. package/dist/agents/eliza/index.js +54 -0
  38. package/dist/agents/eliza/index.js.map +1 -0
  39. package/dist/agents/eliza/parser.d.ts +16 -0
  40. package/dist/agents/eliza/parser.d.ts.map +1 -0
  41. package/dist/agents/eliza/parser.js +67 -0
  42. package/dist/agents/eliza/parser.js.map +1 -0
  43. package/dist/agents/eliza/patterns.d.ts +41 -0
  44. package/dist/agents/eliza/patterns.d.ts.map +1 -0
  45. package/dist/agents/eliza/patterns.js +259 -0
  46. package/dist/agents/eliza/patterns.js.map +1 -0
  47. package/dist/agents/eliza/tools.d.ts +7 -0
  48. package/dist/agents/eliza/tools.d.ts.map +1 -0
  49. package/dist/agents/eliza/tools.js +14 -0
  50. package/dist/agents/eliza/tools.js.map +1 -0
  51. package/dist/agents/gemini/index.d.ts +11 -0
  52. package/dist/agents/gemini/index.d.ts.map +1 -0
  53. package/dist/agents/gemini/index.js +46 -0
  54. package/dist/agents/gemini/index.js.map +1 -0
  55. package/dist/agents/gemini/parser.d.ts +31 -0
  56. package/dist/agents/gemini/parser.d.ts.map +1 -0
  57. package/dist/agents/gemini/parser.js +106 -0
  58. package/dist/agents/gemini/parser.js.map +1 -0
  59. package/dist/agents/gemini/tools.d.ts +7 -0
  60. package/dist/agents/gemini/tools.d.ts.map +1 -0
  61. package/dist/agents/gemini/tools.js +23 -0
  62. package/dist/agents/gemini/tools.js.map +1 -0
  63. package/dist/agents/goose/index.d.ts +11 -0
  64. package/dist/agents/goose/index.d.ts.map +1 -0
  65. package/dist/agents/goose/index.js +73 -0
  66. package/dist/agents/goose/index.js.map +1 -0
  67. package/dist/agents/goose/parser.d.ts +24 -0
  68. package/dist/agents/goose/parser.d.ts.map +1 -0
  69. package/dist/agents/goose/parser.js +86 -0
  70. package/dist/agents/goose/parser.js.map +1 -0
  71. package/dist/agents/goose/tools.d.ts +10 -0
  72. package/dist/agents/goose/tools.d.ts.map +1 -0
  73. package/dist/agents/goose/tools.js +30 -0
  74. package/dist/agents/goose/tools.js.map +1 -0
  75. package/dist/agents/index.d.ts +27 -0
  76. package/dist/agents/index.d.ts.map +1 -0
  77. package/dist/agents/index.js +46 -0
  78. package/dist/agents/index.js.map +1 -0
  79. package/dist/agents/opencode/index.d.ts +12 -0
  80. package/dist/agents/opencode/index.d.ts.map +1 -0
  81. package/dist/agents/opencode/index.js +53 -0
  82. package/dist/agents/opencode/index.js.map +1 -0
  83. package/dist/agents/opencode/parser.d.ts +15 -0
  84. package/dist/agents/opencode/parser.d.ts.map +1 -0
  85. package/dist/agents/opencode/parser.js +71 -0
  86. package/dist/agents/opencode/parser.js.map +1 -0
  87. package/dist/agents/opencode/tools.d.ts +7 -0
  88. package/dist/agents/opencode/tools.d.ts.map +1 -0
  89. package/dist/agents/opencode/tools.js +10 -0
  90. package/dist/agents/opencode/tools.js.map +1 -0
  91. package/dist/agents/openhands/index.d.ts +17 -0
  92. package/dist/agents/openhands/index.d.ts.map +1 -0
  93. package/dist/agents/openhands/index.js +67 -0
  94. package/dist/agents/openhands/index.js.map +1 -0
  95. package/dist/agents/openhands/parser.d.ts +16 -0
  96. package/dist/agents/openhands/parser.d.ts.map +1 -0
  97. package/dist/agents/openhands/parser.js +93 -0
  98. package/dist/agents/openhands/parser.js.map +1 -0
  99. package/dist/agents/openhands/tools.d.ts +7 -0
  100. package/dist/agents/openhands/tools.d.ts.map +1 -0
  101. package/dist/agents/openhands/tools.js +24 -0
  102. package/dist/agents/openhands/tools.js.map +1 -0
  103. package/dist/agents/pi/index.d.ts +14 -0
  104. package/dist/agents/pi/index.d.ts.map +1 -0
  105. package/dist/agents/pi/index.js +54 -0
  106. package/dist/agents/pi/index.js.map +1 -0
  107. package/dist/agents/pi/parser.d.ts +21 -0
  108. package/dist/agents/pi/parser.d.ts.map +1 -0
  109. package/dist/agents/pi/parser.js +91 -0
  110. package/dist/agents/pi/parser.js.map +1 -0
  111. package/dist/agents/pi/tools.d.ts +8 -0
  112. package/dist/agents/pi/tools.d.ts.map +1 -0
  113. package/dist/agents/pi/tools.js +16 -0
  114. package/dist/agents/pi/tools.js.map +1 -0
  115. package/dist/agents/picocode/index.d.ts +18 -0
  116. package/dist/agents/picocode/index.d.ts.map +1 -0
  117. package/dist/agents/picocode/index.js +68 -0
  118. package/dist/agents/picocode/index.js.map +1 -0
  119. package/dist/agents/picocode/parser.d.ts +19 -0
  120. package/dist/agents/picocode/parser.d.ts.map +1 -0
  121. package/dist/agents/picocode/parser.js +104 -0
  122. package/dist/agents/picocode/parser.js.map +1 -0
  123. package/dist/agents/picocode/tools.d.ts +9 -0
  124. package/dist/agents/picocode/tools.d.ts.map +1 -0
  125. package/dist/agents/picocode/tools.js +27 -0
  126. package/dist/agents/picocode/tools.js.map +1 -0
  127. package/dist/background/index.d.ts +6 -0
  128. package/dist/background/index.d.ts.map +1 -0
  129. package/dist/background/index.js +5 -0
  130. package/dist/background/index.js.map +1 -0
  131. package/dist/background/session.d.ts +47 -0
  132. package/dist/background/session.d.ts.map +1 -0
  133. package/dist/background/session.js +481 -0
  134. package/dist/background/session.js.map +1 -0
  135. package/dist/background/types.d.ts +55 -0
  136. package/dist/background/types.d.ts.map +1 -0
  137. package/dist/background/types.js +5 -0
  138. package/dist/background/types.js.map +1 -0
  139. package/dist/core/agent.d.ts +95 -0
  140. package/dist/core/agent.d.ts.map +1 -0
  141. package/dist/core/agent.js +8 -0
  142. package/dist/core/agent.js.map +1 -0
  143. package/dist/core/index.d.ts +7 -0
  144. package/dist/core/index.d.ts.map +1 -0
  145. package/dist/core/index.js +6 -0
  146. package/dist/core/index.js.map +1 -0
  147. package/dist/core/registry.d.ts +48 -0
  148. package/dist/core/registry.d.ts.map +1 -0
  149. package/dist/core/registry.js +68 -0
  150. package/dist/core/registry.js.map +1 -0
  151. package/dist/core/tools.d.ts +31 -0
  152. package/dist/core/tools.d.ts.map +1 -0
  153. package/dist/core/tools.js +82 -0
  154. package/dist/core/tools.js.map +1 -0
  155. package/dist/debug.js +1 -1
  156. package/dist/debug.js.map +1 -1
  157. package/dist/factory.d.ts +1 -4
  158. package/dist/factory.d.ts.map +1 -1
  159. package/dist/factory.js +1 -4
  160. package/dist/factory.js.map +1 -1
  161. package/dist/index.d.ts +29 -11
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +41 -14
  164. package/dist/index.js.map +1 -1
  165. package/dist/providers/base.d.ts +45 -18
  166. package/dist/providers/base.d.ts.map +1 -1
  167. package/dist/providers/base.js +228 -265
  168. package/dist/providers/base.js.map +1 -1
  169. package/dist/providers/gemini.d.ts.map +1 -1
  170. package/dist/providers/gemini.js +18 -8
  171. package/dist/providers/gemini.js.map +1 -1
  172. package/dist/sandbox/daytona.d.ts +5 -1
  173. package/dist/sandbox/daytona.d.ts.map +1 -1
  174. package/dist/sandbox/daytona.js +157 -214
  175. package/dist/sandbox/daytona.js.map +1 -1
  176. package/dist/sandbox/index.d.ts +3 -3
  177. package/dist/sandbox/index.d.ts.map +1 -1
  178. package/dist/sandbox/index.js +2 -2
  179. package/dist/sandbox/index.js.map +1 -1
  180. package/dist/session.d.ts +62 -51
  181. package/dist/session.d.ts.map +1 -1
  182. package/dist/session.js +94 -90
  183. package/dist/session.js.map +1 -1
  184. package/dist/types/index.d.ts +2 -2
  185. package/dist/types/index.d.ts.map +1 -1
  186. package/dist/types/index.js +2 -2
  187. package/dist/types/index.js.map +1 -1
  188. package/dist/types/provider.d.ts +37 -94
  189. package/dist/types/provider.d.ts.map +1 -1
  190. package/dist/types/provider.js +3 -0
  191. package/dist/types/provider.js.map +1 -1
  192. package/dist/utils/index.d.ts +2 -3
  193. package/dist/utils/index.d.ts.map +1 -1
  194. package/dist/utils/index.js +2 -3
  195. package/dist/utils/index.js.map +1 -1
  196. package/dist/utils/install.d.ts +12 -2
  197. package/dist/utils/install.d.ts.map +1 -1
  198. package/dist/utils/install.js +40 -4
  199. package/dist/utils/install.js.map +1 -1
  200. package/package.json +24 -13
  201. package/src/index.ts +156 -0
  202. package/dist/sandbox/daytona-ssh.d.ts +0 -9
  203. package/dist/sandbox/daytona-ssh.d.ts.map +0 -1
  204. package/dist/sandbox/daytona-ssh.js +0 -113
  205. package/dist/sandbox/daytona-ssh.js.map +0 -1
  206. package/dist/utils/session.d.ts +0 -17
  207. package/dist/utils/session.d.ts.map +0 -1
  208. package/dist/utils/session.js +0 -59
  209. package/dist/utils/session.js.map +0 -1
  210. package/next.config.codeagentsdk.cjs +0 -22
@@ -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 = null;
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
- 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 })");
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
- 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
- }
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
- if (this._creationEnv)
112
- this.sandboxManager.setEnvVars(this._creationEnv);
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
- 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);
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 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).
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
- await mgr.killBackgroundProcess(meta.pid);
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
- await mgr.executeCommand(`kill ${meta.pid} 2>/dev/null || true`, 10);
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 donePath = meta.outputFile + ".done";
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
- const running = !doneExists;
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
- * Use isSandboxBackgroundProcessRunning() to check if the agent is still running.
376
+ * Uses optimized polling when available (2 round trips instead of 4).
402
377
  */
403
378
  async getEventsSandboxBackgroundFromMeta(sessionDir) {
404
- const meta = await this.readSandboxMeta(sessionDir);
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
- const result = await this.pollSandboxBackground(outputFile, cursor, meta.rawCursor != null ? String(meta.rawCursor) : null);
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
- 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, {
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
- cursor: Number(result.cursor) || 0,
437
- rawCursor: Number(result.rawCursor) || meta.rawCursor || 0,
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
- provider: this.name,
444
- sessionId: this.sessionId,
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 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,
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
- return { sessionId: result.sessionId, events: result.events, cursor: result.cursor };
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
- * Cursor is an opaque string representing the last processed line index.
541
+ * If prefetchedContent is provided, uses that instead of fetching from sandbox.
533
542
  */
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");
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
- 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 ?? "";
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 isJsonLine = (line) => {
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 line = rawLines[i];
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 (!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)) {
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
- // 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);
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
- 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
- };
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