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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/agent-sdk/src/adapters.d.ts +14 -0
- package/dist/agent-sdk/src/index.d.ts +4 -0
- package/dist/agent-sdk/src/registry.d.ts +2 -0
- package/dist/agent-sdk/src/runtime.d.ts +44 -0
- package/dist/agent-sdk/src/types.d.ts +84 -0
- package/dist/contracts/src/index.d.ts +995 -0
- package/package.json +16 -0
- package/src/adapters.ts +324 -0
- package/src/index.ts +4 -0
- package/src/registry.ts +13 -0
- package/src/runtime.ts +585 -0
- package/src/types.ts +92 -0
- package/tsconfig.json +9 -0
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
|
+
}
|
package/src/adapters.ts
ADDED
|
@@ -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
package/src/registry.ts
ADDED
|
@@ -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
|
+
}
|