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/src/runtime.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, lstat, mkdir, mkdtemp, readdir, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { AgentRuntimeConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface RuntimeExecutionOutput {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
code: number | null;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
timedOut: boolean;
|
|
14
|
+
elapsedMs: number;
|
|
15
|
+
attemptCount: number;
|
|
16
|
+
failureType?: "timeout" | "spawn_error" | "nonzero_exit";
|
|
17
|
+
attempts: RuntimeAttemptTrace[];
|
|
18
|
+
parsedUsage?: {
|
|
19
|
+
tokenInput?: number;
|
|
20
|
+
tokenOutput?: number;
|
|
21
|
+
usdCost?: number;
|
|
22
|
+
summary?: string;
|
|
23
|
+
executionMode?: "local_work" | "control_plane";
|
|
24
|
+
gatedControlPlaneReasons?: string[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RuntimeAttemptTrace {
|
|
29
|
+
attempt: number;
|
|
30
|
+
code: number | null;
|
|
31
|
+
timedOut: boolean;
|
|
32
|
+
elapsedMs: number;
|
|
33
|
+
signal: NodeJS.Signals | null;
|
|
34
|
+
spawnErrorCode?: string;
|
|
35
|
+
forcedKill: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RuntimeCommandHealth {
|
|
39
|
+
command: string;
|
|
40
|
+
available: boolean;
|
|
41
|
+
exitCode: number | null;
|
|
42
|
+
elapsedMs: number;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pickDefaultCommand(provider: "claude_code" | "codex") {
|
|
47
|
+
if (provider === "claude_code") {
|
|
48
|
+
return "claude";
|
|
49
|
+
}
|
|
50
|
+
return "codex";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function providerDefaultArgs(provider: "claude_code" | "codex") {
|
|
54
|
+
if (provider === "claude_code") {
|
|
55
|
+
return ["-p"];
|
|
56
|
+
}
|
|
57
|
+
// Keep Codex non-interactive, sandboxed, and writable in-workspace by default.
|
|
58
|
+
// This avoids inheriting read-only profiles that can cause flaky heartbeat failures.
|
|
59
|
+
return ["exec", "--full-auto"];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function executeAgentRuntime(
|
|
63
|
+
provider: "claude_code" | "codex",
|
|
64
|
+
prompt: string,
|
|
65
|
+
config?: AgentRuntimeConfig
|
|
66
|
+
): Promise<RuntimeExecutionOutput> {
|
|
67
|
+
const commandOverride = Boolean(config?.command && config.command.trim().length > 0);
|
|
68
|
+
const effectiveRetryCount = config?.retryCount ?? (provider === "codex" ? 1 : 0);
|
|
69
|
+
const mergedArgs = [
|
|
70
|
+
...(commandOverride ? [] : providerDefaultArgs(provider)),
|
|
71
|
+
...(config?.args ?? [])
|
|
72
|
+
];
|
|
73
|
+
return executePromptRuntime(
|
|
74
|
+
config?.command ?? pickDefaultCommand(provider),
|
|
75
|
+
prompt,
|
|
76
|
+
{ ...config, args: mergedArgs, retryCount: effectiveRetryCount },
|
|
77
|
+
{
|
|
78
|
+
provider
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function executePromptRuntime(
|
|
84
|
+
command: string,
|
|
85
|
+
prompt: string,
|
|
86
|
+
config?: AgentRuntimeConfig,
|
|
87
|
+
options?: { provider?: "claude_code" | "codex" }
|
|
88
|
+
): Promise<RuntimeExecutionOutput> {
|
|
89
|
+
const baseArgs = [...(config?.args ?? [])];
|
|
90
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
91
|
+
const maxAttempts = Math.max(1, Math.min(3, 1 + (config?.retryCount ?? 0)));
|
|
92
|
+
const retryBackoffMs = Math.max(100, config?.retryBackoffMs ?? 400);
|
|
93
|
+
const env = {
|
|
94
|
+
...process.env,
|
|
95
|
+
...(config?.env ?? {})
|
|
96
|
+
};
|
|
97
|
+
const provider = options?.provider;
|
|
98
|
+
const injection = await prepareSkillInjection(provider, env);
|
|
99
|
+
const args = [...baseArgs, ...injection.additionalArgs, prompt];
|
|
100
|
+
const attempts: RuntimeAttemptTrace[] = [];
|
|
101
|
+
let stdout = "";
|
|
102
|
+
let stderr = injection.warning ? `${injection.warning}\n` : "";
|
|
103
|
+
let lastResult:
|
|
104
|
+
| {
|
|
105
|
+
code: number | null;
|
|
106
|
+
timedOut: boolean;
|
|
107
|
+
elapsedMs: number;
|
|
108
|
+
signal: NodeJS.Signals | null;
|
|
109
|
+
spawnErrorCode?: string;
|
|
110
|
+
forcedKill: boolean;
|
|
111
|
+
}
|
|
112
|
+
| undefined;
|
|
113
|
+
try {
|
|
114
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
115
|
+
const attemptResult = await executeSinglePromptAttempt(command, args, config?.cwd || process.cwd(), env, timeoutMs);
|
|
116
|
+
stdout += attemptResult.stdout;
|
|
117
|
+
stderr = [stderr, attemptResult.stderr].filter(Boolean).join("\n").trim();
|
|
118
|
+
attempts.push({
|
|
119
|
+
attempt,
|
|
120
|
+
code: attemptResult.code,
|
|
121
|
+
timedOut: attemptResult.timedOut,
|
|
122
|
+
elapsedMs: attemptResult.elapsedMs,
|
|
123
|
+
signal: attemptResult.signal,
|
|
124
|
+
spawnErrorCode: attemptResult.spawnErrorCode,
|
|
125
|
+
forcedKill: attemptResult.forcedKill
|
|
126
|
+
});
|
|
127
|
+
lastResult = {
|
|
128
|
+
code: attemptResult.code,
|
|
129
|
+
timedOut: attemptResult.timedOut,
|
|
130
|
+
elapsedMs: attemptResult.elapsedMs,
|
|
131
|
+
signal: attemptResult.signal,
|
|
132
|
+
spawnErrorCode: attemptResult.spawnErrorCode,
|
|
133
|
+
forcedKill: attemptResult.forcedKill
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (attemptResult.ok) {
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
code: attemptResult.code,
|
|
140
|
+
stdout,
|
|
141
|
+
stderr,
|
|
142
|
+
timedOut: false,
|
|
143
|
+
elapsedMs: attempts.reduce((sum, item) => sum + item.elapsedMs, 0),
|
|
144
|
+
attemptCount: attempts.length,
|
|
145
|
+
attempts,
|
|
146
|
+
parsedUsage: parseStructuredUsage(stdout)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const retryableSpawnError = Boolean(
|
|
151
|
+
attemptResult.spawnErrorCode && TRANSIENT_SPAWN_ERROR_CODES.has(attemptResult.spawnErrorCode)
|
|
152
|
+
);
|
|
153
|
+
const retryableCodexNonZero =
|
|
154
|
+
provider === "codex" && !attemptResult.timedOut && !attemptResult.spawnErrorCode && attemptResult.code !== 0;
|
|
155
|
+
const retryable = retryableSpawnError || retryableCodexNonZero;
|
|
156
|
+
if (!retryable || attempt >= maxAttempts) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
await sleep(retryBackoffMs * attempt);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: lastResult?.code ?? null,
|
|
165
|
+
stdout,
|
|
166
|
+
stderr,
|
|
167
|
+
timedOut: lastResult?.timedOut ?? false,
|
|
168
|
+
elapsedMs: attempts.reduce((sum, item) => sum + item.elapsedMs, 0),
|
|
169
|
+
attemptCount: attempts.length,
|
|
170
|
+
attempts,
|
|
171
|
+
failureType: classifyFailure(lastResult?.timedOut ?? false, lastResult?.spawnErrorCode, lastResult?.code ?? null),
|
|
172
|
+
parsedUsage: parseStructuredUsage(stdout)
|
|
173
|
+
};
|
|
174
|
+
} finally {
|
|
175
|
+
await injection.cleanup();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const SKILLS_DIR_NAME = "skills";
|
|
180
|
+
const CLAUDE_SKILLS_DIR = ".claude/skills";
|
|
181
|
+
const SKILL_MD = "SKILL.md";
|
|
182
|
+
|
|
183
|
+
type SkillInjectionContext = {
|
|
184
|
+
additionalArgs: string[];
|
|
185
|
+
warning?: string;
|
|
186
|
+
cleanup: () => Promise<void>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
async function prepareSkillInjection(
|
|
190
|
+
provider: "claude_code" | "codex" | undefined,
|
|
191
|
+
env: NodeJS.ProcessEnv
|
|
192
|
+
): Promise<SkillInjectionContext> {
|
|
193
|
+
if (!provider) {
|
|
194
|
+
return noSkillInjection();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const skillsSource = await resolveSkillsSourceDir();
|
|
198
|
+
if (!skillsSource) {
|
|
199
|
+
return {
|
|
200
|
+
...noSkillInjection(),
|
|
201
|
+
warning: "[bopohq] skills injection skipped: no skills directory found."
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (provider === "codex") {
|
|
206
|
+
try {
|
|
207
|
+
await ensureCodexSkillsInjected(skillsSource, env);
|
|
208
|
+
return noSkillInjection();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
...noSkillInjection(),
|
|
212
|
+
warning: `[bopohq] skills injection failed for codex: ${String(error)}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const tempSkillsRoot = await buildClaudeSkillsAddDir(skillsSource);
|
|
219
|
+
return {
|
|
220
|
+
additionalArgs: ["--add-dir", tempSkillsRoot],
|
|
221
|
+
cleanup: async () => {
|
|
222
|
+
await rm(tempSkillsRoot, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return {
|
|
227
|
+
...noSkillInjection(),
|
|
228
|
+
warning: `[bopohq] skills injection failed for claude_code: ${String(error)}`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function noSkillInjection(): SkillInjectionContext {
|
|
234
|
+
return {
|
|
235
|
+
additionalArgs: [],
|
|
236
|
+
cleanup: async () => {}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const TRANSIENT_SPAWN_ERROR_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE", "ETXTBSY", "EBUSY"]);
|
|
241
|
+
const FORCE_KILL_AFTER_MS = 2_000;
|
|
242
|
+
|
|
243
|
+
async function executeSinglePromptAttempt(
|
|
244
|
+
command: string,
|
|
245
|
+
args: string[],
|
|
246
|
+
cwd: string,
|
|
247
|
+
env: NodeJS.ProcessEnv,
|
|
248
|
+
timeoutMs: number
|
|
249
|
+
) {
|
|
250
|
+
const startedAt = Date.now();
|
|
251
|
+
return new Promise<{
|
|
252
|
+
ok: boolean;
|
|
253
|
+
code: number | null;
|
|
254
|
+
stdout: string;
|
|
255
|
+
stderr: string;
|
|
256
|
+
timedOut: boolean;
|
|
257
|
+
elapsedMs: number;
|
|
258
|
+
signal: NodeJS.Signals | null;
|
|
259
|
+
spawnErrorCode?: string;
|
|
260
|
+
forcedKill: boolean;
|
|
261
|
+
}>((resolve) => {
|
|
262
|
+
const child = spawn(command, args, {
|
|
263
|
+
cwd,
|
|
264
|
+
env,
|
|
265
|
+
shell: false
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let stdout = "";
|
|
269
|
+
let stderr = "";
|
|
270
|
+
let resolved = false;
|
|
271
|
+
let timedOut = false;
|
|
272
|
+
let forcedKill = false;
|
|
273
|
+
let timeoutKillTimer: NodeJS.Timeout | undefined;
|
|
274
|
+
const timeout = setTimeout(() => {
|
|
275
|
+
timedOut = true;
|
|
276
|
+
child.kill("SIGTERM");
|
|
277
|
+
timeoutKillTimer = setTimeout(() => {
|
|
278
|
+
if (!resolved) {
|
|
279
|
+
forcedKill = true;
|
|
280
|
+
child.kill("SIGKILL");
|
|
281
|
+
}
|
|
282
|
+
}, FORCE_KILL_AFTER_MS);
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
|
|
285
|
+
child.stdout.on("data", (chunk) => {
|
|
286
|
+
stdout += String(chunk);
|
|
287
|
+
});
|
|
288
|
+
child.stderr.on("data", (chunk) => {
|
|
289
|
+
stderr += String(chunk);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
child.on("close", (code, signal) => {
|
|
293
|
+
if (resolved) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
resolved = true;
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
if (timeoutKillTimer) {
|
|
299
|
+
clearTimeout(timeoutKillTimer);
|
|
300
|
+
}
|
|
301
|
+
resolve({
|
|
302
|
+
ok: code === 0 && !timedOut,
|
|
303
|
+
code,
|
|
304
|
+
stdout,
|
|
305
|
+
stderr,
|
|
306
|
+
timedOut,
|
|
307
|
+
elapsedMs: Date.now() - startedAt,
|
|
308
|
+
signal,
|
|
309
|
+
forcedKill
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
child.on("error", (error) => {
|
|
314
|
+
if (resolved) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
resolved = true;
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
if (timeoutKillTimer) {
|
|
320
|
+
clearTimeout(timeoutKillTimer);
|
|
321
|
+
}
|
|
322
|
+
const errorWithCode = error as NodeJS.ErrnoException;
|
|
323
|
+
resolve({
|
|
324
|
+
ok: false,
|
|
325
|
+
code: null,
|
|
326
|
+
stdout,
|
|
327
|
+
stderr: `${stderr}\n${String(error)}`.trim(),
|
|
328
|
+
timedOut,
|
|
329
|
+
elapsedMs: Date.now() - startedAt,
|
|
330
|
+
signal: null,
|
|
331
|
+
spawnErrorCode: errorWithCode.code,
|
|
332
|
+
forcedKill
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function classifyFailure(timedOut: boolean, spawnErrorCode: string | undefined, code: number | null) {
|
|
339
|
+
if (timedOut) {
|
|
340
|
+
return "timeout" as const;
|
|
341
|
+
}
|
|
342
|
+
if (spawnErrorCode) {
|
|
343
|
+
return "spawn_error" as const;
|
|
344
|
+
}
|
|
345
|
+
if (code !== 0) {
|
|
346
|
+
return "nonzero_exit" as const;
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function sleep(ms: number) {
|
|
352
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function checkRuntimeCommandHealth(
|
|
356
|
+
command: string,
|
|
357
|
+
options?: { cwd?: string; timeoutMs?: number }
|
|
358
|
+
): Promise<RuntimeCommandHealth> {
|
|
359
|
+
const startedAt = Date.now();
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
const child = spawn(command, ["--version"], {
|
|
362
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
363
|
+
env: process.env,
|
|
364
|
+
shell: false
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
let resolved = false;
|
|
368
|
+
const timeout = setTimeout(() => {
|
|
369
|
+
if (resolved) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
resolved = true;
|
|
373
|
+
child.kill("SIGTERM");
|
|
374
|
+
resolve({
|
|
375
|
+
command,
|
|
376
|
+
available: false,
|
|
377
|
+
exitCode: null,
|
|
378
|
+
elapsedMs: Date.now() - startedAt,
|
|
379
|
+
error: "Command health check timed out."
|
|
380
|
+
});
|
|
381
|
+
}, options?.timeoutMs ?? 5_000);
|
|
382
|
+
|
|
383
|
+
child.on("close", (code) => {
|
|
384
|
+
if (resolved) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
resolved = true;
|
|
388
|
+
clearTimeout(timeout);
|
|
389
|
+
resolve({
|
|
390
|
+
command,
|
|
391
|
+
available: true,
|
|
392
|
+
exitCode: code,
|
|
393
|
+
elapsedMs: Date.now() - startedAt
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
child.on("error", (error) => {
|
|
398
|
+
if (resolved) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
resolved = true;
|
|
402
|
+
clearTimeout(timeout);
|
|
403
|
+
resolve({
|
|
404
|
+
command,
|
|
405
|
+
available: false,
|
|
406
|
+
exitCode: null,
|
|
407
|
+
elapsedMs: Date.now() - startedAt,
|
|
408
|
+
error: String(error)
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function resolveSkillsSourceDir() {
|
|
415
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
416
|
+
const candidates = [resolve(moduleDir, "../../../skills"), resolve(process.cwd(), "skills")];
|
|
417
|
+
for (const candidate of candidates) {
|
|
418
|
+
if (await isDirectory(candidate)) {
|
|
419
|
+
return candidate;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function ensureCodexSkillsInjected(skillsSourceDir: string, env: NodeJS.ProcessEnv) {
|
|
426
|
+
const codexHome = resolveCodexHome(env);
|
|
427
|
+
const targetRoot = join(codexHome, SKILLS_DIR_NAME);
|
|
428
|
+
await mkdir(targetRoot, { recursive: true });
|
|
429
|
+
|
|
430
|
+
const entries = await readdir(skillsSourceDir, { withFileTypes: true });
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
if (!entry.isDirectory()) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const source = join(skillsSourceDir, entry.name);
|
|
436
|
+
if (!(await hasSkillManifest(source))) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const target = join(targetRoot, entry.name);
|
|
440
|
+
const existing = await lstat(target).catch(() => null);
|
|
441
|
+
if (existing) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
await symlink(source, target);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function resolveCodexHome(env: NodeJS.ProcessEnv) {
|
|
449
|
+
const configured = env.CODEX_HOME?.trim();
|
|
450
|
+
if (configured) {
|
|
451
|
+
return configured;
|
|
452
|
+
}
|
|
453
|
+
return join(homedir(), ".codex");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function buildClaudeSkillsAddDir(skillsSourceDir: string) {
|
|
457
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "bopohq-skills-"));
|
|
458
|
+
const skillsTargetDir = join(tempRoot, CLAUDE_SKILLS_DIR);
|
|
459
|
+
await mkdir(skillsTargetDir, { recursive: true });
|
|
460
|
+
|
|
461
|
+
const entries = await readdir(skillsSourceDir, { withFileTypes: true });
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
if (!entry.isDirectory()) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const source = join(skillsSourceDir, entry.name);
|
|
467
|
+
if (!(await hasSkillManifest(source))) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
await symlink(source, join(skillsTargetDir, entry.name));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return tempRoot;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function hasSkillManifest(skillDir: string) {
|
|
477
|
+
return fileExists(join(skillDir, SKILL_MD));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function isDirectory(path: string) {
|
|
481
|
+
const stats = await lstat(path).catch(() => null);
|
|
482
|
+
return stats?.isDirectory() ?? false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function fileExists(path: string) {
|
|
486
|
+
try {
|
|
487
|
+
await access(path);
|
|
488
|
+
return true;
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function parseStructuredUsage(stdout: string) {
|
|
495
|
+
const lines = stdout
|
|
496
|
+
.split(/\r?\n/)
|
|
497
|
+
.map((line) => line.trim())
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
|
|
500
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
501
|
+
const candidate = lines[index];
|
|
502
|
+
if (candidate?.startsWith("{") && candidate.endsWith("}")) {
|
|
503
|
+
const parsed = tryParseUsage(candidate);
|
|
504
|
+
if (parsed) {
|
|
505
|
+
return parsed;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const fragments = candidate?.match(/\{[^{}]+\}/g) ?? [];
|
|
510
|
+
for (let fragmentIndex = fragments.length - 1; fragmentIndex >= 0; fragmentIndex -= 1) {
|
|
511
|
+
const parsed = tryParseUsage(fragments[fragmentIndex] ?? "");
|
|
512
|
+
if (parsed) {
|
|
513
|
+
return parsed;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function tryParseUsage(candidate: string) {
|
|
522
|
+
try {
|
|
523
|
+
const parsed = JSON.parse(candidate) as Record<string, unknown>;
|
|
524
|
+
const tokenInput = toNumber(parsed.tokenInput);
|
|
525
|
+
const tokenOutput = toNumber(parsed.tokenOutput);
|
|
526
|
+
const usdCost = toNumber(parsed.usdCost);
|
|
527
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary : undefined;
|
|
528
|
+
const executionMode: "local_work" | "control_plane" | undefined =
|
|
529
|
+
parsed.executionMode === "local_work" || parsed.executionMode === "control_plane"
|
|
530
|
+
? parsed.executionMode
|
|
531
|
+
: undefined;
|
|
532
|
+
const gatedControlPlaneReasons = Array.isArray(parsed.gatedControlPlaneReasons)
|
|
533
|
+
? parsed.gatedControlPlaneReasons.map((item) => String(item))
|
|
534
|
+
: undefined;
|
|
535
|
+
if (isPromptTemplateUsage(summary, tokenInput, tokenOutput, usdCost, executionMode, gatedControlPlaneReasons)) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
if (
|
|
539
|
+
tokenInput === undefined &&
|
|
540
|
+
tokenOutput === undefined &&
|
|
541
|
+
usdCost === undefined &&
|
|
542
|
+
!summary &&
|
|
543
|
+
!executionMode &&
|
|
544
|
+
!gatedControlPlaneReasons
|
|
545
|
+
) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
return { tokenInput, tokenOutput, usdCost, summary, executionMode, gatedControlPlaneReasons };
|
|
549
|
+
} catch {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function isPromptTemplateUsage(
|
|
555
|
+
summary: string | undefined,
|
|
556
|
+
tokenInput: number | undefined,
|
|
557
|
+
tokenOutput: number | undefined,
|
|
558
|
+
usdCost: number | undefined,
|
|
559
|
+
executionMode: "local_work" | "control_plane" | undefined,
|
|
560
|
+
gatedControlPlaneReasons: string[] | undefined
|
|
561
|
+
) {
|
|
562
|
+
return (
|
|
563
|
+
summary === "..." &&
|
|
564
|
+
tokenInput === 123 &&
|
|
565
|
+
tokenOutput === 456 &&
|
|
566
|
+
usdCost === 0.123456 &&
|
|
567
|
+
executionMode === "local_work" &&
|
|
568
|
+
Array.isArray(gatedControlPlaneReasons) &&
|
|
569
|
+
gatedControlPlaneReasons.length === 1 &&
|
|
570
|
+
gatedControlPlaneReasons[0] === "..."
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function toNumber(value: unknown) {
|
|
575
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
if (typeof value === "string") {
|
|
579
|
+
const parsed = Number(value);
|
|
580
|
+
if (Number.isFinite(parsed)) {
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProviderType } from "bopodev-contracts";
|
|
2
|
+
|
|
3
|
+
export type AgentProviderType = ProviderType;
|
|
4
|
+
|
|
5
|
+
export interface AgentWorkItem {
|
|
6
|
+
issueId: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
projectName?: string | null;
|
|
9
|
+
title: string;
|
|
10
|
+
body?: string | null;
|
|
11
|
+
status?: string;
|
|
12
|
+
priority?: string;
|
|
13
|
+
labels?: string[];
|
|
14
|
+
tags?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentState {
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HeartbeatContext {
|
|
24
|
+
companyId: string;
|
|
25
|
+
agentId: string;
|
|
26
|
+
providerType: AgentProviderType;
|
|
27
|
+
heartbeatRunId: string;
|
|
28
|
+
company: {
|
|
29
|
+
name: string;
|
|
30
|
+
mission?: string | null;
|
|
31
|
+
};
|
|
32
|
+
agent: {
|
|
33
|
+
name: string;
|
|
34
|
+
role: string;
|
|
35
|
+
managerAgentId?: string | null;
|
|
36
|
+
};
|
|
37
|
+
workItems: AgentWorkItem[];
|
|
38
|
+
goalContext?: {
|
|
39
|
+
companyGoals: string[];
|
|
40
|
+
projectGoals: string[];
|
|
41
|
+
agentGoals: string[];
|
|
42
|
+
};
|
|
43
|
+
state: AgentState;
|
|
44
|
+
runtime?: AgentRuntimeConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AdapterExecutionResult {
|
|
48
|
+
status: "ok" | "skipped" | "failed";
|
|
49
|
+
summary: string;
|
|
50
|
+
tokenInput: number;
|
|
51
|
+
tokenOutput: number;
|
|
52
|
+
usdCost: number;
|
|
53
|
+
nextState?: AgentState;
|
|
54
|
+
trace?: AdapterTrace;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AgentAdapter {
|
|
58
|
+
providerType: AgentProviderType;
|
|
59
|
+
execute(context: HeartbeatContext): Promise<AdapterExecutionResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AgentRuntimeConfig {
|
|
63
|
+
command?: string;
|
|
64
|
+
args?: string[];
|
|
65
|
+
cwd?: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
retryCount?: number;
|
|
68
|
+
retryBackoffMs?: number;
|
|
69
|
+
env?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AdapterTrace {
|
|
73
|
+
command?: string;
|
|
74
|
+
exitCode?: number | null;
|
|
75
|
+
elapsedMs?: number;
|
|
76
|
+
timedOut?: boolean;
|
|
77
|
+
failureType?: string;
|
|
78
|
+
attemptCount?: number;
|
|
79
|
+
attempts?: Array<{
|
|
80
|
+
attempt: number;
|
|
81
|
+
code: number | null;
|
|
82
|
+
timedOut: boolean;
|
|
83
|
+
elapsedMs: number;
|
|
84
|
+
signal: NodeJS.Signals | null;
|
|
85
|
+
spawnErrorCode?: string;
|
|
86
|
+
forcedKill: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
stdoutPreview?: string;
|
|
89
|
+
stderrPreview?: string;
|
|
90
|
+
executionMode?: "local_work" | "control_plane";
|
|
91
|
+
gatedControlPlaneReasons?: string[];
|
|
92
|
+
}
|