bopodev-agent-sdk 0.1.14 → 0.1.15

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.
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > bopodev-agent-sdk@0.1.14 build /Users/danielkrusenstrahle/Documents/Projects/Monorepo/bopohq/packages/agent-sdk
3
+ > bopodev-agent-sdk@0.1.15 build /Users/danielkrusenstrahle/Documents/Projects/Monorepo/bopodev/packages/agent-sdk
4
4
  > tsc -p tsconfig.json --emitDeclarationOnly
5
5
 
@@ -1,2 +1,13 @@
1
- export { parseClaudeStreamOutput as parseClaudeOutput } from "../../../../agent-sdk/src/runtime-parsers";
1
+ import type { AdapterRuntimeUsageResolution } from "../../../../agent-sdk/src/adapters";
2
2
  export { isClaudeRunIncomplete, isUnknownSessionError as isClaudeUnknownSessionError } from "../../../../agent-sdk/src/adapters";
3
+ export declare function resolveClaudeRuntimeUsage(input: {
4
+ stdout: string;
5
+ stderr: string;
6
+ parsedUsage?: {
7
+ tokenInput?: number;
8
+ tokenOutput?: number;
9
+ usdCost?: number;
10
+ summary?: string;
11
+ };
12
+ structuredOutputSource?: "stdout" | "stderr";
13
+ }): AdapterRuntimeUsageResolution;
@@ -1,2 +1,13 @@
1
- export { parseStructuredUsage as parseCodexOutput } from "../../../../agent-sdk/src/runtime-parsers";
1
+ import type { AdapterRuntimeUsageResolution } from "../../../../agent-sdk/src/adapters";
2
2
  export { isUnknownSessionError as isCodexUnknownSessionError } from "../../../../agent-sdk/src/adapters";
3
+ export declare function resolveCodexRuntimeUsage(input: {
4
+ stdout: string;
5
+ stderr: string;
6
+ parsedUsage?: {
7
+ tokenInput?: number;
8
+ tokenOutput?: number;
9
+ usdCost?: number;
10
+ summary?: string;
11
+ };
12
+ structuredOutputSource?: "stdout" | "stderr";
13
+ }): AdapterRuntimeUsageResolution;
@@ -1,2 +1,13 @@
1
- export { parseCursorStreamOutput as parseCursorOutput } from "../../../../agent-sdk/src/runtime-parsers";
1
+ import type { AdapterRuntimeUsageResolution } from "../../../../agent-sdk/src/adapters";
2
2
  export { isUnknownSessionError as isCursorUnknownSessionError, readRuntimeSessionId } from "../../../../agent-sdk/src/adapters";
3
+ export declare function resolveCursorRuntimeUsage(input: {
4
+ stdout: string;
5
+ stderr: string;
6
+ parsedUsage?: {
7
+ tokenInput?: number;
8
+ tokenOutput?: number;
9
+ usdCost?: number;
10
+ summary?: string;
11
+ };
12
+ structuredOutputSource?: "stdout" | "stderr";
13
+ }): AdapterRuntimeUsageResolution;
@@ -1 +1,13 @@
1
+ import type { AdapterRuntimeUsageResolution } from "../../../../agent-sdk/src/adapters";
1
2
  export { parseGeminiOutput, isGeminiUnknownSessionError } from "../../../../agent-sdk/src/adapters";
3
+ export declare function resolveGeminiRuntimeUsage(input: {
4
+ stdout: string;
5
+ stderr: string;
6
+ parsedUsage?: {
7
+ tokenInput?: number;
8
+ tokenOutput?: number;
9
+ usdCost?: number;
10
+ summary?: string;
11
+ };
12
+ structuredOutputSource?: "stdout" | "stderr";
13
+ }): AdapterRuntimeUsageResolution;
@@ -1,5 +1,25 @@
1
- import type { AdapterEnvironmentCheck, AdapterEnvironmentResult, AdapterExecutionResult, AdapterMetadata, AdapterModelOption, AgentAdapter, AgentProviderType, AgentRuntimeConfig, HeartbeatContext } from "./types";
1
+ import type { AdapterEnvironmentCheck, AdapterEnvironmentResult, AdapterExecutionResult, AdapterMetadata, AdapterModelOption, AdapterNormalizedUsage, AgentAdapter, AgentProviderType, AgentRuntimeConfig, HeartbeatContext } from "./types";
2
2
  import { type DirectApiProvider } from "./runtime-http";
