bopodev-agent-sdk 0.1.11 → 0.1.13

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 (100) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/dist/adapters/anthropic-api/src/cli/format-event.d.ts +1 -0
  4. package/dist/adapters/anthropic-api/src/cli/index.d.ts +1 -0
  5. package/dist/adapters/anthropic-api/src/index.d.ts +16 -0
  6. package/dist/adapters/anthropic-api/src/server/execute.d.ts +2 -0
  7. package/dist/adapters/anthropic-api/src/server/index.d.ts +6 -0
  8. package/dist/adapters/anthropic-api/src/server/parse.d.ts +1 -0
  9. package/dist/adapters/anthropic-api/src/server/test.d.ts +2 -0
  10. package/dist/adapters/anthropic-api/src/ui/build-config.d.ts +3 -0
  11. package/dist/adapters/anthropic-api/src/ui/index.d.ts +2 -0
  12. package/dist/adapters/anthropic-api/src/ui/parse-stdout.d.ts +6 -0
  13. package/dist/adapters/claude-code/src/cli/format-event.d.ts +1 -0
  14. package/dist/adapters/claude-code/src/cli/index.d.ts +1 -0
  15. package/dist/adapters/claude-code/src/index.d.ts +16 -0
  16. package/dist/adapters/claude-code/src/server/execute.d.ts +2 -0
  17. package/dist/adapters/claude-code/src/server/index.d.ts +6 -0
  18. package/dist/adapters/claude-code/src/server/parse.d.ts +2 -0
  19. package/dist/adapters/claude-code/src/server/test.d.ts +2 -0
  20. package/dist/adapters/claude-code/src/ui/build-config.d.ts +3 -0
  21. package/dist/adapters/claude-code/src/ui/index.d.ts +2 -0
  22. package/dist/adapters/claude-code/src/ui/parse-stdout.d.ts +6 -0
  23. package/dist/adapters/codex/src/cli/format-event.d.ts +1 -0
  24. package/dist/adapters/codex/src/cli/index.d.ts +1 -0
  25. package/dist/adapters/codex/src/index.d.ts +34 -0
  26. package/dist/adapters/codex/src/server/execute.d.ts +2 -0
  27. package/dist/adapters/codex/src/server/index.d.ts +6 -0
  28. package/dist/adapters/codex/src/server/parse.d.ts +2 -0
  29. package/dist/adapters/codex/src/server/test.d.ts +2 -0
  30. package/dist/adapters/codex/src/ui/build-config.d.ts +3 -0
  31. package/dist/adapters/codex/src/ui/index.d.ts +2 -0
  32. package/dist/adapters/codex/src/ui/parse-stdout.d.ts +6 -0
  33. package/dist/adapters/cursor/src/cli/format-event.d.ts +1 -0
  34. package/dist/adapters/cursor/src/cli/index.d.ts +1 -0
  35. package/dist/adapters/cursor/src/index.d.ts +22 -0
  36. package/dist/adapters/cursor/src/server/execute.d.ts +2 -0
  37. package/dist/adapters/cursor/src/server/index.d.ts +6 -0
  38. package/dist/adapters/cursor/src/server/parse.d.ts +2 -0
  39. package/dist/adapters/cursor/src/server/test.d.ts +2 -0
  40. package/dist/adapters/cursor/src/ui/build-config.d.ts +3 -0
  41. package/dist/adapters/cursor/src/ui/index.d.ts +2 -0
  42. package/dist/adapters/cursor/src/ui/parse-stdout.d.ts +6 -0
  43. package/dist/adapters/http/src/cli/format-event.d.ts +1 -0
  44. package/dist/adapters/http/src/cli/index.d.ts +1 -0
  45. package/dist/adapters/http/src/index.d.ts +7 -0
  46. package/dist/adapters/http/src/server/execute.d.ts +2 -0
  47. package/dist/adapters/http/src/server/index.d.ts +6 -0
  48. package/dist/adapters/http/src/server/parse.d.ts +1 -0
  49. package/dist/adapters/http/src/server/test.d.ts +2 -0
  50. package/dist/adapters/http/src/ui/build-config.d.ts +3 -0
  51. package/dist/adapters/http/src/ui/index.d.ts +2 -0
  52. package/dist/adapters/http/src/ui/parse-stdout.d.ts +6 -0
  53. package/dist/adapters/openai-api/src/cli/format-event.d.ts +1 -0
  54. package/dist/adapters/openai-api/src/cli/index.d.ts +1 -0
  55. package/dist/adapters/openai-api/src/index.d.ts +22 -0
  56. package/dist/adapters/openai-api/src/server/execute.d.ts +2 -0
  57. package/dist/adapters/openai-api/src/server/index.d.ts +6 -0
  58. package/dist/adapters/openai-api/src/server/parse.d.ts +1 -0
  59. package/dist/adapters/openai-api/src/server/test.d.ts +2 -0
  60. package/dist/adapters/openai-api/src/ui/build-config.d.ts +3 -0
  61. package/dist/adapters/openai-api/src/ui/index.d.ts +2 -0
  62. package/dist/adapters/openai-api/src/ui/parse-stdout.d.ts +6 -0
  63. package/dist/adapters/opencode/src/cli/format-event.d.ts +1 -0
  64. package/dist/adapters/opencode/src/cli/index.d.ts +1 -0
  65. package/dist/adapters/opencode/src/index.d.ts +7 -0
  66. package/dist/adapters/opencode/src/server/execute.d.ts +2 -0
  67. package/dist/adapters/opencode/src/server/index.d.ts +6 -0
  68. package/dist/adapters/opencode/src/server/parse.d.ts +1 -0
  69. package/dist/adapters/opencode/src/server/test.d.ts +2 -0
  70. package/dist/adapters/opencode/src/ui/build-config.d.ts +3 -0
  71. package/dist/adapters/opencode/src/ui/index.d.ts +2 -0
  72. package/dist/adapters/opencode/src/ui/parse-stdout.d.ts +6 -0
  73. package/dist/adapters/shell/src/cli/format-event.d.ts +1 -0
  74. package/dist/adapters/shell/src/cli/index.d.ts +1 -0
  75. package/dist/adapters/shell/src/index.d.ts +7 -0
  76. package/dist/adapters/shell/src/server/execute.d.ts +2 -0
  77. package/dist/adapters/shell/src/server/index.d.ts +6 -0
  78. package/dist/adapters/shell/src/server/parse.d.ts +1 -0
  79. package/dist/adapters/shell/src/server/test.d.ts +2 -0
  80. package/dist/adapters/shell/src/ui/build-config.d.ts +3 -0
  81. package/dist/adapters/shell/src/ui/index.d.ts +2 -0
  82. package/dist/adapters/shell/src/ui/parse-stdout.d.ts +6 -0
  83. package/dist/agent-sdk/src/adapters.d.ts +226 -1
  84. package/dist/agent-sdk/src/index.d.ts +2 -0
  85. package/dist/agent-sdk/src/registry.d.ts +2 -1
  86. package/dist/agent-sdk/src/runtime-core.d.ts +2 -0
  87. package/dist/agent-sdk/src/runtime-http.d.ts +38 -0
  88. package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -0
  89. package/dist/agent-sdk/src/runtime.d.ts +36 -0
  90. package/dist/agent-sdk/src/types.d.ts +55 -0
  91. package/dist/contracts/src/index.d.ts +889 -12
  92. package/package.json +2 -2
  93. package/src/adapters.ts +385 -36
  94. package/src/index.ts +2 -0
  95. package/src/registry.ts +67 -18
  96. package/src/runtime-core.ts +7 -0
  97. package/src/runtime-http.ts +455 -0
  98. package/src/runtime-parsers.ts +6 -0
  99. package/src/runtime.ts +848 -33
  100. package/src/types.ts +61 -0
