@rudderhq/agent-runtime-opencode-local 0.5.1 → 0.5.2-canary.1

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 (37) hide show
  1. package/dist/server/browser-capability.test.js +12 -2
  2. package/dist/server/browser-capability.test.js.map +1 -1
  3. package/dist/server/execute.d.ts +1 -0
  4. package/dist/server/execute.d.ts.map +1 -1
  5. package/dist/server/execute.js +466 -419
  6. package/dist/server/execute.js.map +1 -1
  7. package/package.json +2 -2
  8. package/skills/browser/SKILL.md +172 -41
  9. package/skills/browser/agents/openai.yaml +12 -2
  10. package/skills/browser/evals/evals.json +41 -0
  11. package/skills/browser/references/interaction-guide.md +171 -0
  12. package/skills/browser/references/tool-contract.md +192 -68
  13. package/skills/rudder-docs/references/cli-reference.md +18 -1
  14. package/skills/skill-creator/CHANGELOG.md +26 -0
  15. package/skills/skill-creator/LICENSE.txt +202 -0
  16. package/skills/skill-creator/SKILL.md +544 -3
  17. package/skills/skill-creator/agents/analyzer.md +274 -0
  18. package/skills/skill-creator/agents/comparator.md +202 -0
  19. package/skills/skill-creator/agents/grader.md +223 -0
  20. package/skills/skill-creator/assets/eval_review.html +146 -0
  21. package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  22. package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  23. package/skills/skill-creator/references/compatibility/claudecode.md +29 -0
  24. package/skills/skill-creator/references/compatibility/codex.md +69 -0
  25. package/skills/skill-creator/references/compatibility/other.md +41 -0
  26. package/skills/skill-creator/references/rudder.md +96 -0
  27. package/skills/skill-creator/references/schemas.md +430 -0
  28. package/skills/skill-creator/scripts/__init__.py +0 -0
  29. package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  30. package/skills/skill-creator/scripts/generate_report.py +335 -0
  31. package/skills/skill-creator/scripts/improve_description.py +197 -0
  32. package/skills/skill-creator/scripts/model_backends.py +115 -0
  33. package/skills/skill-creator/scripts/package_skill.py +136 -0
  34. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  35. package/skills/skill-creator/scripts/run_eval.py +363 -0
  36. package/skills/skill-creator/scripts/run_loop.py +319 -0
  37. package/skills/skill-creator/scripts/utils.py +223 -0
@@ -1,8 +1,9 @@
1
- import { RUDDER_MCP_MANAGED_ENV_KEYS, RUDDER_MCP_SERVER_NAME, applyRudderBrowserCapabilityEnv, inferOpenAiCompatibleBiller, pickRudderMcpManagedEnv, rudderMcpRuntimeMetadata, } from "@rudderhq/agent-runtime-utils";
1
+ import { RUDDER_BROWSER_MCP_SERVER_NAME, RUDDER_MCP_MANAGED_ENV_KEYS, RUDDER_MCP_SERVER_NAME, applyRudderBrowserCapabilityEnv, inferOpenAiCompatibleBiller, pickRudderMcpManagedEnv, rudderBrowserMcpRuntimeMetadata, rudderMcpRuntimeMetadata, } from "@rudderhq/agent-runtime-utils";
2
2
  import { applyGitCredentialHelperPolicyEnv, applyGitIdentityPreparationEnv, ensureGitIdentityFileConfig } from "@rudderhq/agent-runtime-utils/git-identity";
3
- import { assertRudderMcpCoreAvailable, preflightRudderMcpServer, } from "@rudderhq/agent-runtime-utils/rudder-mcp-preflight";
4
- import { resolveRudderMcpCliCommand } from "@rudderhq/agent-runtime-utils/rudder-mcp-server";
3
+ import { assertRudderMcpCoreAvailable, preflightRudderBrowserMcpServer, preflightRudderMcpServer, } from "@rudderhq/agent-runtime-utils/rudder-mcp-preflight";
4
+ import { resolveRudderBrowserMcpCliCommand, resolveRudderMcpCliCommand, } from "@rudderhq/agent-runtime-utils/rudder-mcp-server";
5
5
  import { asBoolean, asNumber, asString, asStringArray, buildRudderEnv, cleanupRetiredRudderManagedEntries, ensureAbsoluteDirectory, ensureCommandResolvable, ensurePathInEnv, ensureRudderCliInPath, ensureRudderSkillSymlink, filterRudderDesiredSkillsForBrowserCapability, formatRetiredRudderManagedEntryCleanupWarnings, joinPromptSections, loadAgentInstructionsPrefix, parseObject, prepareAgentInstructionRuntimeContext, pruneLegacyLocalCliCredentialHomeEntries, readRudderRuntimeSkillEntries, redactEnvForLogs, removeUnselectedRudderSkillSymlinks, renderTemplate, resolveLocalOperatorHome, resolveRudderDesiredSkillNames, runChildProcess, selectPromptTemplate, shouldIncludeRuntimeHeartbeatInstructions, } from "@rudderhq/agent-runtime-utils/server-utils";
6
+ import { randomUUID } from "node:crypto";
6
7
  import fs from "node:fs/promises";
7
8
  import os from "node:os";
8
9
  import path from "node:path";
@@ -185,6 +186,7 @@ export async function resolveRudderOpenCodeMcpConfig(managedEnv = {}, verifiedCo
185
186
  ...(rudderMcp.env ?? {}),
186
187
  ...managedEnv,
187
188
  };
189
+ delete env.RUDDER_BROWSER_ENABLED;
188
190
  return {
189
191
  type: "local",
190
192
  command: [rudderMcp.command, ...rudderMcp.args],
@@ -192,20 +194,45 @@ export async function resolveRudderOpenCodeMcpConfig(managedEnv = {}, verifiedCo
192
194
  ...(Object.keys(env).length > 0 ? { environment: env } : {}),
193
195
  };
194
196
  }