3
+ type RuntimeParsedUsage = {
4
+ tokenInput?: number;
5
+ tokenOutput?: number;
6
+ usdCost?: number;
7
+ summary?: string;
8
+ inputTokens?: number;
9
+ cachedInputTokens?: number;
10
+ outputTokens?: number;
11
+ costUsd?: number;
12
+ };
13
+ export type AdapterRuntimeUsageResolution = {
14
+ parsedUsage?: RuntimeParsedUsage;
15
+ structuredOutputSource?: "stdout" | "stderr";
16
+ };
17
+ export type AdapterRuntimeUsageResolver = (runtime: {
18
+ stdout: string;
19
+ stderr: string;
20
+ parsedUsage?: RuntimeParsedUsage;
21
+ structuredOutputSource?: "stdout" | "stderr";
22
+ }) => AdapterRuntimeUsageResolution;
3
23
  export declare class ClaudeCodeAdapter implements AgentAdapter {
4
24
  providerType: "claude_code";
5
25
  execute(context: HeartbeatContext): Promise<AdapterExecutionResult>;
@@ -47,17 +67,18 @@ export declare function testDirectApiEnvironment(providerType: DirectApiProvider
47
67
  export declare function runProviderWork(context: HeartbeatContext, provider: "claude_code" | "codex", pricing: {
48
68
  inputRate: number;
49
69
  outputRate: number;
70
+ }, options?: {
71
+ usageResolver?: AdapterRuntimeUsageResolver;
72
+ }): Promise<AdapterExecutionResult>;
73
+ export declare function runCursorWork(context: HeartbeatContext, options?: {
74
+ usageResolver?: AdapterRuntimeUsageResolver;
50
75
  }): Promise<AdapterExecutionResult>;
51
- export declare function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutionResult>;
52
76
  export declare function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecutionResult>;
53
- export declare function runGeminiCliWork(context: HeartbeatContext): Promise<AdapterExecutionResult>;
77
+ export declare function runGeminiCliWork(context: HeartbeatContext, options?: {
78
+ usageResolver?: AdapterRuntimeUsageResolver;
79
+ }): Promise<AdapterExecutionResult>;
54
80
  export declare function resolveFailedUsage(runtime: {
55
- parsedUsage?: {
56
- tokenInput?: number;
57
- tokenOutput?: number;
58
- usdCost?: number;
59
- summary?: string;
60
- };
81
+ parsedUsage?: RuntimeParsedUsage;
61
82
  failureType?: "timeout" | "spawn_error" | "nonzero_exit";
62
83
  stdout: string;
63
84
  stderr: string;
@@ -65,11 +86,13 @@ export declare function resolveFailedUsage(runtime: {
65
86
  tokenInput: number;
66
87
  tokenOutput: number;
67
88
  usdCost: number;
89
+ usage: AdapterNormalizedUsage | undefined;
68
90
  source: "structured";
69
91
  } | {
70
92
  tokenInput: number;
71
93
  tokenOutput: number;
72
94
  usdCost: number;
95
+ usage: AdapterNormalizedUsage;
73
96
  source: "none";
74
97
  };
75
98
  export declare function toProviderResult(context: HeartbeatContext, provider: AgentProviderType, prompt: string, runtime: {
@@ -90,12 +113,7 @@ export declare function toProviderResult(context: HeartbeatContext, provider: Ag
90
113
  spawnErrorCode?: string;
91
114
  forcedKill: boolean;
92
115
  }>;
93
- parsedUsage?: {
94
- tokenInput?: number;
95
- tokenOutput?: number;
96
- usdCost?: number;
97
- summary?: string;
98
- };
116
+ parsedUsage?: RuntimeParsedUsage;
99
117
  structuredOutputSource?: "stdout" | "stderr";
100
118
  structuredOutputDiagnostics?: {
101
119
  stdoutJsonObjectCount: number;
@@ -1 +1 @@
1
- export { parseClaudeStreamOutput, parseCursorStreamOutput, parseRuntimeTranscript, parseStructuredUsage } from "./runtime";
1
+ export { parseClaudeStreamOutput, parseCursorStreamOutput, parseGeminiStreamOutput, parseRuntimeTranscript, parseStructuredUsage } from "./runtime";
@@ -97,13 +97,9 @@ export declare function containsRateLimitFailure(text: string): boolean;
97
97
  export declare function checkRuntimeCommandHealth(command: string, options?: {
98
98
  cwd?: string;
99
99
  timeoutMs?: number;
100
+ env?: Record<string, string>;
100
101
  }): Promise<RuntimeCommandHealth>;
101
- export declare function parseStructuredUsage(stdout: string): {
102
- tokenInput: number | undefined;
103
- tokenOutput: number | undefined;
104
- usdCost: number | undefined;
105
- summary: string | undefined;
106
- } | undefined;
102
+ export declare function parseStructuredUsage(stdout: string): ParsedUsageRecord | undefined;
107
103
  export declare function parseClaudeStreamOutput(stdout: string): {
108
104
  usage: {
109
105
  summary: string | undefined;
@@ -58,12 +58,29 @@ export interface HeartbeatContext {
58
58
  memoryContext?: AgentMemoryContext;
59
59
  runtime?: AgentRuntimeConfig;
60
60
  }
61
+ /**
62
+ * Normalized usage contract produced by adapter execution.
63
+ *
64
+ * Invariants:
65
+ * - `inputTokens` excludes cache reads.
66
+ * - `cachedInputTokens` tracks cache-hit prompt tokens only.
67
+ * - persisted `tokenInput` = `inputTokens + cachedInputTokens` for backwards compatibility.
68
+ * - `outputTokens` reflects generated/completion tokens.
69
+ */
70
+ export interface AdapterNormalizedUsage {
71
+ inputTokens: number;
72
+ cachedInputTokens: number;
73
+ outputTokens: number;
74
+ costUsd?: number;
75
+ summary?: string;
76
+ }
61
77
  export interface AdapterExecutionResult {
62
78
  status: "ok" | "skipped" | "failed";
63
79
  summary: string;
64
80
  tokenInput: number;
65
81
  tokenOutput: number;
66
82
  usdCost: number;
83
+ usage?: AdapterNormalizedUsage;
67
84
  pricingProviderType?: string | null;
68
85
  pricingModelId?: string | null;
69
86
  outcome?: ExecutionOutcome;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "bopodev-agent-sdk",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
8
8
  "dependencies": {
9
- "bopodev-contracts": "0.1.14"
9
+ "bopodev-contracts": "0.1.15"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.json --emitDeclarationOnly",
package/src/adapters.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  AdapterExecutionResult,
5
5
  AdapterMetadata,
6
6
  AdapterModelOption,
7
+ AdapterNormalizedUsage,
7
8
  AgentAdapter,
8
9
  AgentProviderType,
9
10
  AgentRuntimeConfig,
@@ -11,6 +12,12 @@ import type {
11
12
  } from "./types";
12
13
  import { ExecutionOutcomeSchema, type ExecutionOutcome } from "bopodev-contracts";
13
14
  import { checkRuntimeCommandHealth, containsRateLimitFailure, executeAgentRuntime, executePromptRuntime } from "./runtime-core";
15
+ import {
16
+ parseClaudeStreamOutput,
17
+ parseCursorStreamOutput,
18
+ parseGeminiStreamOutput,
19
+ parseStructuredUsage
20
+ } from "./runtime-parsers";
14
21
  import {
15
22
  executeDirectApiRuntime,
16
23
  probeDirectApiEnvironment,
@@ -39,6 +46,195 @@ function isRateLimitedRuntimeFailure(runtime: { stdout: string; stderr: string }
39
46
  return containsRateLimitFailure(`${detail ?? ""}\n${runtime.stderr}\n${runtime.stdout}`);
40
47
  }
41
48
 
49
+ type RuntimeParsedUsage = {
50
+ tokenInput?: number;
51
+ tokenOutput?: number;
52
+ usdCost?: number;
53
+ summary?: string;
54
+ inputTokens?: number;
55
+ cachedInputTokens?: number;
56
+ outputTokens?: number;
57
+ costUsd?: number;
58
+ };
59
+
60
+ export type AdapterRuntimeUsageResolution = {
61
+ parsedUsage?: RuntimeParsedUsage;
62
+ structuredOutputSource?: "stdout" | "stderr";
63
+ };
64
+
65
+ export type AdapterRuntimeUsageResolver = (runtime: {
66
+ stdout: string;
67
+ stderr: string;
68
+ parsedUsage?: RuntimeParsedUsage;
69
+ structuredOutputSource?: "stdout" | "stderr";
70
+ }) => AdapterRuntimeUsageResolution;
71
+
72
+ function withResolvedRuntimeUsage<
73
+ T extends {
74
+ stdout: string;
75
+ stderr: string;
76
+ parsedUsage?: RuntimeParsedUsage;
77
+ structuredOutputSource?: "stdout" | "stderr";
78
+ }
79
+ >(
80
+ runtime: T,
81
+ usageResolver?: AdapterRuntimeUsageResolver
82
+ ): Omit<T, "parsedUsage" | "structuredOutputSource"> & {
83
+ parsedUsage?: RuntimeParsedUsage;
84
+ structuredOutputSource?: "stdout" | "stderr";
85
+ } {
86
+ if (!usageResolver) {
87
+ return runtime;
88
+ }
89
+ const resolution = usageResolver({
90
+ stdout: runtime.stdout,
91
+ stderr: runtime.stderr,
92
+ parsedUsage: runtime.parsedUsage,
93
+ structuredOutputSource: runtime.structuredOutputSource
94
+ });
95
+ if (!resolution.parsedUsage && !resolution.structuredOutputSource) {
96
+ return runtime;
97
+ }
98
+ return {
99
+ ...runtime,
100
+ parsedUsage: resolution.parsedUsage ?? runtime.parsedUsage,
101
+ structuredOutputSource: resolution.structuredOutputSource ?? runtime.structuredOutputSource
102
+ };
103
+ }
104
+
105
+ function toNormalizedUsage(usage: RuntimeParsedUsage | undefined): AdapterNormalizedUsage | undefined {
106
+ if (!usage) {
107
+ return undefined;
108
+ }
109
+ const inputTokens = usage.inputTokens ?? usage.tokenInput ?? 0;
110
+ const cachedInputTokens = usage.cachedInputTokens ?? 0;
111
+ const outputTokens = usage.outputTokens ?? usage.tokenOutput ?? 0;
112
+ const costUsd = usage.costUsd ?? usage.usdCost;
113
+ const summary = usage.summary;
114
+ return {
115
+ inputTokens: Math.max(0, inputTokens),
116
+ cachedInputTokens: Math.max(0, cachedInputTokens),
117
+ outputTokens: Math.max(0, outputTokens),
118
+ ...(costUsd !== undefined ? { costUsd } : {}),
119
+ ...(summary ? { summary } : {})
120
+ };
121
+ }
122
+
123
+ function usageTokenInputTotal(usage: RuntimeParsedUsage | undefined) {
124
+ if (!usage) {
125
+ return 0;
126
+ }
127
+ if (usage.inputTokens !== undefined || usage.cachedInputTokens !== undefined) {
128
+ return Math.max(0, (usage.inputTokens ?? 0) + (usage.cachedInputTokens ?? 0));
129
+ }
130
+ return Math.max(0, usage.tokenInput ?? 0);
131
+ }
132
+
133
+ function hasUsageMetrics(usage: RuntimeParsedUsage | undefined) {
134
+ if (!usage) {
135
+ return false;
136
+ }
137
+ return usage.tokenInput !== undefined || usage.tokenOutput !== undefined || usage.usdCost !== undefined;
138
+ }
139
+
140
+ function resolveCodexDefaultRuntimeUsage(input: {
141
+ stdout: string;
142
+ stderr: string;
143
+ parsedUsage?: RuntimeParsedUsage;
144
+ structuredOutputSource?: "stdout" | "stderr";
145
+ }): AdapterRuntimeUsageResolution {
146
+ const stdoutUsage = parseStructuredUsage(input.stdout);
147
+ const stderrUsage = parseStructuredUsage(input.stderr);
148
+ if (!hasUsageMetrics(stdoutUsage) && hasUsageMetrics(stderrUsage)) {
149
+ return { parsedUsage: { ...stdoutUsage, ...stderrUsage }, structuredOutputSource: "stderr" };
150
+ }
151
+ if (hasUsageMetrics(stdoutUsage)) {
152
+ return { parsedUsage: stdoutUsage, structuredOutputSource: "stdout" };
153
+ }
154
+ if (hasUsageMetrics(stderrUsage)) {
155
+ return { parsedUsage: stderrUsage, structuredOutputSource: "stderr" };
156
+ }
157
+ return {
158
+ parsedUsage: stdoutUsage ?? stderrUsage ?? input.parsedUsage,
159
+ structuredOutputSource: input.structuredOutputSource
160
+ };
161
+ }
162
+
163
+ function resolveClaudeDefaultRuntimeUsage(input: {
164
+ stdout: string;
165
+ stderr: string;
166
+ parsedUsage?: RuntimeParsedUsage;
167
+ structuredOutputSource?: "stdout" | "stderr";
168
+ }): AdapterRuntimeUsageResolution {
169
+ const parsed = parseClaudeStreamOutput(input.stdout);
170
+ if (parsed?.usage) {
171
+ return {
172
+ parsedUsage: {
173
+ summary: parsed.usage.summary ?? input.parsedUsage?.summary,
174
+ tokenInput: parsed.usage.tokenInput,
175
+ tokenOutput: parsed.usage.tokenOutput,
176
+ usdCost: parsed.usage.usdCost,
177
+ inputTokens: parsed.usage.tokenInput ?? 0,
178
+ cachedInputTokens: 0,
179
+ outputTokens: parsed.usage.tokenOutput ?? 0,
180
+ costUsd: parsed.usage.usdCost
181
+ },
182
+ structuredOutputSource: "stdout"
183
+ };
184
+ }
185
+ return resolveCodexDefaultRuntimeUsage(input);
186
+ }
187
+
188
+ function resolveCursorDefaultRuntimeUsage(input: {
189
+ stdout: string;
190
+ stderr: string;
191
+ parsedUsage?: RuntimeParsedUsage;
192
+ structuredOutputSource?: "stdout" | "stderr";
193
+ }): AdapterRuntimeUsageResolution {
194
+ const parsed = parseCursorStreamOutput(input.stdout);
195
+ if (parsed?.usage) {
196
+ return {
197
+ parsedUsage: {
198
+ summary: parsed.usage.summary ?? input.parsedUsage?.summary,
199
+ tokenInput: parsed.usage.tokenInput,
200
+ tokenOutput: parsed.usage.tokenOutput,
201
+ usdCost: parsed.usage.usdCost,
202
+ inputTokens: parsed.usage.tokenInput ?? 0,
203
+ cachedInputTokens: 0,
204
+ outputTokens: parsed.usage.tokenOutput ?? 0,
205
+ costUsd: parsed.usage.usdCost
206
+ },
207
+ structuredOutputSource: "stdout"
208
+ };
209
+ }
210
+ return resolveCodexDefaultRuntimeUsage(input);
211
+ }
212
+
213
+ function resolveGeminiDefaultRuntimeUsage(input: {
214
+ stdout: string;
215
+ stderr: string;
216
+ parsedUsage?: RuntimeParsedUsage;
217
+ structuredOutputSource?: "stdout" | "stderr";
218
+ }): AdapterRuntimeUsageResolution {
219
+ const parsed = parseGeminiStreamOutput(input.stdout, input.stderr);
220
+ if (parsed?.usage) {
221
+ return {
222
+ parsedUsage: {
223
+ summary: parsed.usage.summary ?? input.parsedUsage?.summary,
224
+ tokenInput: parsed.usage.tokenInput,
225
+ tokenOutput: parsed.usage.tokenOutput,
226
+ usdCost: parsed.usage.usdCost,
227
+ inputTokens: parsed.usage.tokenInput ?? 0,
228
+ cachedInputTokens: 0,
229
+ outputTokens: parsed.usage.tokenOutput ?? 0,
230
+ costUsd: parsed.usage.usdCost
231
+ },
232
+ structuredOutputSource: "stdout"
233
+ };
234
+ }
235
+ return resolveCodexDefaultRuntimeUsage(input);
236
+ }
237
+
42
238
  export class ClaudeCodeAdapter implements AgentAdapter {
43
239
  providerType = "claude_code" as const;
44
240
 
@@ -699,11 +895,27 @@ export async function testDirectApiEnvironment(
699
895
  export async function runProviderWork(
700
896
  context: HeartbeatContext,
701
897
  provider: "claude_code" | "codex",
702
- pricing: { inputRate: number; outputRate: number }
898
+ pricing: { inputRate: number; outputRate: number },
899
+ options?: { usageResolver?: AdapterRuntimeUsageResolver }
703
900
  ): Promise<AdapterExecutionResult> {
901
+ const usageResolver =
902
+ options?.usageResolver ?? (provider === "claude_code" ? resolveClaudeDefaultRuntimeUsage : resolveCodexDefaultRuntimeUsage);
704
903
  const pricingProviderType = resolveCanonicalPricingProviderKey(provider);
705
904
  const prompt = createPrompt(context);
706
- const runtime = await executeAgentRuntime(provider, prompt, context.runtime);
905
+ const hasCodexResume = provider === "codex" && hasCodexResumeArgs(context.runtime?.args ?? []);
906
+ let runtimeOutput = await executeAgentRuntime(
907
+ provider,
908
+ prompt,
909
+ hasCodexResume ? { ...context.runtime, retryCount: 0 } : context.runtime
910
+ );
911
+ if (provider === "codex" && !runtimeOutput.ok && hasCodexResume) {
912
+ runtimeOutput = await executeAgentRuntime(provider, prompt, {
913
+ ...context.runtime,
914
+ retryCount: 0,
915
+ args: stripCodexResumeArgs(context.runtime?.args ?? [])
916
+ });
917
+ }
918
+ const runtime = withResolvedRuntimeUsage(runtimeOutput, usageResolver);
707
919
  const pricingModelId = resolvePricingModelId(context.runtime?.model, runtime);
708
920
  if (runtime.ok) {
709
921
  if (!runtime.parsedUsage) {
@@ -746,12 +958,14 @@ export async function runProviderWork(
746
958
  }
747
959
  if (provider === "claude_code" && isClaudeRunIncomplete(runtime)) {
748
960
  const detail = "Claude run reached max-turns before completing execution for this issue.";
961
+ const usage = toNormalizedUsage(runtime.parsedUsage);
749
962
  return {
750
963
  status: "failed",
751
964
  summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${detail}`,
752
- tokenInput: runtime.parsedUsage?.tokenInput ?? 0,
753
- tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
754
- usdCost: runtime.parsedUsage?.usdCost ?? 0,
965
+ tokenInput: usageTokenInputTotal(runtime.parsedUsage),
966
+ tokenOutput: runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0,
967
+ usdCost: runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0,
968
+ usage,
755
969
  pricingProviderType,
756
970
  pricingModelId,
757
971
  outcome: toOutcome({
@@ -783,9 +997,10 @@ export async function runProviderWork(
783
997
  nextState: context.state
784
998
  };
785
999
  }
786
- const tokenInput = runtime.parsedUsage?.tokenInput ?? 0;
787
- const tokenOutput = runtime.parsedUsage?.tokenOutput ?? 0;
788
- const usdCost = runtime.parsedUsage?.usdCost ?? 0;
1000
+ const usage = toNormalizedUsage(runtime.parsedUsage);
1001
+ const tokenInput = usageTokenInputTotal(runtime.parsedUsage);
1002
+ const tokenOutput = runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0;
1003
+ const usdCost = runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0;
789
1004
  const summary = runtime.parsedUsage?.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`;
790
1005
 
791
1006
  return {
@@ -794,6 +1009,7 @@ export async function runProviderWork(
794
1009
  tokenInput,
795
1010
  tokenOutput,
796
1011
  usdCost,
1012
+ usage,
797
1013
  pricingProviderType,
798
1014
  pricingModelId,
799
1015
  outcome: toOutcome({
@@ -834,6 +1050,7 @@ export async function runProviderWork(
834
1050
  tokenInput: failedUsage.tokenInput,
835
1051
  tokenOutput: failedUsage.tokenOutput,
836
1052
  usdCost: failedUsage.usdCost,
1053
+ usage: failedUsage.usage,
837
1054
  pricingProviderType,
838
1055
  pricingModelId,
839
1056
  outcome: toOutcome({
@@ -872,7 +1089,11 @@ export async function runProviderWork(
872
1089
  };
873
1090
  }
874
1091
 
875
- export async function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
1092
+ export async function runCursorWork(
1093
+ context: HeartbeatContext,
1094
+ options?: { usageResolver?: AdapterRuntimeUsageResolver }
1095
+ ): Promise<AdapterExecutionResult> {
1096
+ const usageResolver = options?.usageResolver ?? resolveCursorDefaultRuntimeUsage;
876
1097
  const prompt = createPrompt(context);
877
1098
  const cursorLaunch = await resolveCursorLaunchConfig(context.runtime);
878
1099
  const cwd = context.runtime?.cwd?.trim() || process.cwd();
@@ -892,16 +1113,19 @@ export async function runCursorWork(context: HeartbeatContext): Promise<AdapterE
892
1113
  }
893
1114
  return [...baseArgs, ...(context.runtime?.args ?? [])];
894
1115
  };
895
- const runtime = await executePromptRuntime(
896
- cursorLaunch.command,
897
- prompt,
898
- {
899
- ...context.runtime,
900
- timeoutMs: runtimeTimeoutMs,
901
- retryCount: 0,
902
- args: buildArgs(resumeState.resumeSessionId)
903
- },
904
- { provider: "cursor" }
1116
+ const runtime = withResolvedRuntimeUsage(
1117
+ await executePromptRuntime(
1118
+ cursorLaunch.command,
1119
+ prompt,
1120
+ {
1121
+ ...context.runtime,
1122
+ timeoutMs: runtimeTimeoutMs,
1123
+ retryCount: 0,
1124
+ args: buildArgs(resumeState.resumeSessionId)
1125
+ },
1126
+ { provider: "cursor" }
1127
+ ),
1128
+ usageResolver
905
1129
  );
906
1130
  const initialSessionId = readRuntimeSessionId(
907
1131
  runtime,
@@ -913,16 +1137,19 @@ export async function runCursorWork(context: HeartbeatContext): Promise<AdapterE
913
1137
  !isRateLimitedRuntimeFailure(runtime) &&
914
1138
  isUnknownSessionError(runtime.stderr, runtime.stdout)
915
1139
  ) {
916
- const retry = await executePromptRuntime(
917
- cursorLaunch.command,
918
- prompt,
919
- {
920
- ...context.runtime,
921
- timeoutMs: runtimeTimeoutMs,
922
- retryCount: 0,
923
- args: buildArgs(null)
924
- },
925
- { provider: "cursor" }
1140
+ const retry = withResolvedRuntimeUsage(
1141
+ await executePromptRuntime(
1142
+ cursorLaunch.command,
1143
+ prompt,
1144
+ {
1145
+ ...context.runtime,
1146
+ timeoutMs: runtimeTimeoutMs,
1147
+ retryCount: 0,
1148
+ args: buildArgs(null)
1149
+ },
1150
+ { provider: "cursor" }
1151
+ ),
1152
+ usageResolver
926
1153
  );
927
1154
  return toProviderResult(context, "cursor", prompt, retry, {
928
1155
  inputRate: 0.0000015,
@@ -1066,7 +1293,11 @@ function resolveGeminiResumeState(state: HeartbeatContext["state"], cwd: string,
1066
1293
  return { resumeSessionId: savedSessionId, resumeAttempted: true, resumeSkippedReason: null };
1067
1294
  }
1068
1295
 
1069
- export async function runGeminiCliWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
1296
+ export async function runGeminiCliWork(
1297
+ context: HeartbeatContext,
1298
+ options?: { usageResolver?: AdapterRuntimeUsageResolver }
1299
+ ): Promise<AdapterExecutionResult> {
1300
+ const usageResolver = options?.usageResolver ?? resolveGeminiDefaultRuntimeUsage;
1070
1301
  const prompt = createPrompt(context);
1071
1302
  const cwd = context.runtime?.cwd?.trim() || process.cwd();
1072
1303
  const command = context.runtime?.command?.trim() || "gemini";
@@ -1089,16 +1320,19 @@ export async function runGeminiCliWork(context: HeartbeatContext): Promise<Adapt
1089
1320
  return base;
1090
1321
  };
1091
1322
 
1092
- const runtime = await executePromptRuntime(
1093
- command,
1094
- prompt,
1095
- {
1096
- ...context.runtime,
1097
- timeoutMs: runtimeTimeoutMs,
1098
- retryCount: 0,
1099
- args: buildArgs(resumeState.resumeSessionId)
1100
- },
1101
- { provider: "gemini_cli" }
1323
+ const runtime = withResolvedRuntimeUsage(
1324
+ await executePromptRuntime(
1325
+ command,
1326
+ prompt,
1327
+ {
1328
+ ...context.runtime,
1329
+ timeoutMs: runtimeTimeoutMs,
1330
+ retryCount: 0,
1331
+ args: buildArgs(resumeState.resumeSessionId)
1332
+ },
1333
+ { provider: "gemini_cli" }
1334
+ ),
1335
+ usageResolver
1102
1336
  );
1103
1337
 
1104
1338
  const parsed = parseGeminiOutput(runtime.stdout);
@@ -1109,16 +1343,19 @@ export async function runGeminiCliWork(context: HeartbeatContext): Promise<Adapt
1109
1343
  !isRateLimitedRuntimeFailure(runtime) &&
1110
1344
  isGeminiUnknownSessionError(runtime.stdout, runtime.stderr)
1111
1345
  ) {
1112
- const retry = await executePromptRuntime(
1113
- command,
1114
- prompt,
1115
- {
1116
- ...context.runtime,
1117
- timeoutMs: runtimeTimeoutMs,
1118
- retryCount: 0,
1119
- args: buildArgs(null)
1120
- },
1121
- { provider: "gemini_cli" }
1346
+ const retry = withResolvedRuntimeUsage(
1347
+ await executePromptRuntime(
1348
+ command,
1349
+ prompt,
1350
+ {
1351
+ ...context.runtime,
1352
+ timeoutMs: runtimeTimeoutMs,
1353
+ retryCount: 0,
1354
+ args: buildArgs(null)
1355
+ },
1356
+ { provider: "gemini_cli" }
1357
+ ),
1358
+ usageResolver
1122
1359
  );
1123
1360
  const retryParsed = parseGeminiOutput(retry.stdout);
1124
1361
  return toProviderResult(context, "gemini_cli", prompt, retry, {
@@ -1147,22 +1384,19 @@ export async function runGeminiCliWork(context: HeartbeatContext): Promise<Adapt
1147
1384
 
1148
1385
  export function resolveFailedUsage(
1149
1386
  runtime: {
1150
- parsedUsage?: {
1151
- tokenInput?: number;
1152
- tokenOutput?: number;
1153
- usdCost?: number;
1154
- summary?: string;
1155
- };
1387
+ parsedUsage?: RuntimeParsedUsage;
1156
1388
  failureType?: "timeout" | "spawn_error" | "nonzero_exit";
1157
1389
  stdout: string;
1158
1390
  stderr: string;
1159
1391
  }
1160
1392
  ) {
1161
1393
  if (runtime.parsedUsage) {
1394
+ const usage = toNormalizedUsage(runtime.parsedUsage);
1162
1395
  return {
1163
- tokenInput: runtime.parsedUsage.tokenInput ?? 0,
1164
- tokenOutput: runtime.parsedUsage.tokenOutput ?? 0,
1165
- usdCost: runtime.parsedUsage.usdCost ?? 0,
1396
+ tokenInput: usageTokenInputTotal(runtime.parsedUsage),
1397
+ tokenOutput: runtime.parsedUsage.outputTokens ?? runtime.parsedUsage.tokenOutput ?? 0,
1398
+ usdCost: runtime.parsedUsage.costUsd ?? runtime.parsedUsage.usdCost ?? 0,
1399
+ usage,
1166
1400
  source: "structured" as const
1167
1401
  };
1168
1402
  }
@@ -1171,6 +1405,11 @@ export function resolveFailedUsage(
1171
1405
  tokenInput: 0,
1172
1406
  tokenOutput: 0,
1173
1407
  usdCost: 0,
1408
+ usage: {
1409
+ inputTokens: 0,
1410
+ cachedInputTokens: 0,
1411
+ outputTokens: 0
1412
+ } as AdapterNormalizedUsage,
1174
1413
  source: "none" as const
1175
1414
  };
1176
1415
  }
@@ -1178,6 +1417,11 @@ export function resolveFailedUsage(
1178
1417
  tokenInput: 0,
1179
1418
  tokenOutput: 0,
1180
1419
  usdCost: 0,
1420
+ usage: {
1421
+ inputTokens: 0,
1422
+ cachedInputTokens: 0,
1423
+ outputTokens: 0
1424
+ } as AdapterNormalizedUsage,
1181
1425
  source: "none" as const
1182
1426
  };
1183
1427
  }
@@ -1204,12 +1448,7 @@ export function toProviderResult(
1204
1448
  spawnErrorCode?: string;
1205
1449
  forcedKill: boolean;
1206
1450
  }>;
1207
- parsedUsage?: {
1208
- tokenInput?: number;
1209
- tokenOutput?: number;
1210
- usdCost?: number;
1211
- summary?: string;
1212
- };
1451
+ parsedUsage?: RuntimeParsedUsage;
1213
1452
  structuredOutputSource?: "stdout" | "stderr";
1214
1453
  structuredOutputDiagnostics?: {
1215
1454
  stdoutJsonObjectCount: number;
@@ -1301,16 +1540,17 @@ export function toProviderResult(
1301
1540
  nextState: applyProviderSessionState(context, provider, sessionUpdate)
1302
1541
  };
1303
1542
  }
1304
- const tokenInput = runtime.parsedUsage?.tokenInput ?? 0;
1305
- const tokenOutput = runtime.parsedUsage?.tokenOutput ?? 0;
1306
- const usdCost = runtime.parsedUsage?.usdCost ?? 0;
1543
+ const tokenOutput = runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0;
1544
+ const usdCost = runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0;
1545
+ const usage = toNormalizedUsage(runtime.parsedUsage);
1307
1546
  const summary = runtime.parsedUsage?.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`;
1308
1547
  return {
1309
1548
  status: "ok",
1310
1549
  summary,
1311
- tokenInput,
1550
+ tokenInput: usageTokenInputTotal(runtime.parsedUsage),
1312
1551
  tokenOutput,
1313
1552
  usdCost,
1553
+ usage,
1314
1554
  pricingProviderType,
1315
1555
  pricingModelId,
1316
1556
  outcome: toOutcome({
@@ -1352,6 +1592,7 @@ export function toProviderResult(
1352
1592
  tokenInput: failedUsage.tokenInput,
1353
1593
  tokenOutput: failedUsage.tokenOutput,
1354
1594
  usdCost: failedUsage.usdCost,
1595
+ usage: failedUsage.usage,
1355
1596
  pricingProviderType,
1356
1597
  pricingModelId,
1357
1598
  outcome: toOutcome({
@@ -1537,6 +1778,40 @@ export function isUnknownSessionError(stderr: string, stdout: string) {
1537
1778
  );
1538
1779
  }
1539
1780
 
1781
+ function hasCodexResumeArgs(args: string[]) {
1782
+ for (let index = 0; index < args.length; index += 1) {
1783
+ const current = (args[index] ?? "").trim().toLowerCase();
1784
+ if (current === "--resume" || current.startsWith("--resume=")) {
1785
+ return true;
1786
+ }
1787
+ if (current === "resume") {
1788
+ return true;
1789
+ }
1790
+ }
1791
+ return false;
1792
+ }
1793
+
1794
+ function stripCodexResumeArgs(args: string[]) {
1795
+ const next: string[] = [];
1796
+ for (let index = 0; index < args.length; index += 1) {
1797
+ const current = (args[index] ?? "").trim();
1798
+ const lowered = current.toLowerCase();
1799
+ if (lowered === "--resume") {
1800
+ index += 1;
1801
+ continue;
1802
+ }
1803
+ if (lowered.startsWith("--resume=")) {
1804
+ continue;
1805
+ }
1806
+ if (lowered === "resume") {
1807
+ index += 1;
1808
+ continue;
1809
+ }
1810
+ next.push(args[index] ?? "");
1811
+ }
1812
+ return next;
1813
+ }
1814
+
1540
1815
  export function hasTrustFlag(args: string[]) {
1541
1816
  return args.includes("--trust") || args.includes("--yolo") || args.includes("-f");
1542
1817
  }
@@ -1970,13 +2245,18 @@ export function createPrompt(context: HeartbeatContext) {
1970
2245
  const controlPlaneApiBaseUrl =
1971
2246
  context.runtime?.env?.BOPODEV_API_BASE_URL?.trim() || context.runtime?.env?.BOPODEV_API_URL?.trim() || "";
1972
2247
  const hasControlPlaneHeaders = Boolean(context.runtime?.env?.BOPODEV_REQUEST_HEADERS_JSON?.trim());
2248
+ const safeControlPlaneCurl =
2249
+ 'curl -sS -H "x-company-id: $BOPODEV_COMPANY_ID" -H "x-actor-type: $BOPODEV_ACTOR_TYPE" -H "x-actor-id: $BOPODEV_ACTOR_ID" -H "x-actor-companies: $BOPODEV_ACTOR_COMPANIES" -H "x-actor-permissions: $BOPODEV_ACTOR_PERMISSIONS" "$BOPODEV_API_BASE_URL/agents"';
1973
2250
  const controlPlaneDirectives = [
1974
2251
  "Control-plane API directives:",
1975
2252
  controlPlaneApiBaseUrl
1976
2253
  ? `- Use BOPODEV_API_BASE_URL (or BOPODEV_API_URL) for API calls. Current value: ${controlPlaneApiBaseUrl}`
1977
2254
  : "- BOPODEV_API_BASE_URL is missing. Report this as blocker instead of guessing URLs.",
1978
2255
  "- Never guess fallback URLs such as localhost:3000.",
1979
- "- Include actor headers via BOPODEV_REQUEST_HEADERS_JSON for curl requests.",
2256
+ "- For curl requests, pass control-plane headers directly from env vars (`BOPODEV_COMPANY_ID`, `BOPODEV_ACTOR_TYPE`, `BOPODEV_ACTOR_ID`, `BOPODEV_ACTOR_COMPANIES`, `BOPODEV_ACTOR_PERMISSIONS`).",
2257
+ "- Use BOPODEV_REQUEST_HEADERS_JSON only as a compatibility fallback when direct vars are unavailable.",
2258
+ `- Safe example command (copy and edit path only): ${safeControlPlaneCurl}`,
2259
+ "- Avoid building curl headers by parsing JSON in shell unless direct header env vars are unavailable.",
1980
2260
  hasControlPlaneHeaders
1981
2261
  ? "- BOPODEV_REQUEST_HEADERS_JSON is present in env."
1982
2262
  : "- BOPODEV_REQUEST_HEADERS_JSON is missing. Report this as blocker."
package/src/registry.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { listAdapterModels, testAdapterEnvironment } from "./adapters";
2
1
  import type {
3
2
  AdapterEnvironmentResult,
4
3
  AdapterMetadata,
@@ -85,7 +84,7 @@ export async function getAdapterModels(
85
84
  if (fromModule) {
86
85
  return fromModule;
87
86
  }
88
- return adapterModules[providerType].models ? [...adapterModules[providerType].models] : listAdapterModels(providerType, runtime);
87
+ return adapterModules[providerType].models ? [...adapterModules[providerType].models] : [];
89
88
  }
90
89
 
91
90
  export function getAdapterMetadata(): AdapterMetadata[] {
@@ -100,5 +99,10 @@ export async function runAdapterEnvironmentTest(
100
99
  if (testEnvironment) {
101
100
  return testEnvironment(runtime);
102
101
  }
103
- return testAdapterEnvironment(providerType, runtime);
102
+ return {
103
+ providerType,
104
+ status: "warn",
105
+ testedAt: new Date().toISOString(),
106
+ checks: [{ code: "test_environment_unavailable", level: "warn", message: "Adapter does not expose testEnvironment." }]
107
+ };
104
108
  }
@@ -378,7 +378,7 @@ function isRetryableFailure(
378
378
  failureType: NonNullable<DirectApiExecutionOutput["failureType"]>,
379
379
  statusCode: number
380
380
  ) {
381
- if (failureType === "timeout" || failureType === "network") {
381
+ if (failureType === "timeout" || failureType === "network" || failureType === "rate_limit") {
382
382
  return true;
383
383
  }
384
384
  if (failureType === "http_error" && statusCode >= 500) {
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  parseClaudeStreamOutput,
3
3
  parseCursorStreamOutput,
4
+ parseGeminiStreamOutput,
4
5
  parseRuntimeTranscript,
5
6
  parseStructuredUsage
6
7
  } from "./runtime";
package/src/runtime.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { access, cp, lstat, mkdir, mkdtemp, readdir, rm, symlink } from "node:fs/promises";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { delimiter, dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { AgentRuntimeConfig } from "./types";
7
7
 
@@ -27,6 +27,11 @@ type ParsedUsageRecord = {
27
27
  summary?: string;
28
28
  };
29
29
 
30
+ type UsageSourceResolution = {
31
+ usage?: ParsedUsageRecord;
32
+ source?: "stdout" | "stderr";
33
+ };
34
+
30
35
  type CursorParsedStream = {
31
36
  usage: ParsedUsageRecord;
32
37
  sessionId?: string;
@@ -142,6 +147,10 @@ function providerConfigArgs(provider: "claude_code" | "codex", config?: AgentRun
142
147
  if (config?.model?.trim()) {
143
148
  args.push("--model", config.model.trim());
144
149
  }
150
+ if (!hasCliFlag(config?.args ?? [], "--json")) {
151
+ // Codex JSON events carry usage metrics we need for reliable cost accounting.
152
+ args.push("--json");
153
+ }
145
154
  if (config?.thinkingEffort && config.thinkingEffort !== "auto") {
146
155
  args.push("--reasoning-effort", config.thinkingEffort);
147
156
  }
@@ -199,7 +208,11 @@ export async function executeAgentRuntime(
199
208
  prompt: string,
200
209
  config?: AgentRuntimeConfig
201
210
  ): Promise<RuntimeExecutionOutput> {
202
- const command = resolveProviderCommand(provider, config?.command);
211
+ const mergedEnv = {
212
+ ...process.env,
213
+ ...(config?.env ?? {})
214
+ };
215
+ const command = await resolveProviderCommand(provider, config?.command, mergedEnv);
203
216
  const commandOverride = Boolean(config?.command && config.command.trim().length > 0);
204
217
  const effectiveRetryCount = config?.retryCount ?? (provider === "codex" ? 1 : 0);
205
218
  const candidateArgs = [
@@ -364,8 +377,9 @@ export async function executePromptRuntime(
364
377
  const claudeStream = provider === "claude_code" ? parseClaudeStreamOutput(stdout) : undefined;
365
378
  const cursorStream = provider === "cursor" ? parseCursorStreamOutput(stdout) : undefined;
366
379
  const geminiStream = provider === "gemini_cli" ? parseGeminiStreamOutput(stdout, stderr) : undefined;
367
- const stdoutUsage = cursorStream?.usage ?? claudeStream?.usage ?? geminiStream?.usage ?? parseStructuredUsage(stdout);
380
+ const stdoutUsage = parseStructuredUsage(stdout);
368
381
  const stderrUsage = parseStructuredUsage(stderr);
382
+ const usageResolution = resolveUsageSource(stdoutUsage, stderrUsage);
369
383
  return {
370
384
  ok: true,
371
385
  code: attemptResult.code,
@@ -375,8 +389,8 @@ export async function executePromptRuntime(
375
389
  elapsedMs: attempts.reduce((sum, item) => sum + item.elapsedMs, 0),
376
390
  attemptCount: attempts.length,
377
391
  attempts,
378
- parsedUsage: stdoutUsage ?? stderrUsage,
379
- structuredOutputSource: stdoutUsage ? "stdout" : stderrUsage ? "stderr" : undefined,
392
+ parsedUsage: usageResolution.usage,
393
+ structuredOutputSource: usageResolution.source,
380
394
  structuredOutputDiagnostics: {
381
395
  stdoutJsonObjectCount: extractJsonObjectBlocks(stdout).length,
382
396
  stderrJsonObjectCount: extractJsonObjectBlocks(stderr).length,
@@ -422,8 +436,9 @@ export async function executePromptRuntime(
422
436
  const claudeStream = provider === "claude_code" ? parseClaudeStreamOutput(stdout) : undefined;
423
437
  const cursorStream = provider === "cursor" ? parseCursorStreamOutput(stdout) : undefined;
424
438
  const geminiStream = provider === "gemini_cli" ? parseGeminiStreamOutput(stdout, stderr) : undefined;
425
- const stdoutUsage = cursorStream?.usage ?? claudeStream?.usage ?? geminiStream?.usage ?? parseStructuredUsage(stdout);
439
+ const stdoutUsage = parseStructuredUsage(stdout);
426
440
  const stderrUsage = parseStructuredUsage(stderr);
441
+ const usageResolution = resolveUsageSource(stdoutUsage, stderrUsage);
427
442
  return {
428
443
  ok: false,
429
444
  code: lastResult?.code ?? null,
@@ -434,8 +449,8 @@ export async function executePromptRuntime(
434
449
  attemptCount: attempts.length,
435
450
  attempts,
436
451
  failureType: classifyFailure(lastResult?.timedOut ?? false, lastResult?.spawnErrorCode, lastResult?.code ?? null),
437
- parsedUsage: stdoutUsage ?? stderrUsage,
438
- structuredOutputSource: stdoutUsage ? "stdout" : stderrUsage ? "stderr" : undefined,
452
+ parsedUsage: usageResolution.usage,
453
+ structuredOutputSource: usageResolution.source,
439
454
  structuredOutputDiagnostics: {
440
455
  stdoutJsonObjectCount: extractJsonObjectBlocks(stdout).length,
441
456
  stderrJsonObjectCount: extractJsonObjectBlocks(stderr).length,
@@ -536,14 +551,62 @@ function inspectClaudeOutputContract(command: string, args: string[], commandOve
536
551
  };
537
552
  }
538
553
 
539
- function resolveProviderCommand(provider: "claude_code" | "codex", configuredCommand: string | undefined) {
554
+ function isBareCommandToken(command: string) {
555
+ return command.length > 0 && !command.includes("/") && !command.includes("\\");
556
+ }
557
+
558
+ function splitPathEntries(pathValue: string | undefined) {
559
+ return (pathValue ?? "")
560
+ .split(delimiter)
561
+ .map((segment) => segment.trim())
562
+ .filter(Boolean);
563
+ }
564
+
565
+ async function pathExists(path: string) {
566
+ try {
567
+ await access(path);
568
+ return true;
569
+ } catch {
570
+ return false;
571
+ }
572
+ }
573
+
574
+ async function resolveClaudeBinaryCommand(env: NodeJS.ProcessEnv) {
575
+ const binaryName = process.platform === "win32" ? "claude.exe" : "claude";
576
+ const candidates = new Set<string>();
577
+ for (const entry of splitPathEntries(env.PATH)) {
578
+ candidates.add(join(entry, binaryName));
579
+ }
580
+ const home = (env.HOME ?? "").trim() || homedir();
581
+ if (home) {
582
+ candidates.add(join(home, ".local", "bin", binaryName));
583
+ }
584
+ for (const candidate of candidates) {
585
+ if (await pathExists(candidate)) {
586
+ return candidate;
587
+ }
588
+ }
589
+ return "claude";
590
+ }
591
+
592
+ async function resolveProviderCommand(
593
+ provider: "claude_code" | "codex",
594
+ configuredCommand: string | undefined,
595
+ env: NodeJS.ProcessEnv
596
+ ) {
540
597
  const trimmed = configuredCommand?.trim();
541
598
  if (!trimmed) {
599
+ if (provider === "claude_code") {
600
+ return resolveClaudeBinaryCommand(env);
601
+ }
542
602
  return pickDefaultCommand(provider);
543
603
  }
544
604
  // Normalize accidental provider id aliases used as command strings.
545
605
  if (provider === "claude_code" && trimmed === "claude_code") {
546
- return "claude";
606
+ return resolveClaudeBinaryCommand(env);
607
+ }
608
+ if (provider === "claude_code" && isBareCommandToken(trimmed) && trimmed.toLowerCase() === "claude") {
609
+ return resolveClaudeBinaryCommand(env);
547
610
  }
548
611
  return trimmed;
549
612
  }
@@ -1259,13 +1322,22 @@ export function containsRateLimitFailure(text: string) {
1259
1322
 
1260
1323
  export async function checkRuntimeCommandHealth(
1261
1324
  command: string,
1262
- options?: { cwd?: string; timeoutMs?: number }
1325
+ options?: { cwd?: string; timeoutMs?: number; env?: Record<string, string> }
1263
1326
  ): Promise<RuntimeCommandHealth> {
1264
1327
  const startedAt = Date.now();
1328
+ const mergedEnv: NodeJS.ProcessEnv = {
1329
+ ...process.env,
1330
+ ...(options?.env ?? {})
1331
+ };
1332
+ const trimmedCommand = command.trim();
1333
+ const normalizedCommand =
1334
+ trimmedCommand === "claude_code" || trimmedCommand.toLowerCase() === "claude" || trimmedCommand.toLowerCase() === "claude.exe"
1335
+ ? await resolveClaudeBinaryCommand(mergedEnv)
1336
+ : command;
1265
1337
  return new Promise((resolve) => {
1266
- const child = spawn(command, ["--version"], {
1338
+ const child = spawn(normalizedCommand, ["--version"], {
1267
1339
  cwd: options?.cwd ?? process.cwd(),
1268
- env: process.env,
1340
+ env: mergedEnv,
1269
1341
  shell: false
1270
1342
  });
1271
1343
 
@@ -1277,7 +1349,7 @@ export async function checkRuntimeCommandHealth(
1277
1349
  resolved = true;
1278
1350
  child.kill("SIGTERM");
1279
1351
  resolve({
1280
- command,
1352
+ command: normalizedCommand,
1281
1353
  available: false,
1282
1354
  exitCode: null,
1283
1355
  elapsedMs: Date.now() - startedAt,
@@ -1292,7 +1364,7 @@ export async function checkRuntimeCommandHealth(
1292
1364
  resolved = true;
1293
1365
  clearTimeout(timeout);
1294
1366
  resolve({
1295
- command,
1367
+ command: normalizedCommand,
1296
1368
  available: true,
1297
1369
  exitCode: code,
1298
1370
  elapsedMs: Date.now() - startedAt
@@ -1306,7 +1378,7 @@ export async function checkRuntimeCommandHealth(
1306
1378
  resolved = true;
1307
1379
  clearTimeout(timeout);
1308
1380
  resolve({
1309
- command,
1381
+ command: normalizedCommand,
1310
1382
  available: false,
1311
1383
  exitCode: null,
1312
1384
  elapsedMs: Date.now() - startedAt,
@@ -3149,10 +3221,13 @@ function tryParseUsage(candidate: string) {
3149
3221
  try {
3150
3222
  const parsed = JSON.parse(candidate) as Record<string, unknown>;
3151
3223
  const direct = toUsageRecord(parsed);
3224
+ const nested = findNestedUsage(parsed);
3225
+ if (direct && nested) {
3226
+ return mergeUsageRecords(direct, nested);
3227
+ }
3152
3228
  if (direct) {
3153
3229
  return direct;
3154
3230
  }
3155
- const nested = findNestedUsage(parsed);
3156
3231
  if (nested) {
3157
3232
  return nested;
3158
3233
  }
@@ -3163,22 +3238,119 @@ function tryParseUsage(candidate: string) {
3163
3238
  }
3164
3239
 
3165
3240
  function toUsageRecord(parsed: Record<string, unknown>) {
3166
- const tokenInput = toNumber(parsed.tokenInput);
3167
- const tokenOutput = toNumber(parsed.tokenOutput);
3168
- const usdCost = toNumber(parsed.usdCost);
3169
- const summary = typeof parsed.summary === "string" ? parsed.summary : undefined;
3170
- if (isPromptTemplateUsage(summary, tokenInput, tokenOutput, usdCost)) {
3241
+ const directInputTokens =
3242
+ toNumber(parsed.tokenInput) ??
3243
+ toNumber(parsed.input_tokens) ??
3244
+ toNumber(parsed.inputTokens) ??
3245
+ toNumber(parsed.prompt_tokens) ??
3246
+ toNumber(parsed.promptTokens) ??
3247
+ toNumber(parsed.promptTokenCount);
3248
+ const cachedInputTokens =
3249
+ toNumber(parsed.cached_input_tokens) ??
3250
+ toNumber(parsed.cachedInputTokens) ??
3251
+ toNumber(parsed.cache_read_input_tokens) ??
3252
+ toNumber(parsed.cacheReadInputTokens) ??
3253
+ toNumber(parsed.cache_read_tokens) ??
3254
+ toNumber(parsed.cacheReadTokens) ??
3255
+ toNumber(parsed.cachedContentTokenCount);
3256
+ const tokenInput =
3257
+ directInputTokens !== undefined || cachedInputTokens !== undefined
3258
+ ? (directInputTokens ?? 0) + (cachedInputTokens ?? 0)
3259
+ : undefined;
3260
+ const tokenOutput =
3261
+ toNumber(parsed.tokenOutput) ??
3262
+ toNumber(parsed.output_tokens) ??
3263
+ toNumber(parsed.outputTokens) ??
3264
+ toNumber(parsed.completion_tokens) ??
3265
+ toNumber(parsed.completionTokens) ??
3266
+ toNumber(parsed.candidatesTokenCount) ??
3267
+ toNumber(parsed.generated_tokens) ??
3268
+ toNumber(parsed.generatedTokens);
3269
+ const estimatedOutputFromTotals =
3270
+ tokenOutput === undefined &&
3271
+ tokenInput !== undefined &&
3272
+ (toNumber(parsed.total_tokens) ?? toNumber(parsed.totalTokens) ?? toNumber(parsed.totalTokenCount)) !== undefined
3273
+ ? Math.max(
3274
+ 0,
3275
+ (toNumber(parsed.total_tokens) ?? toNumber(parsed.totalTokens) ?? toNumber(parsed.totalTokenCount) ?? 0) -
3276
+ tokenInput
3277
+ )
3278
+ : undefined;
3279
+ const resolvedTokenOutput = tokenOutput ?? estimatedOutputFromTotals;
3280
+ const usdCost = toNumber(parsed.usdCost) ?? toNumber(parsed.total_cost_usd) ?? toNumber(parsed.cost_usd) ?? toNumber(parsed.cost);
3281
+ const summary =
3282
+ (typeof parsed.summary === "string" ? parsed.summary : undefined) ??
3283
+ (typeof parsed.result === "string" ? parsed.result : undefined) ??
3284
+ (typeof parsed.message === "string" ? parsed.message : undefined);
3285
+ if (isPromptTemplateUsage(summary, tokenInput, resolvedTokenOutput, usdCost)) {
3171
3286
  return undefined;
3172
3287
  }
3173
3288
  if (
3174
3289
  tokenInput === undefined &&
3175
- tokenOutput === undefined &&
3290
+ resolvedTokenOutput === undefined &&
3176
3291
  usdCost === undefined &&
3177
3292
  !summary
3178
3293
  ) {
3179
3294
  return undefined;
3180
3295
  }
3181
- return { tokenInput, tokenOutput, usdCost, summary };
3296
+ return { tokenInput, tokenOutput: resolvedTokenOutput, usdCost, summary };
3297
+ }
3298
+
3299
+ function hasUsageMetrics(usage: ParsedUsageRecord | undefined) {
3300
+ if (!usage) {
3301
+ return false;
3302
+ }
3303
+ return usage.tokenInput !== undefined || usage.tokenOutput !== undefined || usage.usdCost !== undefined;
3304
+ }
3305
+
3306
+ function mergeUsageRecords(primary: ParsedUsageRecord, secondary: ParsedUsageRecord): ParsedUsageRecord {
3307
+ return {
3308
+ summary: primary.summary ?? secondary.summary,
3309
+ tokenInput: primary.tokenInput ?? secondary.tokenInput,
3310
+ tokenOutput: primary.tokenOutput ?? secondary.tokenOutput,
3311
+ usdCost: primary.usdCost ?? secondary.usdCost
3312
+ };
3313
+ }
3314
+
3315
+ function resolveUsageSource(
3316
+ stdoutUsage: ParsedUsageRecord | undefined,
3317
+ stderrUsage: ParsedUsageRecord | undefined
3318
+ ): UsageSourceResolution {
3319
+ if (!stdoutUsage && !stderrUsage) {
3320
+ return {};
3321
+ }
3322
+ if (stdoutUsage && !stderrUsage) {
3323
+ return { usage: stdoutUsage, source: "stdout" };
3324
+ }
3325
+ if (!stdoutUsage && stderrUsage) {
3326
+ return { usage: stderrUsage, source: "stderr" };
3327
+ }
3328
+ const stdoutRecord = stdoutUsage as ParsedUsageRecord;
3329
+ const stderrRecord = stderrUsage as ParsedUsageRecord;
3330
+ const stdoutHasMetrics = hasUsageMetrics(stdoutUsage);
3331
+ const stderrHasMetrics = hasUsageMetrics(stderrUsage);
3332
+ if (!stdoutHasMetrics && stderrHasMetrics) {
3333
+ return {
3334
+ usage: mergeUsageRecords(stderrRecord, stdoutRecord),
3335
+ source: "stderr"
3336
+ };
3337
+ }
3338
+ if (stdoutHasMetrics && !stderrHasMetrics) {
3339
+ return {
3340
+ usage: mergeUsageRecords(stdoutRecord, stderrRecord),
3341
+ source: "stdout"
3342
+ };
3343
+ }
3344
+ if (stdoutHasMetrics && stderrHasMetrics) {
3345
+ return {
3346
+ usage: mergeUsageRecords(stdoutRecord, stderrRecord),
3347
+ source: "stdout"
3348
+ };
3349
+ }
3350
+ return {
3351
+ usage: mergeUsageRecords(stdoutRecord, stderrRecord),
3352
+ source: "stdout"
3353
+ };
3182
3354
  }
3183
3355
 
3184
3356
  function findNestedUsage(parsed: Record<string, unknown>) {
@@ -3266,15 +3438,15 @@ function tailLine(value: string) {
3266
3438
  function classifyStructuredOutputLikelyCause(
3267
3439
  stdout: string,
3268
3440
  stderr: string,
3269
- stdoutUsage: { summary?: string } | undefined,
3270
- stderrUsage: { summary?: string } | undefined
3441
+ stdoutUsage: ParsedUsageRecord | undefined,
3442
+ stderrUsage: ParsedUsageRecord | undefined
3271
3443
  ) {
3272
3444
  const hasStdout = stdout.trim().length > 0;
3273
3445
  const hasStderr = stderr.trim().length > 0;
3274
3446
  if (!hasStdout && !hasStderr) {
3275
3447
  return "no_output_from_runtime" as const;
3276
3448
  }
3277
- if (!stdoutUsage && stderrUsage) {
3449
+ if (!hasUsageMetrics(stdoutUsage) && hasUsageMetrics(stderrUsage)) {
3278
3450
  return "json_on_stderr_only" as const;
3279
3451
  }
3280
3452
  const jsonLike = /[\{\}\[\]\"]/m.test(stdout) || /[\{\}\[\]\"]/m.test(stderr);
package/src/types.ts CHANGED
@@ -64,12 +64,30 @@ export interface HeartbeatContext {
64
64
  runtime?: AgentRuntimeConfig;
65
65
  }
66
66
 
67
+ /**
68
+ * Normalized usage contract produced by adapter execution.
69
+ *
70
+ * Invariants:
71
+ * - `inputTokens` excludes cache reads.
72
+ * - `cachedInputTokens` tracks cache-hit prompt tokens only.
73
+ * - persisted `tokenInput` = `inputTokens + cachedInputTokens` for backwards compatibility.
74
+ * - `outputTokens` reflects generated/completion tokens.
75
+ */
76
+ export interface AdapterNormalizedUsage {
77
+ inputTokens: number;
78
+ cachedInputTokens: number;
79
+ outputTokens: number;
80
+ costUsd?: number;
81
+ summary?: string;
82
+ }
83
+
67
84
  export interface AdapterExecutionResult {
68
85
  status: "ok" | "skipped" | "failed";
69
86
  summary: string;
70
87
  tokenInput: number;
71
88
  tokenOutput: number;
72
89
  usdCost: number;
90
+ usage?: AdapterNormalizedUsage;
73
91
  pricingProviderType?: string | null;
74
92
  pricingModelId?: string | null;
75
93
  outcome?: ExecutionOutcome;
@@ -1,4 +0,0 @@
1
-
2
- > bopodev-agent-sdk@0.1.12 typecheck /Users/danielkrusenstrahle/Documents/Projects/Monorepo/bopohq/packages/agent-sdk
3
- > tsc -p tsconfig.json --noEmit
4
-