package/src/adapters.ts CHANGED
@@ -10,7 +10,13 @@ import type {
10
10
  HeartbeatContext
11
11
  } from "./types";
12
12
  import { ExecutionOutcomeSchema, type ExecutionOutcome } from "bopodev-contracts";
13
- import { checkRuntimeCommandHealth, executeAgentRuntime, executePromptRuntime } from "./runtime";
13
+ import { checkRuntimeCommandHealth, executeAgentRuntime, executePromptRuntime } from "./runtime-core";
14
+ import {
15
+ executeDirectApiRuntime,
16
+ probeDirectApiEnvironment,
17
+ resolveDirectApiCredentials,
18
+ type DirectApiProvider
19
+ } from "./runtime-http";
14
20
  import { homedir } from "node:os";
15
21
  import { join, resolve } from "node:path";
16
22
 
@@ -79,6 +85,28 @@ export class OpenCodeAdapter implements AgentAdapter {
79
85
  }
80
86
  }
81
87
 
88
+ export class OpenAIApiAdapter implements AgentAdapter {
89
+ providerType = "openai_api" as const;
90
+
91
+ async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
92
+ if (context.workItems.length === 0) {
93
+ return createSkippedResult("OpenAI API", "openai_api", context);
94
+ }
95
+ return runDirectApiWork(context, "openai_api");
96
+ }
97
+ }
98
+
99
+ export class AnthropicApiAdapter implements AgentAdapter {
100
+ providerType = "anthropic_api" as const;
101
+
102
+ async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
103
+ if (context.workItems.length === 0) {
104
+ return createSkippedResult("Anthropic API", "anthropic_api", context);
105
+ }
106
+ return runDirectApiWork(context, "anthropic_api");
107
+ }
108
+ }
109
+
82
110
  export class GenericHeartbeatAdapter implements AgentAdapter {
83
111
  constructor(public providerType: "http" | "shell") {}
84
112
 
@@ -268,6 +296,24 @@ const staticMetadata: AdapterMetadata[] = [
268
296
  supportsThinkingEffort: false,
269
297
  requiresRuntimeCwd: true
270
298
  },
299
+ {
300
+ providerType: "openai_api",
301
+ label: "OpenAI API",
302
+ supportsModelSelection: true,
303
+ supportsEnvironmentTest: true,
304
+ supportsWebSearch: false,
305
+ supportsThinkingEffort: false,
306
+ requiresRuntimeCwd: false
307
+ },
308
+ {
309
+ providerType: "anthropic_api",
310
+ label: "Anthropic API",
311
+ supportsModelSelection: true,
312
+ supportsEnvironmentTest: true,
313
+ supportsWebSearch: false,
314
+ supportsThinkingEffort: false,
315
+ requiresRuntimeCwd: false
316
+ },
271
317
  {
272
318
  providerType: "http",
273
319
  label: "HTTP",
@@ -288,6 +334,8 @@ const staticMetadata: AdapterMetadata[] = [
288
334
  }
289
335
  ];
290
336
 
337
+ const metadataByProviderType = new Map(staticMetadata.map((entry) => [entry.providerType, entry] as const));
338
+
291
339
  const modelCatalog: Record<Exclude<AgentProviderType, "http" | "shell">, AdapterModelOption[]> = {
292
340
  codex: [
293
341
  { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
@@ -312,13 +360,33 @@ const modelCatalog: Record<Exclude<AgentProviderType, "http" | "shell">, Adapter
312
360
  { id: "sonnet-4.5", label: "sonnet-4.5" },
313
361
  { id: "opus-4.6", label: "opus-4.6" }
314
362
  ],
315
- opencode: []
363
+ opencode: [],
364
+ openai_api: [
365
+ { id: "gpt-5", label: "GPT-5" },
366
+ { id: "gpt-5-mini", label: "GPT-5 Mini" },
367
+ { id: "gpt-5-nano", label: "GPT-5 Nano" },
368
+ { id: "o3", label: "o3" },
369
+ { id: "o4-mini", label: "o4-mini" }
370
+ ],
371
+ anthropic_api: [
372
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
373
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
374
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }
375
+ ]
316
376
  };
317
377
 
318
378
  export function listAdapterMetadata(): AdapterMetadata[] {
319
379
  return staticMetadata;
320
380
  }
321
381
 
382
+ export function getAdapterMetadataByProviderType(providerType: AgentProviderType): AdapterMetadata {
383
+ const metadata = metadataByProviderType.get(providerType);
384
+ if (!metadata) {
385
+ throw new Error(`Missing adapter metadata for provider: ${providerType}`);
386
+ }
387
+ return metadata;
388
+ }
389
+
322
390
  export async function listAdapterModels(
323
391
  providerType: AgentProviderType,
324
392
  runtime?: AgentRuntimeConfig
@@ -331,7 +399,7 @@ export async function listAdapterModels(
331
399
  return dedupeModels([...discovered, ...modelCatalog.cursor]);
332
400
  }
333
401
  if (providerType === "opencode") {
334
- const discovered = await discoverOpenCodeModels(runtime);
402
+ const discovered = await discoverOpenCodeModelsCached(runtime);
335
403
  return dedupeModels(discovered);
336
404
  }
337
405
  return modelCatalog[providerType];
@@ -342,6 +410,9 @@ export async function testAdapterEnvironment(
342
410
  runtime?: AgentRuntimeConfig
343
411
  ): Promise<AdapterEnvironmentResult> {
344
412
  const checks: AdapterEnvironmentCheck[] = [];
413
+ if (providerType === "openai_api" || providerType === "anthropic_api") {
414
+ return testDirectApiEnvironment(providerType, runtime);
415
+ }
345
416
  const command =
346
417
  providerType === "cursor"
347
418
  ? (await resolveCursorLaunchConfig(runtime)).command
@@ -433,7 +504,7 @@ export async function testAdapterEnvironment(
433
504
  };
434
505
  }
435
506
 
436
- function createSkippedResult(providerLabel: string, providerKey: string, context: HeartbeatContext): AdapterExecutionResult {
507
+ export function createSkippedResult(providerLabel: string, providerKey: string, context: HeartbeatContext): AdapterExecutionResult {
437
508
  return {
438
509
  status: "skipped",
439
510
  summary: `${providerLabel} adapter: ${summarizeWork(context)}`,
@@ -452,7 +523,148 @@ function createSkippedResult(providerLabel: string, providerKey: string, context
452
523
  };
453
524
  }
454
525
 
455
- async function runProviderWork(
526
+ export async function runDirectApiWork(
527
+ context: HeartbeatContext,
528
+ provider: "openai_api" | "anthropic_api"
529
+ ): Promise<AdapterExecutionResult> {
530
+ const prompt = createPrompt(context);
531
+ const runtime = await executeDirectApiRuntime(provider, prompt, context.runtime);
532
+ if (runtime.ok) {
533
+ return {
534
+ status: "ok",
535
+ summary: runtime.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`,
536
+ tokenInput: runtime.tokenInput ?? 0,
537
+ tokenOutput: runtime.tokenOutput ?? 0,
538
+ usdCost: runtime.usdCost ?? 0,
539
+ outcome: toOutcome({
540
+ kind: "completed",
541
+ issueIdsTouched: issueIdsTouched(context),
542
+ actions: [{ type: "runtime.execute", status: "ok", detail: `${provider} runtime completed.` }],
543
+ blockers: [],
544
+ artifacts: [],
545
+ nextSuggestedState: "in_review"
546
+ }),
547
+ trace: {
548
+ command: runtime.endpoint,
549
+ cwd: context.runtime?.cwd,
550
+ exitCode: runtime.statusCode,
551
+ elapsedMs: runtime.elapsedMs,
552
+ failureType: runtime.failureType,
553
+ usageSource: "structured",
554
+ attemptCount: runtime.attemptCount,
555
+ attempts: runtime.attempts.map((attempt) => ({
556
+ attempt: attempt.attempt,
557
+ code: attempt.statusCode || null,
558
+ timedOut: attempt.failureType === "timeout",
559
+ elapsedMs: attempt.elapsedMs,
560
+ signal: null,
561
+ forcedKill: false
562
+ })),
563
+ stdoutPreview: runtime.responsePreview
564
+ },
565
+ nextState: withProviderMetadata(context, provider, runtime.elapsedMs, runtime.statusCode)
566
+ };
567
+ }
568
+ const failureDetail = runtime.error ?? "direct API request failed";
569
+ return {
570
+ status: "failed",
571
+ summary: `${provider} runtime failed: ${failureDetail}`,
572
+ tokenInput: 0,
573
+ tokenOutput: 0,
574
+ usdCost: 0,
575
+ outcome: toOutcome({
576
+ kind: "failed",
577
+ issueIdsTouched: issueIdsTouched(context),
578
+ actions: [{ type: "runtime.execute", status: "error", detail: failureDetail }],
579
+ blockers: [{
580
+ code: runtime.failureType ?? "runtime_failed",
581
+ message: failureDetail,
582
+ retryable: runtime.failureType !== "auth" && runtime.failureType !== "bad_response"
583
+ }],
584
+ artifacts: [],
585
+ nextSuggestedState: "blocked"
586
+ }),
587
+ trace: {
588
+ command: runtime.endpoint,
589
+ cwd: context.runtime?.cwd,
590
+ exitCode: runtime.statusCode || null,
591
+ elapsedMs: runtime.elapsedMs,
592
+ failureType: runtime.failureType,
593
+ usageSource: "none",
594
+ attemptCount: runtime.attemptCount,
595
+ attempts: runtime.attempts.map((attempt) => ({
596
+ attempt: attempt.attempt,
597
+ code: attempt.statusCode || null,
598
+ timedOut: attempt.failureType === "timeout",
599
+ elapsedMs: attempt.elapsedMs,
600
+ signal: null,
601
+ forcedKill: false
602
+ })),
603
+ stderrPreview: runtime.error,
604
+ stdoutPreview: runtime.responsePreview
605
+ },
606
+ nextState: context.state
607
+ };
608
+ }
609
+
610
+ export async function testDirectApiEnvironment(
611
+ providerType: DirectApiProvider,
612
+ runtime?: AgentRuntimeConfig
613
+ ): Promise<AdapterEnvironmentResult> {
614
+ const checks: AdapterEnvironmentCheck[] = [];
615
+ const credentials = resolveDirectApiCredentials(providerType, runtime);
616
+ if (!credentials.key) {
617
+ checks.push({
618
+ code: "api_key_missing",
619
+ level: "error",
620
+ message: `${providerType} API key is missing.`,
621
+ hint:
622
+ providerType === "openai_api"
623
+ ? "Set OPENAI_API_KEY or BOPO_OPENAI_API_KEY in runtime env or host environment."
624
+ : "Set ANTHROPIC_API_KEY or BOPO_ANTHROPIC_API_KEY in runtime env or host environment."
625
+ });
626
+ return {
627
+ providerType,
628
+ status: "fail",
629
+ testedAt: new Date().toISOString(),
630
+ checks
631
+ };
632
+ }
633
+ checks.push({
634
+ code: "api_key_present",
635
+ level: "info",
636
+ message: "API key is present."
637
+ });
638
+ checks.push({
639
+ code: "base_url",
640
+ level: "info",
641
+ message: `Using base URL: ${credentials.baseUrl}`
642
+ });
643
+ const probe = await probeDirectApiEnvironment(providerType, runtime);
644
+ if (probe.ok) {
645
+ checks.push({
646
+ code: "api_probe_ok",
647
+ level: "info",
648
+ message: `${providerType} API probe succeeded.`
649
+ });
650
+ } else {
651
+ checks.push({
652
+ code: "api_probe_failed",
653
+ level: probe.statusCode === 401 || probe.statusCode === 403 ? "error" : "warn",
654
+ message: `${providerType} API probe failed.`,
655
+ detail: probe.message,
656
+ hint: probe.statusCode === 401 || probe.statusCode === 403 ? "Verify API key and organization/project access." : undefined
657
+ });
658
+ }
659
+ return {
660
+ providerType,
661
+ status: toEnvironmentStatus(checks),
662
+ testedAt: new Date().toISOString(),
663
+ checks
664
+ };
665
+ }
666
+
667
+ export async function runProviderWork(
456
668
  context: HeartbeatContext,
457
669
  provider: "claude_code" | "codex",
458
670
  pricing: { inputRate: number; outputRate: number }
@@ -615,12 +827,13 @@ async function runProviderWork(
615
827
  };
616
828
  }
617
829
 
618
- async function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
830
+ export async function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
619
831
  const prompt = createPrompt(context);
620
832
  const cursorLaunch = await resolveCursorLaunchConfig(context.runtime);
621
833
  const cwd = context.runtime?.cwd?.trim() || process.cwd();
622
834
  const resumeState = resolveCursorResumeState(context.state, cwd);
623
- const runtimeTimeoutMs = context.runtime?.timeoutMs && context.runtime.timeoutMs > 0 ? context.runtime.timeoutMs : 30_000;
835
+ const runtimeTimeoutMs =
836
+ context.runtime?.timeoutMs && context.runtime.timeoutMs > 0 ? context.runtime.timeoutMs : 15 * 60 * 1000;
624
837
  const buildArgs = (resumeSessionId: string | null) => {
625
838
  const baseArgs = [...cursorLaunch.prefixArgs, "-p", "--output-format", "stream-json", "--workspace", cwd];
626
839
  if (resumeSessionId) {
@@ -685,9 +898,11 @@ async function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutio
685
898
  });
686
899
  }
687
900
 
688
- async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
901
+ export async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
689
902
  const prompt = createPrompt(context);
690
903
  const model = context.runtime?.model?.trim();
904
+ const runtimeTimeoutMs =
905
+ context.runtime?.timeoutMs && context.runtime.timeoutMs > 0 ? context.runtime.timeoutMs : 5 * 60 * 1000;
691
906
  if (!model) {
692
907
  return {
693
908
  status: "failed",
@@ -706,6 +921,32 @@ async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecut
706
921
  nextState: context.state
707
922
  };
708
923
  }
924
+ try {
925
+ await ensureOpenCodeModelConfiguredAndAvailable({
926
+ model,
927
+ command: context.runtime?.command,
928
+ cwd: context.runtime?.cwd,
929
+ env: context.runtime?.env
930
+ });
931
+ } catch (error) {
932
+ const message = error instanceof Error ? error.message : "OpenCode model validation failed.";
933
+ return {
934
+ status: "failed",
935
+ summary: message,
936
+ tokenInput: 0,
937
+ tokenOutput: 0,
938
+ usdCost: 0,
939
+ outcome: toOutcome({
940
+ kind: "blocked",
941
+ issueIdsTouched: issueIdsTouched(context),
942
+ actions: [{ type: "runtime.validate", status: "error", detail: message }],
943
+ blockers: [{ code: "model_unavailable", message, retryable: false }],
944
+ artifacts: [],
945
+ nextSuggestedState: "blocked"
946
+ }),
947
+ nextState: context.state
948
+ };
949
+ }
709
950
  const resumeSessionId = context.state.sessionId?.trim();
710
951
  const baseArgs = ["run", "--format", "json", "--model", model];
711
952
  if (resumeSessionId) {
@@ -716,6 +957,7 @@ async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecut
716
957
  prompt,
717
958
  {
718
959
  ...context.runtime,
960
+ timeoutMs: runtimeTimeoutMs,
719
961
  args: [...baseArgs, ...(context.runtime?.args ?? [])]
720
962
  },
721
963
  { provider: "opencode" }
@@ -727,6 +969,7 @@ async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecut
727
969
  prompt,
728
970
  {
729
971
  ...context.runtime,
972
+ timeoutMs: runtimeTimeoutMs,
730
973
  args: ["run", "--format", "json", "--model", model, ...(context.runtime?.args ?? [])]
731
974
  },
732
975
  { provider: "opencode" }
@@ -744,7 +987,7 @@ async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecut
744
987
  });
745
988
  }
746
989
 
747
- function resolveFailedUsage(
990
+ export function resolveFailedUsage(
748
991
  runtime: {
749
992
  parsedUsage?: {
750
993
  tokenInput?: number;
@@ -786,7 +1029,7 @@ function resolveFailedUsage(
786
1029
  };
787
1030
  }
788
1031
 
789
- function toProviderResult(
1032
+ export function toProviderResult(
790
1033
  context: HeartbeatContext,
791
1034
  provider: AgentProviderType,
792
1035
  prompt: string,
@@ -979,7 +1222,7 @@ function toProviderResult(
979
1222
  };
980
1223
  }
981
1224
 
982
- function resolveRuntimeFailureDetail(runtime: {
1225
+ export function resolveRuntimeFailureDetail(runtime: {
983
1226
  stderr: string;
984
1227
  stdout: string;
985
1228
  code: number | null;
@@ -1010,7 +1253,7 @@ function resolveRuntimeFailureDetail(runtime: {
1010
1253
  return "runtime exited without diagnostic output.";
1011
1254
  }
1012
1255
 
1013
- function parseOpenCodeOutput(stdout: string) {
1256
+ export function parseOpenCodeOutput(stdout: string) {
1014
1257
  let sessionId: string | null = null;
1015
1258
  for (const line of stdout.split(/\r?\n/)) {
1016
1259
  const trimmed = line.trim();
@@ -1031,7 +1274,7 @@ function parseOpenCodeOutput(stdout: string) {
1031
1274
  return { sessionId };
1032
1275
  }
1033
1276
 
1034
- function resolveCursorResumeState(state: HeartbeatContext["state"], cwd: string) {
1277
+ export function resolveCursorResumeState(state: HeartbeatContext["state"], cwd: string) {
1035
1278
  const savedSessionId = state.cursorSession?.sessionId?.trim() || state.sessionId?.trim() || null;
1036
1279
  const savedCwd = state.cursorSession?.cwd?.trim() || state.cwd?.trim() || null;
1037
1280
  if (!savedSessionId) {
@@ -1055,7 +1298,7 @@ function resolveCursorResumeState(state: HeartbeatContext["state"], cwd: string)
1055
1298
  };
1056
1299
  }
1057
1300
 
1058
- function readRuntimeSessionId(
1301
+ export function readRuntimeSessionId(
1059
1302
  runtime: {
1060
1303
  structuredOutputDiagnostics?: {
1061
1304
  claudeSessionId?: string;
@@ -1067,7 +1310,7 @@ function readRuntimeSessionId(
1067
1310
  return runtime.structuredOutputDiagnostics?.cursorSessionId?.trim() || runtime.structuredOutputDiagnostics?.claudeSessionId?.trim() || fallback;
1068
1311
  }
1069
1312
 
1070
- function isUnknownSessionError(stderr: string, stdout: string) {
1313
+ export function isUnknownSessionError(stderr: string, stdout: string) {
1071
1314
  const haystack = `${stderr}\n${stdout}`.toLowerCase();
1072
1315
  return (
1073
1316
  haystack.includes("unknown session") ||
@@ -1076,11 +1319,11 @@ function isUnknownSessionError(stderr: string, stdout: string) {
1076
1319
  );
1077
1320
  }
1078
1321
 
1079
- function hasTrustFlag(args: string[]) {
1322
+ export function hasTrustFlag(args: string[]) {
1080
1323
  return args.includes("--trust") || args.includes("--yolo") || args.includes("-f");
1081
1324
  }
1082
1325
 
1083
- function resolveRuntimeCommand(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
1326
+ export function resolveRuntimeCommand(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
1084
1327
  if (runtime?.command?.trim()) return runtime.command.trim();
1085
1328
  if (providerType === "claude_code") return "claude";
1086
1329
  if (providerType === "codex") return "codex";
@@ -1089,7 +1332,7 @@ function resolveRuntimeCommand(providerType: AgentProviderType, runtime?: AgentR
1089
1332
  return providerType;
1090
1333
  }
1091
1334
 
1092
- async function runRuntimeProbe(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
1335
+ export async function runRuntimeProbe(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
1093
1336
  const prompt = "Respond with hello.";
1094
1337
  if (providerType === "claude_code" || providerType === "codex") {
1095
1338
  return executeAgentRuntime(providerType, prompt, {
@@ -1136,7 +1379,7 @@ async function runRuntimeProbe(providerType: AgentProviderType, runtime?: AgentR
1136
1379
  });
1137
1380
  }
1138
1381
 
1139
- async function discoverCursorModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
1382
+ export async function discoverCursorModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
1140
1383
  const cursorLaunch = await resolveCursorLaunchConfig(runtime);
1141
1384
  const probe = await executePromptRuntime(
1142
1385
  cursorLaunch.command,
@@ -1155,25 +1398,98 @@ async function discoverCursorModels(runtime?: AgentRuntimeConfig): Promise<Adapt
1155
1398
  return parseModelLines(`${probe.stdout}\n${probe.stderr}`);
1156
1399
  }
1157
1400
 
1158
- async function discoverOpenCodeModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
1401
+ export async function discoverOpenCodeModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
1159
1402
  const probe = await executePromptRuntime(
1160
1403
  resolveRuntimeCommand("opencode", runtime),
1161
- "models",
1404
+ "",
1162
1405
  {
1163
1406
  ...runtime,
1164
1407
  args: ["models"],
1165
- timeoutMs: 10_000,
1408
+ timeoutMs: 120_000,
1166
1409
  retryCount: 0
1167
1410
  },
1168
1411
  { provider: "opencode" }
1169
1412
  );
1170
- if (!probe.ok && !probe.stdout.trim()) {
1413
+ if (!probe.ok && !probe.stdout.trim() && !probe.stderr.trim()) {
1171
1414
  return [];
1172
1415
  }
1173
- return parseModelLines(probe.stdout).filter((entry) => entry.id.includes("/"));
1416
+ return parseModelLines(`${probe.stdout}\n${probe.stderr}`).filter((entry) => entry.id.includes("/"));
1417
+ }
1418
+
1419
+ const OPENCODE_MODEL_DISCOVERY_TTL_MS = 60_000;
1420
+ const openCodeModelDiscoveryCache = new Map<string, { expiresAt: number; models: AdapterModelOption[] }>();
1421
+
1422
+ function normalizeRuntimeEnv(env: unknown): Record<string, string> {
1423
+ if (!env || typeof env !== "object" || Array.isArray(env)) {
1424
+ return {};
1425
+ }
1426
+ const out: Record<string, string> = {};
1427
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1428
+ if (typeof value === "string") {
1429
+ out[key] = value;
1430
+ }
1431
+ }
1432
+ return out;
1433
+ }
1434
+
1435
+ function openCodeModelCacheKey(runtime?: AgentRuntimeConfig) {
1436
+ const command = resolveRuntimeCommand("opencode", runtime);
1437
+ const cwd = runtime?.cwd?.trim() || process.cwd();
1438
+ const env = normalizeRuntimeEnv(runtime?.env);
1439
+ const envSignature = Object.entries(env)
1440
+ .sort(([a], [b]) => a.localeCompare(b))
1441
+ .map(([key, value]) => `${key}=${value}`)
1442
+ .join("\n");
1443
+ return `${command}\n${cwd}\n${envSignature}`;
1444
+ }
1445
+
1446
+ export async function discoverOpenCodeModelsCached(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
1447
+ const key = openCodeModelCacheKey(runtime);
1448
+ const now = Date.now();
1449
+ for (const [cacheKey, cacheValue] of openCodeModelDiscoveryCache.entries()) {
1450
+ if (cacheValue.expiresAt <= now) {
1451
+ openCodeModelDiscoveryCache.delete(cacheKey);
1452
+ }
1453
+ }
1454
+ const cached = openCodeModelDiscoveryCache.get(key);
1455
+ if (cached && cached.expiresAt > now) {
1456
+ return cached.models;
1457
+ }
1458
+ const models = await discoverOpenCodeModels(runtime);
1459
+ openCodeModelDiscoveryCache.set(key, {
1460
+ expiresAt: now + OPENCODE_MODEL_DISCOVERY_TTL_MS,
1461
+ models
1462
+ });
1463
+ return models;
1464
+ }
1465
+
1466
+ export async function ensureOpenCodeModelConfiguredAndAvailable(input: {
1467
+ model?: string;
1468
+ command?: string;
1469
+ cwd?: string;
1470
+ env?: Record<string, string>;
1471
+ }) {
1472
+ const normalizedModel = input.model?.trim();
1473
+ if (!normalizedModel) {
1474
+ throw new Error("OpenCode requires runtimeModel in provider/model format.");
1475
+ }
1476
+ const models = await discoverOpenCodeModelsCached({
1477
+ command: input.command,
1478
+ cwd: input.cwd,
1479
+ env: input.env
1480
+ });
1481
+ if (models.length === 0) {
1482
+ throw new Error("OpenCode returned no models. Run `opencode models` and verify provider auth.");
1483
+ }
1484
+ if (!models.some((entry) => entry.id === normalizedModel)) {
1485
+ const sample = models.slice(0, 12).map((entry) => entry.id).join(", ");
1486
+ throw new Error(
1487
+ `Configured OpenCode model is unavailable: ${normalizedModel}. Available models: ${sample}${models.length > 12 ? ", ..." : ""}`
1488
+ );
1489
+ }
1174
1490
  }
1175
1491
 
1176
- function parseModelLines(text: string): AdapterModelOption[] {
1492
+ export function parseModelLines(text: string): AdapterModelOption[] {
1177
1493
  const out: AdapterModelOption[] = [];
1178
1494
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1179
1495
  for (const line of lines) {
@@ -1203,7 +1519,7 @@ function parseModelLines(text: string): AdapterModelOption[] {
1203
1519
  return dedupeModels(out);
1204
1520
  }
1205
1521
 
1206
- function dedupeModels(models: AdapterModelOption[]) {
1522
+ export function dedupeModels(models: AdapterModelOption[]) {
1207
1523
  const seen = new Set<string>();
1208
1524
  const deduped: AdapterModelOption[] = [];
1209
1525
  for (const model of models) {
@@ -1215,7 +1531,7 @@ function dedupeModels(models: AdapterModelOption[]) {
1215
1531
  return deduped;
1216
1532
  }
1217
1533
 
1218
- async function resolveCursorLaunchConfig(runtime?: AgentRuntimeConfig): Promise<{ command: string; prefixArgs: string[] }> {
1534
+ export async function resolveCursorLaunchConfig(runtime?: AgentRuntimeConfig): Promise<{ command: string; prefixArgs: string[] }> {
1219
1535
  const configuredCommand = runtime?.command?.trim();
1220
1536
  if (configuredCommand) {
1221
1537
  const commandToken = configuredCommand.split(/[\\/]/).pop()?.toLowerCase() ?? configuredCommand.toLowerCase();
@@ -1256,13 +1572,13 @@ async function resolveCursorLaunchConfig(runtime?: AgentRuntimeConfig): Promise<
1256
1572
  return { command: "agent", prefixArgs: [] };
1257
1573
  }
1258
1574
 
1259
- function toEnvironmentStatus(checks: AdapterEnvironmentCheck[]): "pass" | "warn" | "fail" {
1575
+ export function toEnvironmentStatus(checks: AdapterEnvironmentCheck[]): "pass" | "warn" | "fail" {
1260
1576
  if (checks.some((check) => check.level === "error")) return "fail";
1261
1577
  if (checks.some((check) => check.level === "warn")) return "warn";
1262
1578
  return "pass";
1263
1579
  }
1264
1580
 
1265
- function createPrompt(context: HeartbeatContext) {
1581
+ export function createPrompt(context: HeartbeatContext) {
1266
1582
  const bootstrapPrompt = context.runtime?.bootstrapPrompt?.trim();
1267
1583
  const companyGoals = context.goalContext?.companyGoals.length
1268
1584
  ? context.goalContext.companyGoals.map((goal) => `- ${goal}`).join("\n")
@@ -1283,13 +1599,33 @@ function createPrompt(context: HeartbeatContext) {
1283
1599
  item.priority ? ` Priority: ${item.priority}` : null,
1284
1600
  item.body ? ` Body: ${item.body}` : null,
1285
1601
  item.labels?.length ? ` Labels: ${item.labels.join(", ")}` : null,
1286
- item.tags?.length ? ` Tags: ${item.tags.join(", ")}` : null
1602
+ item.tags?.length ? ` Tags: ${item.tags.join(", ")}` : null,
1603
+ item.attachments?.length
1604
+ ? [
1605
+ " Attachments:",
1606
+ ...item.attachments.map((attachment) =>
1607
+ ` - ${attachment.fileName} | path: ${attachment.absolutePath} | relative: ${attachment.relativePath}`
1608
+ )
1609
+ ].join("\n")
1610
+ : null
1287
1611
  ]
1288
1612
  .filter(Boolean)
1289
1613
  .join("\n")
1290
1614
  )
1291
1615
  .join("\n")
1292
1616
  : "- No assigned work";
1617
+ const memoryContext = context.memoryContext;
1618
+ const memoryTacitNotes = memoryContext?.tacitNotes?.trim()
1619
+ ? memoryContext.tacitNotes.trim()
1620
+ : "No tacit notes were recorded yet.";
1621
+ const memoryDurableFacts =
1622
+ memoryContext?.durableFacts && memoryContext.durableFacts.length > 0
1623
+ ? memoryContext.durableFacts.map((fact) => `- ${fact}`).join("\n")
1624
+ : "- No durable facts available.";
1625
+ const memoryDailyNotes =
1626
+ memoryContext?.dailyNotes && memoryContext.dailyNotes.length > 0
1627
+ ? memoryContext.dailyNotes.map((note) => `- ${note}`).join("\n")
1628
+ : "- No recent daily notes.";
1293
1629
 
1294
1630
  const executionDirectives = [
1295
1631
  "Execution directives:",
@@ -1299,11 +1635,15 @@ function createPrompt(context: HeartbeatContext) {
1299
1635
  "- Prefer completing assigned issue work in this repository over non-essential coordination tasks.",
1300
1636
  "- Keep command usage minimal and task-focused; avoid broad repository scans unless strictly required for the assigned issue.",
1301
1637
  "- Shell commands run under zsh on macOS; avoid Bash-only features such as `local -n`, `declare -n`, `mapfile`, and `readarray`.",
1302
- "- Prefer POSIX/zsh-compatible shell snippets, direct `curl` headers, `jq`, and temp JSON files/heredocs.",
1638
+ "- Prefer POSIX/zsh-compatible shell snippets, direct `curl` headers, and `jq`.",
1639
+ "- Prefer heredoc/stdin payloads (for example `curl --data-binary @- <<'JSON' ... JSON`) so cleanup is not blocked by runtime policy.",
1640
+ "- If payload files are required, write under `agents/<agent-id>/tmp/` (or OS temp via `mktemp`) and do not treat cleanup command failures as task blockers.",
1303
1641
  "- If control-plane API connectivity fails, report the exact failing command/error once and stop retry loops for the same endpoint.",
1304
1642
  "- If any command fails, avoid further exploratory commands and still return the required final JSON summary.",
1305
1643
  "- Do not stop after planning. You must execute concrete steps for assigned issues in this run (file edits, API calls, or other verifiable actions).",
1306
1644
  "- If you cannot complete concrete execution, set summary to include the blocker explicitly instead of claiming success.",
1645
+ "- Treat file memory as source of truth for long-term context: append raw observations to daily notes first, then promote stable patterns to durable facts.",
1646
+ "- Avoid writing duplicate durable facts when existing memory already contains the same lesson.",
1307
1647
  "- Your final output must be only the JSON object below, with no prose before or after it.",
1308
1648
  "- Do not invent token or cost values; the runtime records usage separately."
1309
1649
  ].join("\n");
@@ -1326,6 +1666,15 @@ ${agentGoals}
1326
1666
  Assigned issues:
1327
1667
  ${workItems}
1328
1668
 
1669
+ Memory context:
1670
+ - Memory root: ${memoryContext?.memoryRoot ?? "Unavailable"}
1671
+ - Tacit notes:
1672
+ ${memoryTacitNotes}
1673
+ - Durable facts:
1674
+ ${memoryDurableFacts}
1675
+ - Recent daily notes:
1676
+ ${memoryDailyNotes}
1677
+
1329
1678
  ${executionDirectives}
1330
1679
 
1331
1680
  At the end of your response, output exactly one JSON object on a single line and nothing else:
@@ -1333,7 +1682,7 @@ At the end of your response, output exactly one JSON object on a single line and
1333
1682
  `;
1334
1683
  }
1335
1684
 
1336
- function withProviderMetadata(
1685
+ export function withProviderMetadata(
1337
1686
  context: HeartbeatContext,
1338
1687
  provider: string,
1339
1688
  lastRuntimeMs?: number,
@@ -1351,7 +1700,7 @@ function withProviderMetadata(
1351
1700
  };
1352
1701
  }
1353
1702
 
1354
- function applyProviderSessionState(
1703
+ export function applyProviderSessionState(
1355
1704
  context: HeartbeatContext,
1356
1705
  provider: AgentProviderType,
1357
1706
  sessionUpdate?: ProviderSessionUpdate,
@@ -1385,14 +1734,14 @@ function applyProviderSessionState(
1385
1734
  return nextState;
1386
1735
  }
1387
1736
 
1388
- function toPreview(value: string, max = 1600) {
1737
+ export function toPreview(value: string, max = 1600) {
1389
1738
  if (value.length <= max) {
1390
1739
  return value;
1391
1740
  }
1392
1741
  return `${value.slice(0, max)}\n...[truncated]`;
1393
1742
  }
1394
1743
 
1395
- function buildMissingStructuredOutputDetail(
1744
+ export function buildMissingStructuredOutputDetail(
1396
1745
  provider: string,
1397
1746
  runtime: {
1398
1747
  structuredOutputSource?: "stdout" | "stderr";
@@ -1476,7 +1825,7 @@ function buildMissingStructuredOutputDetail(
1476
1825
  return hints.length > 0 ? `${base} Diagnostics: ${[...hints, ...diagnostics].join("; ")}.` : `${base} Diagnostics: ${diagnostics.join("; ")}.`;
1477
1826
  }
1478
1827
 
1479
- function isClaudeRunIncomplete(runtime: {
1828
+ export function isClaudeRunIncomplete(runtime: {
1480
1829
  structuredOutputDiagnostics?: {
1481
1830
  claudeStopReason?: string;
1482
1831
  claudeResultSubtype?: string;