197
+ export async function resolveRudderOpenCodeMcpConfigs(managedEnv = {}, verifiedCommand, verifiedBrowserCommand) {
198
+ const configs = {
199
+ [RUDDER_MCP_SERVER_NAME]: await resolveRudderOpenCodeMcpConfig(managedEnv, verifiedCommand),
200
+ };
201
+ if (managedEnv.RUDDER_BROWSER_ENABLED === "true") {
202
+ const browserMcp = verifiedBrowserCommand ?? await resolveRudderBrowserMcpCliCommand(__moduleDir);
203
+ const env = { ...(browserMcp.env ?? {}), ...managedEnv };
204
+ configs[RUDDER_BROWSER_MCP_SERVER_NAME] = {
205
+ type: "local",
206
+ command: [browserMcp.command, ...browserMcp.args],
207
+ enabled: true,
208
+ ...(Object.keys(env).length > 0 ? { environment: env } : {}),
209
+ };
210
+ }
211
+ return configs;
212
+ }
195
213
  async function ensureManagedOpenCodeConfig(input) {
196
- const configDir = path.join(input.targetHome, ".config", "opencode");
214
+ const configDir = input.configPath
215
+ ? path.dirname(input.configPath)
216
+ : path.join(input.targetHome, ".config", "opencode");
197
217
  const existing = await fs.lstat(configDir).catch(() => null);
198
218
  if (existing?.isSymbolicLink()) {
199
219
  await fs.unlink(configDir);
200
220
  }
201
221
  await fs.mkdir(configDir, { recursive: true });
202
- await removeManagedOpenCodeConfigExtensions(configDir);
222
+ if (!input.configPath)
223
+ await removeManagedOpenCodeConfigExtensions(configDir);
203
224
  const config = await readOpenCodeConfigFile(input.sourceHome);
204
- config.mcp = {
205
- [RUDDER_MCP_SERVER_NAME]: await resolveRudderOpenCodeMcpConfig(input.managedEnv ?? {}, input.verifiedCommand),
206
- };
207
- await fs.writeFile(path.join(configDir, MANAGED_OPENCODE_CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
208
- await fs.chmod(path.join(configDir, MANAGED_OPENCODE_CONFIG_FILE), 0o600);
225
+ config.mcp = await resolveRudderOpenCodeMcpConfigs(input.managedEnv ?? {}, input.verifiedCommand, input.verifiedBrowserCommand);
226
+ const configPath = input.configPath ?? path.join(configDir, MANAGED_OPENCODE_CONFIG_FILE);
227
+ const tempPath = `${configPath}.${randomUUID()}.tmp`;
228
+ try {
229
+ await fs.writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
230
+ await fs.rename(tempPath, configPath);
231
+ await fs.chmod(configPath, 0o600);
232
+ }
233
+ finally {
234
+ await fs.rm(tempPath, { force: true });
235
+ }
209
236
  await input.onLog("stdout", `[rudder] Wrote sanitized OpenCode config into adapter-managed runtime state ${configDir}.\n`);
210
237
  }
211
238
  function resolveSharedOpenCodeHomeDir(env) {
@@ -390,434 +417,454 @@ export async function execute(ctx) {
390
417
  const operatorHome = resolveLocalOperatorHome(sourceEnv);
391
418
  if (authToken)
392
419
  env.RUDDER_API_KEY = authToken;
393
- const managedHome = await prepareManagedOpenCodeHome({ ...sourceEnv, ...env }, operatorHome, onLog, agent.orgId, pickRudderMcpManagedEnv(env));
394
- const preparedGitIdentity = await ensureGitIdentityFileConfig({
395
- cwd,
396
- home: managedHome,
397
- sourceEnv,
398
- onLog,
399
- });
400
- env.HOME = operatorHome;
401
- env.USERPROFILE = operatorHome;
402
- env.OPENCODE_CONFIG = path.join(managedHome, ".config", "opencode", MANAGED_OPENCODE_CONFIG_FILE);
403
- env.OPENCODE_DISABLE_CLAUDE_CODE = "true";
404
- env.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT = "true";
405
- env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "true";
406
- env.RUDDER_OPERATOR_HOME = operatorHome;
407
- env.XDG_CONFIG_HOME = path.join(managedHome, ".config");
408
- env.XDG_DATA_HOME = path.join(managedHome, ".local", "share");
409
- env.XDG_CACHE_HOME = path.join(managedHome, ".cache");
410
- applyGitIdentityPreparationEnv(env, preparedGitIdentity);
411
- applyGitCredentialHelperPolicyEnv(env);
412
- const openCodeSkillEntries = await readRudderRuntimeSkillEntries(config, __moduleDir);
413
- const desiredOpenCodeSkillNames = resolveRudderDesiredSkillNames(config, openCodeSkillEntries);
414
- const isChatResultRepair = context.rudderChatResultRepair === true;
415
- const managedOpenCodeSkillsDir = resolveManagedOpenCodeSkillsDir(managedHome);
416
- const runtimeEnv = Object.fromEntries(Object.entries(ensurePathInEnv(await ensureRudderCliInPath(__moduleDir, { ...process.env, ...env })))
417
- .filter((entry) => typeof entry[1] === "string"));
418
- for (const key of OPENCODE_INHERITED_ENV_BLOCKLIST) {
419
- delete runtimeEnv[key];
420
- }
421
- for (const key of RUDDER_MCP_MANAGED_ENV_KEYS) {
422
- const value = env[key];
423
- if (typeof value === "string" && value.trim().length > 0)
424
- runtimeEnv[key] = value;
425
- else
420
+ const managedHome = await prepareManagedOpenCodeHome({ ...sourceEnv, ...env }, operatorHome, onLog, agent.orgId, {});
421
+ const runtimeTmpDir = path.join(managedHome, "runtime-tmp", runId);
422
+ await fs.mkdir(runtimeTmpDir, { recursive: true });
423
+ try {
424
+ const runConfigPath = path.join(runtimeTmpDir, MANAGED_OPENCODE_CONFIG_FILE);
425
+ const preparedGitIdentity = await ensureGitIdentityFileConfig({
426
+ cwd,
427
+ home: managedHome,
428
+ sourceEnv,
429
+ onLog,
430
+ });
431
+ env.HOME = operatorHome;
432
+ env.USERPROFILE = operatorHome;
433
+ env.OPENCODE_CONFIG = runConfigPath;
434
+ env.OPENCODE_DISABLE_CLAUDE_CODE = "true";
435
+ env.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT = "true";
436
+ env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "true";
437
+ env.RUDDER_OPERATOR_HOME = operatorHome;
438
+ env.XDG_CONFIG_HOME = path.join(managedHome, ".config");
439
+ env.XDG_DATA_HOME = path.join(managedHome, ".local", "share");
440
+ env.XDG_CACHE_HOME = path.join(managedHome, ".cache");
441
+ applyGitIdentityPreparationEnv(env, preparedGitIdentity);
442
+ applyGitCredentialHelperPolicyEnv(env);
443
+ const openCodeSkillEntries = await readRudderRuntimeSkillEntries(config, __moduleDir);
444
+ const desiredOpenCodeSkillNames = resolveRudderDesiredSkillNames(config, openCodeSkillEntries);
445
+ const isChatResultRepair = context.rudderChatResultRepair === true;
446
+ const managedOpenCodeSkillsDir = resolveManagedOpenCodeSkillsDir(managedHome);
447
+ const runtimeEnv = Object.fromEntries(Object.entries(ensurePathInEnv(await ensureRudderCliInPath(__moduleDir, { ...process.env, ...env })))
448
+ .filter((entry) => typeof entry[1] === "string"));
449
+ for (const key of OPENCODE_INHERITED_ENV_BLOCKLIST) {
426
450
  delete runtimeEnv[key];
427
- }
428
- const rudderMcpCommand = await resolveRudderMcpCliCommand(__moduleDir);
429
- const rudderMcpPreflight = await preflightRudderMcpServer({
430
- command: rudderMcpCommand,
431
- runtimeEnv,
432
- managedEnv: pickRudderMcpManagedEnv(env),
433
- browserEnabled,
434
- });
435
- assertRudderMcpCoreAvailable(rudderMcpPreflight);
436
- delete runtimeEnv.RUDDER_DESKTOP_CLI_ENTRY;
437
- delete runtimeEnv.RUDDER_MCP_RUDDER_BIN;
438
- if (browserEnabled && !rudderMcpPreflight?.browserAvailable) {
439
- browserEnabled = false;
440
- env.RUDDER_BROWSER_ENABLED = "false";
441
- runtimeEnv.RUDDER_BROWSER_ENABLED = "false";
442
- await onLog("stderr", `[rudder] ${rudderMcpPreflight?.diagnostic}\n`);
443
- }
444
- const effectiveDesiredOpenCodeSkillNames = filterRudderDesiredSkillsForBrowserCapability(openCodeSkillEntries, desiredOpenCodeSkillNames, browserEnabled);
445
- const selectedOpenCodeSkillEntries = isChatResultRepair
446
- ? []
447
- : openCodeSkillEntries.filter((entry) => effectiveDesiredOpenCodeSkillNames.includes(entry.key));
448
- const loadedSkills = openCodeSkillEntries
449
- .filter((entry) => effectiveDesiredOpenCodeSkillNames.includes(entry.key))
450
- .map((entry) => ({
451
- key: entry.key,
452
- runtimeName: entry.runtimeName,
453
- name: entry.name ?? null,
454
- description: entry.description ?? null,
455
- }));
456
- await ensureOpenCodeSkillsInjected(onLog, managedOpenCodeSkillsDir, openCodeSkillEntries, effectiveDesiredOpenCodeSkillNames);
457
- const selectedSkillPrompt = await renderSelectedOpenCodeSkillPrompt(onLog, managedOpenCodeSkillsDir, selectedOpenCodeSkillEntries);
458
- await ensureManagedOpenCodeConfig({
459
- sourceHome: operatorHome,
460
- targetHome: managedHome,
461
- managedEnv: pickRudderMcpManagedEnv(env),
462
- verifiedCommand: rudderMcpCommand,
463
- onLog,
464
- });
465
- await ensureCommandResolvable(command, cwd, runtimeEnv);
466
- validateOpenCodeModelConfig({ model });
467
- const configuredTimeoutSec = asNumber(config.timeoutSec, 0);
468
- const graceSec = asNumber(config.graceSec, 20);
469
- const extraArgs = (() => {
470
- const fromExtraArgs = asStringArray(config.extraArgs);
471
- if (fromExtraArgs.length > 0)
472
- return fromExtraArgs;
473
- return asStringArray(config.args);
474
- })();
475
- const runtimeSessionParams = parseObject(runtime.sessionParams);
476
- const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
477
- const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
478
- const canResumeSession = runtimeSessionId.length > 0 &&
479
- (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
480
- const sessionId = canResumeSession ? runtimeSessionId : null;
481
- if (runtimeSessionId && !canResumeSession) {
482
- await onLog("stdout", `[rudder] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`);
483
- }
484
- const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
485
- const resolvedInstructionsFilePath = instructionsFilePath
486
- ? path.resolve(cwd, instructionsFilePath)
487
- : "";
488
- const instructionRuntimeContext = prepareAgentInstructionRuntimeContext(context);
489
- const loadedInstructions = await loadAgentInstructionsPrefix({
490
- instructionsFilePath: resolvedInstructionsFilePath,
491
- includeHeartbeatInstructions: shouldIncludeRuntimeHeartbeatInstructions(context),
492
- contextSectionsBeforeCurrentTime: instructionRuntimeContext.contextSectionsBeforeCurrentTime,
493
- onLog,
494
- });
495
- const instructionsPrefix = loadedInstructions.prefix;
496
- const instructionsDir = loadedInstructions.instructionsDir;
497
- const commandNotes = (() => {
498
- const rudderMcpNote = "Configured first-party Rudder MCP tools for OpenCode.";
499
- if (!resolvedInstructionsFilePath) {
500
- return [
501
- ...loadedInstructions.commandNotes,
502
- rudderMcpNote,
503
- "Prepended Rudder operating contract to stdin prompt.",
504
- ];
505
451
  }
506
- if (instructionsPrefix.length > 0) {
452
+ for (const key of RUDDER_MCP_MANAGED_ENV_KEYS) {
453
+ const value = env[key];
454
+ if (typeof value === "string" && value.trim().length > 0)
455
+ runtimeEnv[key] = value;
456
+ else
457
+ delete runtimeEnv[key];
458
+ }
459
+ const rudderMcpCommand = await resolveRudderMcpCliCommand(__moduleDir);
460
+ const rudderMcpPreflight = await preflightRudderMcpServer({
461
+ command: rudderMcpCommand,
462
+ runtimeEnv,
463
+ managedEnv: pickRudderMcpManagedEnv(env),
464
+ browserEnabled: false,
465
+ });
466
+ assertRudderMcpCoreAvailable(rudderMcpPreflight);
467
+ delete runtimeEnv.RUDDER_DESKTOP_CLI_ENTRY;
468
+ delete runtimeEnv.RUDDER_MCP_RUDDER_BIN;
469
+ const browserMcpCommand = browserEnabled ? await resolveRudderBrowserMcpCliCommand(__moduleDir) : null;
470
+ const browserMcpPreflight = browserMcpCommand
471
+ ? await preflightRudderBrowserMcpServer({
472
+ command: browserMcpCommand,
473
+ runtimeEnv,
474
+ managedEnv: pickRudderMcpManagedEnv(env),
475
+ })
476
+ : null;
477
+ if (browserEnabled && !browserMcpPreflight?.browserAvailable) {
478
+ browserEnabled = false;
479
+ env.RUDDER_BROWSER_ENABLED = "false";
480
+ runtimeEnv.RUDDER_BROWSER_ENABLED = "false";
481
+ await onLog("stderr", `[rudder] ${browserMcpPreflight?.diagnostic}\n`);
482
+ }
483
+ const effectiveDesiredOpenCodeSkillNames = filterRudderDesiredSkillsForBrowserCapability(openCodeSkillEntries, desiredOpenCodeSkillNames, browserEnabled);
484
+ const selectedOpenCodeSkillEntries = isChatResultRepair
485
+ ? []
486
+ : openCodeSkillEntries.filter((entry) => effectiveDesiredOpenCodeSkillNames.includes(entry.key));
487
+ const loadedSkills = openCodeSkillEntries
488
+ .filter((entry) => effectiveDesiredOpenCodeSkillNames.includes(entry.key))
489
+ .map((entry) => ({
490
+ key: entry.key,
491
+ runtimeName: entry.runtimeName,
492
+ name: entry.name ?? null,
493
+ description: entry.description ?? null,
494
+ }));
495
+ await ensureOpenCodeSkillsInjected(onLog, managedOpenCodeSkillsDir, openCodeSkillEntries, effectiveDesiredOpenCodeSkillNames);
496
+ const selectedSkillPrompt = await renderSelectedOpenCodeSkillPrompt(onLog, managedOpenCodeSkillsDir, selectedOpenCodeSkillEntries);
497
+ await ensureManagedOpenCodeConfig({
498
+ sourceHome: operatorHome,
499
+ targetHome: managedHome,
500
+ configPath: runConfigPath,
501
+ managedEnv: pickRudderMcpManagedEnv(env),
502
+ verifiedCommand: rudderMcpCommand,
503
+ verifiedBrowserCommand: browserMcpCommand ?? undefined,
504
+ onLog,
505
+ });
506
+ await ensureCommandResolvable(command, cwd, runtimeEnv);
507
+ validateOpenCodeModelConfig({ model });
508
+ const configuredTimeoutSec = asNumber(config.timeoutSec, 0);
509
+ const graceSec = asNumber(config.graceSec, 20);
510
+ const extraArgs = (() => {
511
+ const fromExtraArgs = asStringArray(config.extraArgs);
512
+ if (fromExtraArgs.length > 0)
513
+ return fromExtraArgs;
514
+ return asStringArray(config.args);
515
+ })();
516
+ const runtimeSessionParams = parseObject(runtime.sessionParams);
517
+ const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
518
+ const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
519
+ const canResumeSession = runtimeSessionId.length > 0 &&
520
+ (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd));
521
+ const sessionId = canResumeSession ? runtimeSessionId : null;
522
+ if (runtimeSessionId && !canResumeSession) {
523
+ await onLog("stdout", `[rudder] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`);
524
+ }
525
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
526
+ const resolvedInstructionsFilePath = instructionsFilePath
527
+ ? path.resolve(cwd, instructionsFilePath)
528
+ : "";
529
+ const instructionRuntimeContext = prepareAgentInstructionRuntimeContext(context);
530
+ const loadedInstructions = await loadAgentInstructionsPrefix({
531
+ instructionsFilePath: resolvedInstructionsFilePath,
532
+ includeHeartbeatInstructions: shouldIncludeRuntimeHeartbeatInstructions(context),
533
+ contextSectionsBeforeCurrentTime: instructionRuntimeContext.contextSectionsBeforeCurrentTime,
534
+ onLog,
535
+ });
536
+ const instructionsPrefix = loadedInstructions.prefix;
537
+ const instructionsDir = loadedInstructions.instructionsDir;
538
+ const commandNotes = (() => {
539
+ const rudderMcpNote = "Configured first-party Rudder MCP tools for OpenCode.";
540
+ if (!resolvedInstructionsFilePath) {
541
+ return [
542
+ ...loadedInstructions.commandNotes,
543
+ rudderMcpNote,
544
+ "Prepended Rudder operating contract to stdin prompt.",
545
+ ];
546
+ }
547
+ if (instructionsPrefix.length > 0) {
548
+ return [
549
+ ...loadedInstructions.commandNotes,
550
+ rudderMcpNote,
551
+ `Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
552
+ ];
553
+ }
507
554
  return [
508
555
  ...loadedInstructions.commandNotes,
509
556
  rudderMcpNote,
510
- `Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
511
557
  ];
512
- }
513
- return [
514
- ...loadedInstructions.commandNotes,
515
- rudderMcpNote,
516
- ];
517
- })();
518
- /**
519
- * Final prompt assembly order is intentional and shared across runtimes:
520
- * 1) optional injected instructions prefix,
521
- * 2) optional bootstrap prompt (only when not resuming a prior session),
522
- * 3) optional session handoff markdown,
523
- * 4) heartbeat prompt selected by wake trigger (assignment, mention, retry, fallback).
524
- *
525
- * Prompt example (assignment wakeup):
526
- * [instructions prefix]
527
- * [bootstrap prompt]
528
- * [session handoff note]
529
- * You are agent agent-123 (Frontend Maintainer). You have been assigned to work on an issue.
530
- * Issue: "Fix onboarding redirect"
531
- *
532
- * Reasoning: assignment/mention heartbeat templates carry issue/comment context so
533
- * the agent can start useful work on turn one without spending extra tool calls on
534
- * "what changed?" discovery.
535
- *
536
- * Traceability:
537
- * - doc/engineering/DEVELOPING.md
538
- */
539
- const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
540
- const templateData = {
541
- agentId: agent.id,
542
- orgId: agent.orgId,
543
- runId,
544
- organization: { id: agent.orgId },
545
- agent,
546
- run: {
547
- id: runId,
548
- source: context.wakeSource ?? "on_demand",
558
+ })();
559
+ /**
560
+ * Final prompt assembly order is intentional and shared across runtimes:
561
+ * 1) optional injected instructions prefix,
562
+ * 2) optional bootstrap prompt (only when not resuming a prior session),
563
+ * 3) optional session handoff markdown,
564
+ * 4) heartbeat prompt selected by wake trigger (assignment, mention, retry, fallback).
565
+ *
566
+ * Prompt example (assignment wakeup):
567
+ * [instructions prefix]
568
+ * [bootstrap prompt]
569
+ * [session handoff note]
570
+ * You are agent agent-123 (Frontend Maintainer). You have been assigned to work on an issue.
571
+ * Issue: "Fix onboarding redirect"
572
+ *
573
+ * Reasoning: assignment/mention heartbeat templates carry issue/comment context so
574
+ * the agent can start useful work on turn one without spending extra tool calls on
575
+ * "what changed?" discovery.
576
+ *
577
+ * Traceability:
578
+ * - doc/engineering/DEVELOPING.md
579
+ */
580
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
581
+ const templateData = {
582
+ agentId: agent.id,
583
+ orgId: agent.orgId,
584
+ runId,
585
+ organization: { id: agent.orgId },
586
+ agent,
587
+ run: {
588
+ id: runId,
589
+ source: context.wakeSource ?? "on_demand",
590
+ wakeReason: context.wakeReason ?? null,
591
+ },
592
+ context: instructionRuntimeContext.promptContext,
593
+ // Issue and comment context for enriched prompts
594
+ issue: context.issue ?? null,
595
+ comment: context.comment ?? null,
549
596
  wakeReason: context.wakeReason ?? null,
550
- },
551
- context: instructionRuntimeContext.promptContext,
552
- // Issue and comment context for enriched prompts
553
- issue: context.issue ?? null,
554
- comment: context.comment ?? null,
555
- wakeReason: context.wakeReason ?? null,
556
- wakeSource: context.wakeSource ?? null,
557
- };
558
- const renderedPrompt = renderTemplate(promptTemplate, templateData);
559
- const renderedBootstrapPrompt = !sessionId && bootstrapPromptTemplate.trim().length > 0
560
- ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
561
- : "";
562
- const sessionHandoffNote = asString(context.rudderSessionHandoffMarkdown, "").trim();
563
- const prompt = joinPromptSections([
564
- instructionsPrefix,
565
- selectedSkillPrompt,
566
- renderedBootstrapPrompt,
567
- sessionHandoffNote,
568
- renderedPrompt,
569
- ]);
570
- const promptMetrics = {
571
- promptChars: prompt.length,
572
- ...loadedInstructions.metrics,
573
- bootstrapPromptChars: renderedBootstrapPrompt.length,
574
- sessionHandoffChars: sessionHandoffNote.length,
575
- heartbeatPromptChars: renderedPrompt.length,
576
- };
577
- const runtimeTmpDir = path.join(managedHome, "runtime-tmp", runId);
578
- await fs.mkdir(runtimeTmpDir, { recursive: true });
579
- const promptFilePath = path.join(runtimeTmpDir, "rudder-prompt.md");
580
- await fs.writeFile(promptFilePath, prompt, "utf8");
581
- const useStdinPrompt = context.chatMode === true;
582
- const hasChatModeTimeoutFallback = useStdinPrompt && configuredTimeoutSec <= 0;
583
- const timeoutSec = hasChatModeTimeoutFallback
584
- ? CHAT_MODE_DEFAULT_TIMEOUT_SEC
585
- : configuredTimeoutSec;
586
- const toolLoopIdleTimeoutSec = Math.max(1, asNumber(config.toolLoopIdleTimeoutSec, asNumber(env.RUDDER_OPENCODE_TOOL_LOOP_IDLE_TIMEOUT_SEC, DEFAULT_TOOL_LOOP_IDLE_TIMEOUT_SEC)));
587
- const startupIdleTimeoutSec = Math.max(1, asNumber(config.startupIdleTimeoutSec, asNumber(env.RUDDER_OPENCODE_STARTUP_IDLE_TIMEOUT_SEC, DEFAULT_STARTUP_IDLE_TIMEOUT_SEC)));
588
- const effectiveCommandNotes = hasChatModeTimeoutFallback
589
- ? [
590
- ...commandNotes,
591
- `Applied ${CHAT_MODE_DEFAULT_TIMEOUT_SEC}s default timeout for OpenCode chat mode because timeoutSec was unset.`,
592
- ]
593
- : commandNotes;
594
- const buildArgs = (resumeSessionId, redactPromptFile) => {
595
- const args = ["run", "--pure", "--format", "json", "--dir", cwd];
596
- if (!useStdinPrompt)
597
- args.push(OPENCODE_PROMPT_FILE_MESSAGE);
598
- if (resumeSessionId)
599
- args.push("--session", resumeSessionId);
600
- if (model)
601
- args.push("--model", model);
602
- if (variant)
603
- args.push("--variant", variant);
604
- if (dangerouslySkipPermissions)
605
- args.push("--dangerously-skip-permissions");
606
- if (extraArgs.length > 0)
607
- args.push(...extraArgs);
608
- if (!useStdinPrompt) {
609
- args.push("--file", redactPromptFile ? `<rudder prompt file ${prompt.length} chars>` : promptFilePath);
610
- }
611
- return args;
612
- };
613
- const runAttempt = async (resumeSessionId) => {
614
- const args = buildArgs(resumeSessionId, false);
615
- if (onMeta) {
616
- await onMeta({
617
- agentRuntimeType: "opencode_local",
618
- command,
619
- cwd,
620
- commandNotes: effectiveCommandNotes,
621
- commandArgs: buildArgs(resumeSessionId, true),
622
- env: redactEnvForLogs(env),
623
- prompt,
624
- agentInstructionStack: prompt,
625
- promptMetrics,
626
- loadedSkills,
627
- realizedSkills: loadedSkills,
628
- promptInjectedSkills: loadedSkills,
629
- rudderMcp: rudderMcpRuntimeMetadata({ browserEnabled, preflight: rudderMcpPreflight }),
630
- context,
631
- });
632
- }
633
- let stdoutBuffer = "";
634
- let sawTerminalContent = false;
635
- let sawTerminalCompletionSummary = false;
636
- let stoppedAfterTerminalText = false;
637
- let stoppedAfterToolLoopIdle = false;
638
- let stoppedAfterStartupIdle = false;
639
- const terminalStopController = new AbortController();
640
- let startupIdleTimer = null;
641
- let toolLoopIdleTimer = null;
642
- const clearStartupIdleTimer = () => {
643
- if (!startupIdleTimer)
644
- return;
645
- clearTimeout(startupIdleTimer);
646
- startupIdleTimer = null;
597
+ wakeSource: context.wakeSource ?? null,
647
598
  };
648
- const clearToolLoopIdleTimer = () => {
649
- if (!toolLoopIdleTimer)
650
- return;
651
- clearTimeout(toolLoopIdleTimer);
652
- toolLoopIdleTimer = null;
599
+ const renderedPrompt = renderTemplate(promptTemplate, templateData);
600
+ const renderedBootstrapPrompt = !sessionId && bootstrapPromptTemplate.trim().length > 0
601
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
602
+ : "";
603
+ const sessionHandoffNote = asString(context.rudderSessionHandoffMarkdown, "").trim();
604
+ const prompt = joinPromptSections([
605
+ instructionsPrefix,
606
+ selectedSkillPrompt,
607
+ renderedBootstrapPrompt,
608
+ sessionHandoffNote,
609
+ renderedPrompt,
610
+ ]);
611
+ const promptMetrics = {
612
+ promptChars: prompt.length,
613
+ ...loadedInstructions.metrics,
614
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
615
+ sessionHandoffChars: sessionHandoffNote.length,
616
+ heartbeatPromptChars: renderedPrompt.length,
653
617
  };
654
- const armToolLoopIdleTimer = () => {
655
- clearToolLoopIdleTimer();
656
- toolLoopIdleTimer = setTimeout(() => {
657
- stoppedAfterToolLoopIdle = true;
658
- if (!terminalStopController.signal.aborted)
659
- terminalStopController.abort();
660
- }, toolLoopIdleTimeoutSec * 1000);
618
+ const promptFilePath = path.join(runtimeTmpDir, "rudder-prompt.md");
619
+ await fs.writeFile(promptFilePath, prompt, "utf8");
620
+ const useStdinPrompt = context.chatMode === true;
621
+ const hasChatModeTimeoutFallback = useStdinPrompt && configuredTimeoutSec <= 0;
622
+ const timeoutSec = hasChatModeTimeoutFallback
623
+ ? CHAT_MODE_DEFAULT_TIMEOUT_SEC
624
+ : configuredTimeoutSec;
625
+ const toolLoopIdleTimeoutSec = Math.max(1, asNumber(config.toolLoopIdleTimeoutSec, asNumber(env.RUDDER_OPENCODE_TOOL_LOOP_IDLE_TIMEOUT_SEC, DEFAULT_TOOL_LOOP_IDLE_TIMEOUT_SEC)));
626
+ const startupIdleTimeoutSec = Math.max(1, asNumber(config.startupIdleTimeoutSec, asNumber(env.RUDDER_OPENCODE_STARTUP_IDLE_TIMEOUT_SEC, DEFAULT_STARTUP_IDLE_TIMEOUT_SEC)));
627
+ const effectiveCommandNotes = hasChatModeTimeoutFallback
628
+ ? [
629
+ ...commandNotes,
630
+ `Applied ${CHAT_MODE_DEFAULT_TIMEOUT_SEC}s default timeout for OpenCode chat mode because timeoutSec was unset.`,
631
+ ]
632
+ : commandNotes;
633
+ const buildArgs = (resumeSessionId, redactPromptFile) => {
634
+ const args = ["run", "--pure", "--format", "json", "--dir", cwd];
635
+ if (!useStdinPrompt)
636
+ args.push(OPENCODE_PROMPT_FILE_MESSAGE);
637
+ if (resumeSessionId)
638
+ args.push("--session", resumeSessionId);
639
+ if (model)
640
+ args.push("--model", model);
641
+ if (variant)
642
+ args.push("--variant", variant);
643
+ if (dangerouslySkipPermissions)
644
+ args.push("--dangerously-skip-permissions");
645
+ if (extraArgs.length > 0)
646
+ args.push(...extraArgs);
647
+ if (!useStdinPrompt) {
648
+ args.push("--file", redactPromptFile ? `<rudder prompt file ${prompt.length} chars>` : promptFilePath);
649
+ }
650
+ return args;
661
651
  };
662
- startupIdleTimer = setTimeout(() => {
663
- stoppedAfterStartupIdle = true;
664
- if (!terminalStopController.signal.aborted)
665
- terminalStopController.abort();
666
- }, startupIdleTimeoutSec * 1000);
667
- const abortSignals = [
668
- ctx.abortSignal,
669
- terminalStopController.signal,
670
- ].filter((signal) => Boolean(signal));
671
- const combinedAbortSignal = abortSignals.length > 0
672
- ? AbortSignal.any(abortSignals)
673
- : undefined;
674
- const bufferedOnLog = async (stream, chunk) => {
675
- if (stream !== "stdout") {
676
- await onLog(stream, chunk);
677
- return;
652
+ const runAttempt = async (resumeSessionId) => {
653
+ const args = buildArgs(resumeSessionId, false);
654
+ if (onMeta) {
655
+ await onMeta({
656
+ agentRuntimeType: "opencode_local",
657
+ command,
658
+ cwd,
659
+ commandNotes: effectiveCommandNotes,
660
+ commandArgs: buildArgs(resumeSessionId, true),
661
+ env: redactEnvForLogs(env),
662
+ prompt,
663
+ agentInstructionStack: prompt,
664
+ promptMetrics,
665
+ loadedSkills,
666
+ realizedSkills: loadedSkills,
667
+ promptInjectedSkills: loadedSkills,
668
+ rudderMcp: rudderMcpRuntimeMetadata({ browserEnabled, preflight: rudderMcpPreflight }),
669
+ browserMcp: rudderBrowserMcpRuntimeMetadata({
670
+ available: browserEnabled,
671
+ preflight: browserMcpPreflight,
672
+ }),
673
+ context,
674
+ });
678
675
  }
679
- stdoutBuffer += chunk;
680
- const lines = stdoutBuffer.split(/\r?\n/);
681
- stdoutBuffer = lines.pop() ?? "";
682
- for (const line of lines) {
683
- const parsedLine = parseOpenCodeJsonlLine(line.trim());
684
- if (parsedLine)
685
- clearStartupIdleTimer();
686
- if (parsedLine?.type === "assistantText" && parsedLine.text.trim().length > 0) {
687
- sawTerminalContent = true;
688
- }
689
- const sawCompletionSummary = parsedLine?.type === "syntheticText" &&
690
- parsedLine.completion &&
691
- parsedLine.text.trim().length > 0;
692
- if (parsedLine && parsedLine.type !== "other")
693
- clearToolLoopIdleTimer();
694
- if (sawCompletionSummary) {
695
- sawTerminalCompletionSummary = true;
696
- }
697
- if (parsedLine?.type === "toolCallsStepFinish") {
698
- armToolLoopIdleTimer();
699
- }
700
- if (parsedLine?.type === "terminalStop" && sawTerminalContent && !terminalStopController.signal.aborted) {
701
- stoppedAfterTerminalText = true;
676
+ let stdoutBuffer = "";
677
+ let sawTerminalContent = false;
678
+ let sawTerminalCompletionSummary = false;
679
+ let stoppedAfterTerminalText = false;
680
+ let stoppedAfterToolLoopIdle = false;
681
+ let stoppedAfterStartupIdle = false;
682
+ const terminalStopController = new AbortController();
683
+ let startupIdleTimer = null;
684
+ let toolLoopIdleTimer = null;
685
+ const clearStartupIdleTimer = () => {
686
+ if (!startupIdleTimer)
687
+ return;
688
+ clearTimeout(startupIdleTimer);
689
+ startupIdleTimer = null;
690
+ };
691
+ const clearToolLoopIdleTimer = () => {
692
+ if (!toolLoopIdleTimer)
693
+ return;
694
+ clearTimeout(toolLoopIdleTimer);
695
+ toolLoopIdleTimer = null;
696
+ };
697
+ const armToolLoopIdleTimer = () => {
698
+ clearToolLoopIdleTimer();
699
+ toolLoopIdleTimer = setTimeout(() => {
700
+ stoppedAfterToolLoopIdle = true;
701
+ if (!terminalStopController.signal.aborted)
702
+ terminalStopController.abort();
703
+ }, toolLoopIdleTimeoutSec * 1000);
704
+ };
705
+ startupIdleTimer = setTimeout(() => {
706
+ stoppedAfterStartupIdle = true;
707
+ if (!terminalStopController.signal.aborted)
702
708
  terminalStopController.abort();
709
+ }, startupIdleTimeoutSec * 1000);
710
+ const abortSignals = [
711
+ ctx.abortSignal,
712
+ terminalStopController.signal,
713
+ ].filter((signal) => Boolean(signal));
714
+ const combinedAbortSignal = abortSignals.length > 0
715
+ ? AbortSignal.any(abortSignals)
716
+ : undefined;
717
+ const bufferedOnLog = async (stream, chunk) => {
718
+ if (stream !== "stdout") {
719
+ await onLog(stream, chunk);
720
+ return;
703
721
  }
704
- if (parsedLine?.type === "terminalStop" && sawTerminalCompletionSummary && !terminalStopController.signal.aborted) {
705
- stoppedAfterTerminalText = true;
706
- terminalStopController.abort();
722
+ stdoutBuffer += chunk;
723
+ const lines = stdoutBuffer.split(/\r?\n/);
724
+ stdoutBuffer = lines.pop() ?? "";
725
+ for (const line of lines) {
726
+ const parsedLine = parseOpenCodeJsonlLine(line.trim());
727
+ if (parsedLine)
728
+ clearStartupIdleTimer();
729
+ if (parsedLine?.type === "assistantText" && parsedLine.text.trim().length > 0) {
730
+ sawTerminalContent = true;
731
+ }
732
+ const sawCompletionSummary = parsedLine?.type === "syntheticText" &&
733
+ parsedLine.completion &&
734
+ parsedLine.text.trim().length > 0;
735
+ if (parsedLine && parsedLine.type !== "other")
736
+ clearToolLoopIdleTimer();
737
+ if (sawCompletionSummary) {
738
+ sawTerminalCompletionSummary = true;
739
+ }
740
+ if (parsedLine?.type === "toolCallsStepFinish") {
741
+ armToolLoopIdleTimer();
742
+ }
743
+ if (parsedLine?.type === "terminalStop" && sawTerminalContent && !terminalStopController.signal.aborted) {
744
+ stoppedAfterTerminalText = true;
745
+ terminalStopController.abort();
746
+ }
747
+ if (parsedLine?.type === "terminalStop" && sawTerminalCompletionSummary && !terminalStopController.signal.aborted) {
748
+ stoppedAfterTerminalText = true;
749
+ terminalStopController.abort();
750
+ }
707
751
  }
708
- }
709
- await onLog(stream, chunk);
710
- };
711
- const proc = await runChildProcess(runId, command, args, {
712
- cwd,
713
- env: runtimeEnv,
714
- timeoutSec,
715
- graceSec,
716
- stdin: useStdinPrompt ? prompt : undefined,
717
- onSpawn,
718
- abortSignal: combinedAbortSignal,
719
- onLog: bufferedOnLog,
720
- })
721
- .finally(() => {
722
- clearStartupIdleTimer();
723
- clearToolLoopIdleTimer();
724
- });
725
- return {
726
- proc: stoppedAfterTerminalText && !proc.timedOut && proc.signal === "SIGTERM"
727
- ? { ...proc, exitCode: 0, signal: null }
728
- : proc,
729
- rawStderr: proc.stderr,
730
- parsed: parseOpenCodeJsonl(proc.stdout),
731
- stoppedAfterTerminalText,
732
- stoppedAfterToolLoopIdle,
733
- stoppedAfterStartupIdle,
752
+ await onLog(stream, chunk);
753
+ };
754
+ const proc = await runChildProcess(runId, command, args, {
755
+ cwd,
756
+ env: runtimeEnv,
757
+ timeoutSec,
758
+ graceSec,
759
+ stdin: useStdinPrompt ? prompt : undefined,
760
+ onSpawn,
761
+ abortSignal: combinedAbortSignal,
762
+ onLog: bufferedOnLog,
763
+ })
764
+ .finally(() => {
765
+ clearStartupIdleTimer();
766
+ clearToolLoopIdleTimer();
767
+ });
768
+ return {
769
+ proc: stoppedAfterTerminalText && !proc.timedOut && proc.signal === "SIGTERM"
770
+ ? { ...proc, exitCode: 0, signal: null }
771
+ : proc,
772
+ rawStderr: proc.stderr,
773
+ parsed: parseOpenCodeJsonl(proc.stdout),
774
+ stoppedAfterTerminalText,
775
+ stoppedAfterToolLoopIdle,
776
+ stoppedAfterStartupIdle,
777
+ };
734
778
  };
735
- };
736
- const toResult = (attempt, clearSessionOnMissingSession = false) => {
737
- if (attempt.proc.timedOut) {
779
+ const toResult = (attempt, clearSessionOnMissingSession = false) => {
780
+ if (attempt.proc.timedOut) {
781
+ return {
782
+ exitCode: attempt.proc.exitCode,
783
+ signal: attempt.proc.signal,
784
+ timedOut: true,
785
+ errorMessage: `Timed out after ${timeoutSec}s`,
786
+ clearSession: clearSessionOnMissingSession,
787
+ };
788
+ }
789
+ const resolvedSessionId = attempt.parsed.sessionId ??
790
+ (clearSessionOnMissingSession ? null : runtimeSessionId ?? runtime.sessionId ?? null);
791
+ const resolvedSessionParams = resolvedSessionId
792
+ ? {
793
+ sessionId: resolvedSessionId,
794
+ cwd,
795
+ ...(workspaceId ? { workspaceId } : {}),
796
+ ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
797
+ ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
798
+ }
799
+ : null;
800
+ const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
801
+ const rawExitCode = attempt.proc.exitCode;
802
+ const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
803
+ const parsedSummary = attempt.parsed.summary.trim() || attempt.parsed.completionSummary?.trim() || "";
804
+ const startupIdle = attempt.stoppedAfterStartupIdle === true;
805
+ const toolLoopIdle = attempt.stoppedAfterToolLoopIdle === true;
806
+ const missingFinalSummary = !parsedError &&
807
+ !startupIdle &&
808
+ !toolLoopIdle &&
809
+ (rawExitCode ?? 0) === 0 &&
810
+ parsedSummary.length === 0;
811
+ const synthesizedExitCode = (parsedError || missingFinalSummary || startupIdle || toolLoopIdle) && (rawExitCode ?? 0) === 0 ? 1 : rawExitCode;
812
+ const fallbackErrorMessage = parsedError ||
813
+ (startupIdle
814
+ ? `OpenCode stopped after ${startupIdleTimeoutSec}s without emitting JSON output.`
815
+ : "") ||
816
+ (toolLoopIdle
817
+ ? `OpenCode stopped after ${toolLoopIdleTimeoutSec}s without continuing after a Rudder tool-call step.`
818
+ : "") ||
819
+ (missingFinalSummary
820
+ ? "OpenCode completed without a final text summary; Rudder requires final text to persist a trustworthy run result."
821
+ : "") ||
822
+ stderrLine ||
823
+ `OpenCode exited with code ${synthesizedExitCode ?? -1}`;
824
+ const modelId = model || null;
738
825
  return {
739
- exitCode: attempt.proc.exitCode,
826
+ exitCode: synthesizedExitCode,
740
827
  signal: attempt.proc.signal,
741
- timedOut: true,
742
- errorMessage: `Timed out after ${timeoutSec}s`,
743
- clearSession: clearSessionOnMissingSession,
744
- };
745
- }
746
- const resolvedSessionId = attempt.parsed.sessionId ??
747
- (clearSessionOnMissingSession ? null : runtimeSessionId ?? runtime.sessionId ?? null);
748
- const resolvedSessionParams = resolvedSessionId
749
- ? {
828
+ timedOut: false,
829
+ errorMessage: (synthesizedExitCode ?? 0) === 0 ? null : fallbackErrorMessage,
830
+ errorCode: startupIdle ? "opencode_startup_idle" : toolLoopIdle ? "opencode_tool_loop_idle" : null,
831
+ usage: {
832
+ inputTokens: attempt.parsed.usage.inputTokens,
833
+ outputTokens: attempt.parsed.usage.outputTokens,
834
+ cachedInputTokens: attempt.parsed.usage.cachedInputTokens,
835
+ },
750
836
  sessionId: resolvedSessionId,
751
- cwd,
752
- ...(workspaceId ? { workspaceId } : {}),
753
- ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
754
- ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
755
- }
756
- : null;
757
- const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
758
- const rawExitCode = attempt.proc.exitCode;
759
- const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
760
- const parsedSummary = attempt.parsed.summary.trim() || attempt.parsed.completionSummary?.trim() || "";
761
- const startupIdle = attempt.stoppedAfterStartupIdle === true;
762
- const toolLoopIdle = attempt.stoppedAfterToolLoopIdle === true;
763
- const missingFinalSummary = !parsedError &&
764
- !startupIdle &&
765
- !toolLoopIdle &&
766
- (rawExitCode ?? 0) === 0 &&
767
- parsedSummary.length === 0;
768
- const synthesizedExitCode = (parsedError || missingFinalSummary || startupIdle || toolLoopIdle) && (rawExitCode ?? 0) === 0 ? 1 : rawExitCode;
769
- const fallbackErrorMessage = parsedError ||
770
- (startupIdle
771
- ? `OpenCode stopped after ${startupIdleTimeoutSec}s without emitting JSON output.`
772
- : "") ||
773
- (toolLoopIdle
774
- ? `OpenCode stopped after ${toolLoopIdleTimeoutSec}s without continuing after a Rudder tool-call step.`
775
- : "") ||
776
- (missingFinalSummary
777
- ? "OpenCode completed without a final text summary; Rudder requires final text to persist a trustworthy run result."
778
- : "") ||
779
- stderrLine ||
780
- `OpenCode exited with code ${synthesizedExitCode ?? -1}`;
781
- const modelId = model || null;
782
- return {
783
- exitCode: synthesizedExitCode,
784
- signal: attempt.proc.signal,
785
- timedOut: false,
786
- errorMessage: (synthesizedExitCode ?? 0) === 0 ? null : fallbackErrorMessage,
787
- errorCode: startupIdle ? "opencode_startup_idle" : toolLoopIdle ? "opencode_tool_loop_idle" : null,
788
- usage: {
789
- inputTokens: attempt.parsed.usage.inputTokens,
790
- outputTokens: attempt.parsed.usage.outputTokens,
791
- cachedInputTokens: attempt.parsed.usage.cachedInputTokens,
792
- },
793
- sessionId: resolvedSessionId,
794
- sessionParams: resolvedSessionParams,
795
- sessionDisplayId: resolvedSessionId,
796
- provider: parseModelProvider(modelId),
797
- biller: resolveOpenCodeBiller(runtimeEnv, parseModelProvider(modelId)),
798
- model: modelId,
799
- billingType: "unknown",
800
- costUsd: attempt.parsed.costUsd,
801
- resultJson: {
802
- stdout: attempt.proc.stdout,
803
- stderr: attempt.proc.stderr,
804
- ...(missingFinalSummary ? { summaryStatus: "missing_final_text" } : {}),
805
- ...(startupIdle ? { summaryStatus: "startup_idle", stoppedAfterStartupIdle: true } : {}),
806
- ...(toolLoopIdle ? { summaryStatus: "tool_loop_idle", stoppedAfterToolLoopIdle: true } : {}),
807
- },
808
- summary: parsedSummary || ((synthesizedExitCode ?? 0) === 0 ? "" : fallbackErrorMessage),
809
- clearSession: Boolean(clearSessionOnMissingSession && !attempt.parsed.sessionId),
837
+ sessionParams: resolvedSessionParams,
838
+ sessionDisplayId: resolvedSessionId,
839
+ provider: parseModelProvider(modelId),
840
+ biller: resolveOpenCodeBiller(runtimeEnv, parseModelProvider(modelId)),
841
+ model: modelId,
842
+ billingType: "unknown",
843
+ costUsd: attempt.parsed.costUsd,
844
+ resultJson: {
845
+ stdout: attempt.proc.stdout,
846
+ stderr: attempt.proc.stderr,
847
+ ...(missingFinalSummary ? { summaryStatus: "missing_final_text" } : {}),
848
+ ...(startupIdle ? { summaryStatus: "startup_idle", stoppedAfterStartupIdle: true } : {}),
849
+ ...(toolLoopIdle ? { summaryStatus: "tool_loop_idle", stoppedAfterToolLoopIdle: true } : {}),
850
+ },
851
+ summary: parsedSummary || ((synthesizedExitCode ?? 0) === 0 ? "" : fallbackErrorMessage),
852
+ clearSession: Boolean(clearSessionOnMissingSession && !attempt.parsed.sessionId),
853
+ };
810
854
  };
811
- };
812
- const initial = await runAttempt(sessionId);
813
- const initialFailed = !initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || Boolean(initial.parsed.errorMessage));
814
- if (sessionId &&
815
- initialFailed &&
816
- isOpenCodeUnknownSessionError(initial.proc.stdout, initial.rawStderr)) {
817
- await onLog("stdout", `[rudder] OpenCode session "${sessionId}" is unavailable; retrying with a fresh session.\n`);
818
- const retry = await runAttempt(null);
819
- return toResult(retry, true);
855
+ const initial = await runAttempt(sessionId);
856
+ const initialFailed = !initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || Boolean(initial.parsed.errorMessage));
857
+ if (sessionId &&
858
+ initialFailed &&
859
+ isOpenCodeUnknownSessionError(initial.proc.stdout, initial.rawStderr)) {
860
+ await onLog("stdout", `[rudder] OpenCode session "${sessionId}" is unavailable; retrying with a fresh session.\n`);
861
+ const retry = await runAttempt(null);
862
+ return toResult(retry, true);
863
+ }
864
+ return toResult(initial);
865
+ }
866
+ finally {
867
+ await fs.rm(runtimeTmpDir, { recursive: true, force: true });
820
868
  }
821
- return toResult(initial);
822
869
  }
823
870
  //# sourceMappingURL=execute.js.map