bopodev-agent-sdk 0.1.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.
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "bopodev-agent-sdk",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "dependencies": {
9
+ "bopodev-contracts": "0.1.1"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json --emitDeclarationOnly",
13
+ "lint": "tsc -p tsconfig.json --noEmit",
14
+ "typecheck": "tsc -p tsconfig.json --noEmit"
15
+ }
16
+ }
@@ -0,0 +1,324 @@
1
+ import type { AdapterExecutionResult, AgentAdapter, HeartbeatContext } from "./types";
2
+ import { executeAgentRuntime, executePromptRuntime } from "./runtime";
3
+
4
+ function summarizeWork(context: HeartbeatContext) {
5
+ if (context.workItems.length === 0) {
6
+ return "No assigned work found.";
7
+ }
8
+ return `Processed ${context.workItems.length} assigned issue(s).`;
9
+ }
10
+
11
+ export class ClaudeCodeAdapter implements AgentAdapter {
12
+ providerType = "claude_code" as const;
13
+
14
+ async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
15
+ if (context.workItems.length === 0) {
16
+ return createSkippedResult("Claude Code", "claude_code", context);
17
+ }
18
+ return runProviderWork(context, "claude_code", {
19
+ inputRate: 0.000002,
20
+ outputRate: 0.00001
21
+ });
22
+ }
23
+ }
24
+
25
+ export class CodexAdapter implements AgentAdapter {
26
+ providerType = "codex" as const;
27
+
28
+ async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
29
+ if (context.workItems.length === 0) {
30
+ return createSkippedResult("Codex", "codex", context);
31
+ }
32
+ return runProviderWork(context, "codex", {
33
+ inputRate: 0.0000015,
34
+ outputRate: 0.000008
35
+ });
36
+ }
37
+ }
38
+
39
+ export class GenericHeartbeatAdapter implements AgentAdapter {
40
+ constructor(public providerType: "http" | "shell") {}
41
+
42
+ async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
43
+ if (context.workItems.length === 0) {
44
+ return createSkippedResult(this.providerType, this.providerType, context);
45
+ }
46
+ if (!context.runtime?.command) {
47
+ return {
48
+ status: "failed",
49
+ summary: `${this.providerType} adapter is missing a runtime command.`,
50
+ tokenInput: 0,
51
+ tokenOutput: 0,
52
+ usdCost: 0,
53
+ nextState: context.state
54
+ };
55
+ }
56
+
57
+ const credentialState = resolveControlPlaneCredentialState(context);
58
+ const prompt = createPrompt(context, credentialState);
59
+ const runtime = await executePromptRuntime(context.runtime.command, prompt, context.runtime);
60
+ const executionMode = runtime.parsedUsage?.executionMode ?? defaultExecutionMode(credentialState);
61
+ const gatedControlPlaneReasons = runtime.parsedUsage?.gatedControlPlaneReasons ?? credentialState.missingReasons;
62
+ if (runtime.ok) {
63
+ return {
64
+ status: "ok",
65
+ summary: withExecutionDiagnostics(
66
+ runtime.parsedUsage?.summary ?? `${this.providerType} runtime finished in ${runtime.elapsedMs}ms.`,
67
+ executionMode,
68
+ gatedControlPlaneReasons
69
+ ),
70
+ tokenInput: runtime.parsedUsage?.tokenInput ?? 0,
71
+ tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
72
+ usdCost: runtime.parsedUsage?.usdCost ?? 0,
73
+ trace: {
74
+ command: context.runtime.command,
75
+ exitCode: runtime.code,
76
+ elapsedMs: runtime.elapsedMs,
77
+ timedOut: runtime.timedOut,
78
+ failureType: runtime.failureType,
79
+ attemptCount: runtime.attemptCount,
80
+ attempts: runtime.attempts,
81
+ stdoutPreview: toPreview(runtime.stdout),
82
+ stderrPreview: toPreview(runtime.stderr),
83
+ executionMode,
84
+ gatedControlPlaneReasons
85
+ },
86
+ nextState: withProviderMetadata(context, this.providerType, runtime.elapsedMs, runtime.code)
87
+ };
88
+ }
89
+
90
+ return {
91
+ status: "failed",
92
+ summary: withExecutionDiagnostics(
93
+ `${this.providerType} runtime failed: ${runtime.stderr || "unknown error"}`,
94
+ executionMode,
95
+ gatedControlPlaneReasons
96
+ ),
97
+ tokenInput: 0,
98
+ tokenOutput: 0,
99
+ usdCost: 0,
100
+ trace: {
101
+ command: context.runtime.command,
102
+ exitCode: runtime.code,
103
+ elapsedMs: runtime.elapsedMs,
104
+ timedOut: runtime.timedOut,
105
+ failureType: runtime.failureType,
106
+ attemptCount: runtime.attemptCount,
107
+ attempts: runtime.attempts,
108
+ stdoutPreview: toPreview(runtime.stdout),
109
+ stderrPreview: toPreview(runtime.stderr),
110
+ executionMode,
111
+ gatedControlPlaneReasons
112
+ },
113
+ nextState: context.state
114
+ };
115
+ }
116
+ }
117
+
118
+ function createSkippedResult(providerLabel: string, providerKey: string, context: HeartbeatContext): AdapterExecutionResult {
119
+ return {
120
+ status: "skipped",
121
+ summary: `${providerLabel} adapter: ${summarizeWork(context)}`,
122
+ tokenInput: 0,
123
+ tokenOutput: 0,
124
+ usdCost: 0,
125
+ nextState: withProviderMetadata(context, providerKey)
126
+ };
127
+ }
128
+
129
+ async function runProviderWork(
130
+ context: HeartbeatContext,
131
+ provider: "claude_code" | "codex",
132
+ pricing: { inputRate: number; outputRate: number }
133
+ ): Promise<AdapterExecutionResult> {
134
+ const credentialState = resolveControlPlaneCredentialState(context);
135
+ const prompt = createPrompt(context, credentialState);
136
+ const runtime = await executeAgentRuntime(provider, prompt, context.runtime);
137
+ const executionMode = runtime.parsedUsage?.executionMode ?? defaultExecutionMode(credentialState);
138
+ const gatedControlPlaneReasons = runtime.parsedUsage?.gatedControlPlaneReasons ?? credentialState.missingReasons;
139
+ if (runtime.ok) {
140
+ const fallbackOutputTokens = Math.max(Math.round(runtime.stdout.length / 4), 80);
141
+ const fallbackInputTokens = Math.max(Math.round(prompt.length / 4), 120);
142
+ const tokenInput = runtime.parsedUsage?.tokenInput ?? fallbackInputTokens;
143
+ const tokenOutput = runtime.parsedUsage?.tokenOutput ?? fallbackOutputTokens;
144
+ const usdCost =
145
+ runtime.parsedUsage?.usdCost ??
146
+ Number((tokenInput * pricing.inputRate + tokenOutput * pricing.outputRate).toFixed(6));
147
+ const summary = withExecutionDiagnostics(
148
+ runtime.parsedUsage?.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`,
149
+ executionMode,
150
+ gatedControlPlaneReasons
151
+ );
152
+
153
+ return {
154
+ status: "ok",
155
+ summary,
156
+ tokenInput,
157
+ tokenOutput,
158
+ usdCost,
159
+ trace: {
160
+ command: context.runtime?.command ?? provider,
161
+ exitCode: runtime.code,
162
+ elapsedMs: runtime.elapsedMs,
163
+ timedOut: runtime.timedOut,
164
+ failureType: runtime.failureType,
165
+ attemptCount: runtime.attemptCount,
166
+ attempts: runtime.attempts,
167
+ stdoutPreview: toPreview(runtime.stdout),
168
+ stderrPreview: toPreview(runtime.stderr),
169
+ executionMode,
170
+ gatedControlPlaneReasons
171
+ },
172
+ nextState: withProviderMetadata(context, provider, runtime.elapsedMs, runtime.code)
173
+ };
174
+ }
175
+ return {
176
+ status: "failed",
177
+ summary: withExecutionDiagnostics(
178
+ `${provider} runtime failed: ${runtime.stderr || "unknown error"}`,
179
+ executionMode,
180
+ gatedControlPlaneReasons
181
+ ),
182
+ tokenInput: 0,
183
+ tokenOutput: 0,
184
+ usdCost: 0,
185
+ trace: {
186
+ command: context.runtime?.command ?? provider,
187
+ exitCode: runtime.code,
188
+ elapsedMs: runtime.elapsedMs,
189
+ timedOut: runtime.timedOut,
190
+ failureType: runtime.failureType,
191
+ attemptCount: runtime.attemptCount,
192
+ attempts: runtime.attempts,
193
+ stdoutPreview: toPreview(runtime.stdout),
194
+ stderrPreview: toPreview(runtime.stderr),
195
+ executionMode,
196
+ gatedControlPlaneReasons
197
+ },
198
+ nextState: context.state
199
+ };
200
+ }
201
+
202
+ function createPrompt(context: HeartbeatContext, credentialState = resolveControlPlaneCredentialState(context)) {
203
+ const companyGoals = context.goalContext?.companyGoals.length
204
+ ? context.goalContext.companyGoals.map((goal) => `- ${goal}`).join("\n")
205
+ : "- No active company goals";
206
+ const projectGoals = context.goalContext?.projectGoals.length
207
+ ? context.goalContext.projectGoals.map((goal) => `- ${goal}`).join("\n")
208
+ : "- No active project goals";
209
+ const agentGoals = context.goalContext?.agentGoals.length
210
+ ? context.goalContext.agentGoals.map((goal) => `- ${goal}`).join("\n")
211
+ : "- No active agent goals";
212
+ const workItems = context.workItems.length
213
+ ? context.workItems
214
+ .map((item) =>
215
+ [
216
+ `- [${item.issueId}] ${item.title}`,
217
+ ` Project: ${item.projectName ?? item.projectId}`,
218
+ item.status ? ` Status: ${item.status}` : null,
219
+ item.priority ? ` Priority: ${item.priority}` : null,
220
+ item.body ? ` Body: ${item.body}` : null,
221
+ item.labels?.length ? ` Labels: ${item.labels.join(", ")}` : null,
222
+ item.tags?.length ? ` Tags: ${item.tags.join(", ")}` : null
223
+ ]
224
+ .filter(Boolean)
225
+ .join("\n")
226
+ )
227
+ .join("\n")
228
+ : "- No assigned work";
229
+
230
+ const executionDirectives = [
231
+ "Execution directives:",
232
+ "- You are running inside a BopoHQ heartbeat for local repository work.",
233
+ "- Use injected skills from the local Codex/agent skills directory when relevant.",
234
+ "- Prefer completing assigned issue work in this repository over control-plane orchestration tasks.",
235
+ "- Only use BopoHQ control-plane APIs/skills when BOPOHQ_API_URL and BOPOHQ_API_KEY are present.",
236
+ "- If those credentials are missing, do not attempt control-plane calls; continue with local issue execution and report constraints briefly.",
237
+ "- Keep command usage minimal and task-focused; avoid broad repository scans unless strictly required for the assigned issue.",
238
+ "- If any command fails, avoid further exploratory commands and still return the required final JSON summary."
239
+ ].join("\n");
240
+ const credentialSnapshot = [
241
+ "Control-plane credential snapshot:",
242
+ `- BopoHQ credentials: ${credentialState.bopohqEnabled ? "present" : "missing"}`,
243
+ ...credentialState.missingReasons.map((reason) => `- Gate: ${reason}`)
244
+ ].join("\n");
245
+
246
+ return `You are ${context.agent.name} (${context.agent.role}), agent ${context.agentId}.
247
+ Heartbeat run ${context.heartbeatRunId}.
248
+
249
+ Company:
250
+ - Name: ${context.company.name}
251
+ - Mission: ${context.company.mission ?? "No mission set"}
252
+
253
+ Goal context:
254
+ Company goals:
255
+ ${companyGoals}
256
+ Project goals:
257
+ ${projectGoals}
258
+ Agent goals:
259
+ ${agentGoals}
260
+
261
+ Assigned issues:
262
+ ${workItems}
263
+
264
+ ${executionDirectives}
265
+ ${credentialSnapshot}
266
+
267
+ At the end of your response, include exactly one JSON object on a single line:
268
+ {"summary":"...","tokenInput":123,"tokenOutput":456,"usdCost":0.123456,"executionMode":"local_work","gatedControlPlaneReasons":["..."]}
269
+ `;
270
+ }
271
+
272
+ function resolveControlPlaneCredentialState(context: HeartbeatContext) {
273
+ const env = {
274
+ ...process.env,
275
+ ...(context.runtime?.env ?? {})
276
+ };
277
+ const bopohqEnabled = Boolean(env.BOPOHQ_API_URL && env.BOPOHQ_API_KEY);
278
+ const missingReasons: string[] = [];
279
+ if (!bopohqEnabled) {
280
+ missingReasons.push("bopohq credentials missing (BOPOHQ_API_URL and/or BOPOHQ_API_KEY)");
281
+ }
282
+ return {
283
+ bopohqEnabled,
284
+ missingReasons
285
+ };
286
+ }
287
+
288
+ function defaultExecutionMode(credentialState: {
289
+ bopohqEnabled: boolean;
290
+ }) {
291
+ return credentialState.bopohqEnabled ? "control_plane" : "local_work";
292
+ }
293
+
294
+ function withExecutionDiagnostics(summary: string, executionMode: "local_work" | "control_plane", reasons: string[]) {
295
+ if (reasons.length === 0) {
296
+ return `${summary} [mode=${executionMode}]`;
297
+ }
298
+ return `${summary} [mode=${executionMode}; gated=${reasons.join(" | ")}]`;
299
+ }
300
+
301
+ function withProviderMetadata(
302
+ context: HeartbeatContext,
303
+ provider: string,
304
+ lastRuntimeMs?: number,
305
+ lastExitCode?: number | null
306
+ ) {
307
+ return {
308
+ ...context.state,
309
+ metadata: {
310
+ ...(context.state.metadata ?? {}),
311
+ lastProvider: provider,
312
+ lastHeartbeatRunId: context.heartbeatRunId,
313
+ ...(lastRuntimeMs === undefined ? {} : { lastRuntimeMs }),
314
+ ...(lastExitCode === undefined ? {} : { lastExitCode })
315
+ }
316
+ };
317
+ }
318
+
319
+ function toPreview(value: string, max = 1600) {
320
+ if (value.length <= max) {
321
+ return value;
322
+ }
323
+ return `${value.slice(0, max)}\n...[truncated]`;
324
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./adapters";
2
+ export * from "./registry";
3
+ export * from "./runtime";
4
+ export * from "./types";
@@ -0,0 +1,13 @@
1
+ import { ClaudeCodeAdapter, CodexAdapter, GenericHeartbeatAdapter } from "./adapters";
2
+ import type { AgentAdapter, AgentProviderType } from "./types";
3
+
4
+ const adapters: Record<AgentProviderType, AgentAdapter> = {
5
+ claude_code: new ClaudeCodeAdapter(),
6
+ codex: new CodexAdapter(),
7
+ http: new GenericHeartbeatAdapter("http"),
8
+ shell: new GenericHeartbeatAdapter("shell")
9
+ };
10
+
11
+ export function resolveAdapter(providerType: AgentProviderType) {
12
+ return adapters[providerType];
13
+ }