bopodev-agent-sdk 0.1.9 → 0.1.11
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 +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/LICENSE +1 -1
- package/dist/agent-sdk/src/adapters.d.ts +12 -1
- package/dist/agent-sdk/src/registry.d.ts +4 -1
- package/dist/agent-sdk/src/runtime.d.ts +42 -1
- package/dist/agent-sdk/src/types.d.ts +81 -1
- package/dist/contracts/src/index.d.ts +140 -1
- package/package.json +2 -2
- package/src/adapters.ts +1189 -14
- package/src/registry.ts +38 -2
- package/src/runtime.ts +1254 -37
- package/src/types.ts +90 -1
package/src/adapters.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AdapterEnvironmentCheck,
|
|
3
|
+
AdapterEnvironmentResult,
|
|
4
|
+
AdapterExecutionResult,
|
|
5
|
+
AdapterMetadata,
|
|
6
|
+
AdapterModelOption,
|
|
7
|
+
AgentAdapter,
|
|
8
|
+
AgentProviderType,
|
|
9
|
+
AgentRuntimeConfig,
|
|
10
|
+
HeartbeatContext
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { ExecutionOutcomeSchema, type ExecutionOutcome } from "bopodev-contracts";
|
|
13
|
+
import { checkRuntimeCommandHealth, executeAgentRuntime, executePromptRuntime } from "./runtime";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
3
16
|
|
|
4
17
|
function summarizeWork(context: HeartbeatContext) {
|
|
5
18
|
if (context.workItems.length === 0) {
|
|
@@ -8,6 +21,14 @@ function summarizeWork(context: HeartbeatContext) {
|
|
|
8
21
|
return `Processed ${context.workItems.length} assigned issue(s).`;
|
|
9
22
|
}
|
|
10
23
|
|
|
24
|
+
function issueIdsTouched(context: HeartbeatContext) {
|
|
25
|
+
return context.workItems.map((item) => item.issueId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toOutcome(outcome: ExecutionOutcome): ExecutionOutcome {
|
|
29
|
+
return ExecutionOutcomeSchema.parse(outcome);
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
export class ClaudeCodeAdapter implements AgentAdapter {
|
|
12
33
|
providerType = "claude_code" as const;
|
|
13
34
|
|
|
@@ -36,6 +57,28 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
|
|
60
|
+
export class CursorAdapter implements AgentAdapter {
|
|
61
|
+
providerType = "cursor" as const;
|
|
62
|
+
|
|
63
|
+
async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
|
|
64
|
+
if (context.workItems.length === 0) {
|
|
65
|
+
return createSkippedResult("Cursor", "cursor", context);
|
|
66
|
+
}
|
|
67
|
+
return runCursorWork(context);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class OpenCodeAdapter implements AgentAdapter {
|
|
72
|
+
providerType = "opencode" as const;
|
|
73
|
+
|
|
74
|
+
async execute(context: HeartbeatContext): Promise<AdapterExecutionResult> {
|
|
75
|
+
if (context.workItems.length === 0) {
|
|
76
|
+
return createSkippedResult("OpenCode", "opencode", context);
|
|
77
|
+
}
|
|
78
|
+
return runOpenCodeWork(context);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
39
82
|
export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
40
83
|
constructor(public providerType: "http" | "shell") {}
|
|
41
84
|
|
|
@@ -50,6 +93,14 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
50
93
|
tokenInput: 0,
|
|
51
94
|
tokenOutput: 0,
|
|
52
95
|
usdCost: 0,
|
|
96
|
+
outcome: toOutcome({
|
|
97
|
+
kind: "failed",
|
|
98
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
99
|
+
actions: [{ type: "runtime.launch", status: "error", detail: "Missing runtime command." }],
|
|
100
|
+
blockers: [{ code: "runtime_command_missing", message: "Runtime command is required.", retryable: false }],
|
|
101
|
+
artifacts: [],
|
|
102
|
+
nextSuggestedState: "blocked"
|
|
103
|
+
}),
|
|
53
104
|
nextState: context.state
|
|
54
105
|
};
|
|
55
106
|
}
|
|
@@ -57,22 +108,73 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
57
108
|
const prompt = createPrompt(context);
|
|
58
109
|
const runtime = await executePromptRuntime(context.runtime.command, prompt, context.runtime);
|
|
59
110
|
if (runtime.ok) {
|
|
111
|
+
if (!runtime.parsedUsage) {
|
|
112
|
+
const detail = buildMissingStructuredOutputDetail(this.providerType, runtime);
|
|
113
|
+
return {
|
|
114
|
+
status: "failed",
|
|
115
|
+
summary: `${this.providerType} runtime failed: ${detail}`,
|
|
116
|
+
tokenInput: 0,
|
|
117
|
+
tokenOutput: 0,
|
|
118
|
+
usdCost: 0,
|
|
119
|
+
outcome: toOutcome({
|
|
120
|
+
kind: "failed",
|
|
121
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
122
|
+
actions: [{ type: "runtime.execute", status: "error", detail }],
|
|
123
|
+
blockers: [{ code: "missing_structured_output", message: detail, retryable: true }],
|
|
124
|
+
artifacts: [],
|
|
125
|
+
nextSuggestedState: "blocked"
|
|
126
|
+
}),
|
|
127
|
+
trace: {
|
|
128
|
+
command: runtime.commandUsed ?? context.runtime.command,
|
|
129
|
+
args: runtime.argsUsed,
|
|
130
|
+
cwd: context.runtime?.cwd,
|
|
131
|
+
exitCode: runtime.code,
|
|
132
|
+
elapsedMs: runtime.elapsedMs,
|
|
133
|
+
timedOut: runtime.timedOut,
|
|
134
|
+
failureType: "missing_structured_output",
|
|
135
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
136
|
+
attemptCount: runtime.attemptCount,
|
|
137
|
+
attempts: runtime.attempts,
|
|
138
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
139
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
140
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
141
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
142
|
+
transcript: runtime.transcript
|
|
143
|
+
},
|
|
144
|
+
nextState: context.state
|
|
145
|
+
};
|
|
146
|
+
}
|
|
60
147
|
return {
|
|
61
148
|
status: "ok",
|
|
62
149
|
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime finished in ${runtime.elapsedMs}ms.`,
|
|
63
150
|
tokenInput: runtime.parsedUsage?.tokenInput ?? 0,
|
|
64
151
|
tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
|
|
65
152
|
usdCost: runtime.parsedUsage?.usdCost ?? 0,
|
|
153
|
+
outcome: toOutcome({
|
|
154
|
+
kind: "completed",
|
|
155
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
156
|
+
actions: [{ type: "runtime.execute", status: "ok", detail: `${this.providerType} runtime completed.` }],
|
|
157
|
+
blockers: [],
|
|
158
|
+
artifacts: [],
|
|
159
|
+
nextSuggestedState: "in_review"
|
|
160
|
+
}),
|
|
66
161
|
trace: {
|
|
67
|
-
command: context.runtime.command,
|
|
162
|
+
command: runtime.commandUsed ?? context.runtime.command,
|
|
163
|
+
args: runtime.argsUsed,
|
|
164
|
+
cwd: context.runtime?.cwd,
|
|
68
165
|
exitCode: runtime.code,
|
|
69
166
|
elapsedMs: runtime.elapsedMs,
|
|
70
167
|
timedOut: runtime.timedOut,
|
|
71
168
|
failureType: runtime.failureType,
|
|
169
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
170
|
+
usageSource: "structured",
|
|
72
171
|
attemptCount: runtime.attemptCount,
|
|
73
172
|
attempts: runtime.attempts,
|
|
173
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
174
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
74
175
|
stdoutPreview: toPreview(runtime.stdout),
|
|
75
|
-
stderrPreview: toPreview(runtime.stderr)
|
|
176
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
177
|
+
transcript: runtime.transcript
|
|
76
178
|
},
|
|
77
179
|
nextState: withProviderMetadata(context, this.providerType, runtime.elapsedMs, runtime.code)
|
|
78
180
|
};
|
|
@@ -82,29 +184,255 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
82
184
|
inputRate: 0.000001,
|
|
83
185
|
outputRate: 0.000004
|
|
84
186
|
});
|
|
187
|
+
const failureDetail = resolveRuntimeFailureDetail(runtime);
|
|
85
188
|
return {
|
|
86
189
|
status: "failed",
|
|
87
|
-
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime failed: ${
|
|
190
|
+
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime failed: ${failureDetail}`,
|
|
88
191
|
tokenInput: failedUsage.tokenInput,
|
|
89
192
|
tokenOutput: failedUsage.tokenOutput,
|
|
90
193
|
usdCost: failedUsage.usdCost,
|
|
194
|
+
outcome: toOutcome({
|
|
195
|
+
kind: "failed",
|
|
196
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
197
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failureDetail }],
|
|
198
|
+
blockers: [{ code: runtime.failureType ?? "runtime_failed", message: failureDetail, retryable: true }],
|
|
199
|
+
artifacts: [],
|
|
200
|
+
nextSuggestedState: "blocked"
|
|
201
|
+
}),
|
|
91
202
|
trace: {
|
|
92
|
-
command: context.runtime.command,
|
|
203
|
+
command: runtime.commandUsed ?? context.runtime.command,
|
|
204
|
+
args: runtime.argsUsed,
|
|
205
|
+
cwd: context.runtime?.cwd,
|
|
93
206
|
exitCode: runtime.code,
|
|
94
207
|
elapsedMs: runtime.elapsedMs,
|
|
95
208
|
timedOut: runtime.timedOut,
|
|
96
209
|
failureType: runtime.failureType,
|
|
210
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
97
211
|
attemptCount: runtime.attemptCount,
|
|
98
212
|
attempts: runtime.attempts,
|
|
99
213
|
usageSource: failedUsage.source,
|
|
214
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
215
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
100
216
|
stdoutPreview: toPreview(runtime.stdout),
|
|
101
|
-
stderrPreview: toPreview(runtime.stderr)
|
|
217
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
218
|
+
transcript: runtime.transcript
|
|
102
219
|
},
|
|
103
220
|
nextState: context.state
|
|
104
221
|
};
|
|
105
222
|
}
|
|
106
223
|
}
|
|
107
224
|
|
|
225
|
+
type ProviderSessionUpdate = {
|
|
226
|
+
currentSessionId?: string | null;
|
|
227
|
+
resumedSessionId?: string | null;
|
|
228
|
+
resumeAttempted?: boolean;
|
|
229
|
+
resumeSkippedReason?: string | null;
|
|
230
|
+
clearedStaleSession?: boolean;
|
|
231
|
+
cwd?: string | null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const staticMetadata: AdapterMetadata[] = [
|
|
235
|
+
{
|
|
236
|
+
providerType: "claude_code",
|
|
237
|
+
label: "Claude Code",
|
|
238
|
+
supportsModelSelection: true,
|
|
239
|
+
supportsEnvironmentTest: true,
|
|
240
|
+
supportsWebSearch: false,
|
|
241
|
+
supportsThinkingEffort: true,
|
|
242
|
+
requiresRuntimeCwd: true
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
providerType: "codex",
|
|
246
|
+
label: "Codex",
|
|
247
|
+
supportsModelSelection: true,
|
|
248
|
+
supportsEnvironmentTest: true,
|
|
249
|
+
supportsWebSearch: true,
|
|
250
|
+
supportsThinkingEffort: true,
|
|
251
|
+
requiresRuntimeCwd: true
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
providerType: "cursor",
|
|
255
|
+
label: "Cursor",
|
|
256
|
+
supportsModelSelection: true,
|
|
257
|
+
supportsEnvironmentTest: true,
|
|
258
|
+
supportsWebSearch: false,
|
|
259
|
+
supportsThinkingEffort: false,
|
|
260
|
+
requiresRuntimeCwd: true
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
providerType: "opencode",
|
|
264
|
+
label: "OpenCode",
|
|
265
|
+
supportsModelSelection: true,
|
|
266
|
+
supportsEnvironmentTest: true,
|
|
267
|
+
supportsWebSearch: false,
|
|
268
|
+
supportsThinkingEffort: false,
|
|
269
|
+
requiresRuntimeCwd: true
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
providerType: "http",
|
|
273
|
+
label: "HTTP",
|
|
274
|
+
supportsModelSelection: false,
|
|
275
|
+
supportsEnvironmentTest: false,
|
|
276
|
+
supportsWebSearch: false,
|
|
277
|
+
supportsThinkingEffort: false,
|
|
278
|
+
requiresRuntimeCwd: false
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
providerType: "shell",
|
|
282
|
+
label: "Shell",
|
|
283
|
+
supportsModelSelection: false,
|
|
284
|
+
supportsEnvironmentTest: true,
|
|
285
|
+
supportsWebSearch: false,
|
|
286
|
+
supportsThinkingEffort: false,
|
|
287
|
+
requiresRuntimeCwd: true
|
|
288
|
+
}
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const modelCatalog: Record<Exclude<AgentProviderType, "http" | "shell">, AdapterModelOption[]> = {
|
|
292
|
+
codex: [
|
|
293
|
+
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
|
294
|
+
{ id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
|
|
295
|
+
{ id: "gpt-5", label: "GPT-5" },
|
|
296
|
+
{ id: "o3", label: "o3" },
|
|
297
|
+
{ id: "o4-mini", label: "o4-mini" },
|
|
298
|
+
{ id: "gpt-5-mini", label: "GPT-5 Mini" },
|
|
299
|
+
{ id: "gpt-5-nano", label: "GPT-5 Nano" },
|
|
300
|
+
{ id: "o3-mini", label: "o3-mini" },
|
|
301
|
+
{ id: "codex-mini-latest", label: "Codex Mini" }
|
|
302
|
+
],
|
|
303
|
+
claude_code: [
|
|
304
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
305
|
+
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
|
|
306
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }
|
|
307
|
+
],
|
|
308
|
+
cursor: [
|
|
309
|
+
{ id: "auto", label: "Auto" },
|
|
310
|
+
{ id: "gpt-5.3-codex", label: "gpt-5.3-codex" },
|
|
311
|
+
{ id: "gpt-5.3-codex-fast", label: "gpt-5.3-codex-fast" },
|
|
312
|
+
{ id: "sonnet-4.5", label: "sonnet-4.5" },
|
|
313
|
+
{ id: "opus-4.6", label: "opus-4.6" }
|
|
314
|
+
],
|
|
315
|
+
opencode: []
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export function listAdapterMetadata(): AdapterMetadata[] {
|
|
319
|
+
return staticMetadata;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function listAdapterModels(
|
|
323
|
+
providerType: AgentProviderType,
|
|
324
|
+
runtime?: AgentRuntimeConfig
|
|
325
|
+
): Promise<AdapterModelOption[]> {
|
|
326
|
+
if (providerType === "http" || providerType === "shell") {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
if (providerType === "cursor") {
|
|
330
|
+
const discovered = await discoverCursorModels(runtime);
|
|
331
|
+
return dedupeModels([...discovered, ...modelCatalog.cursor]);
|
|
332
|
+
}
|
|
333
|
+
if (providerType === "opencode") {
|
|
334
|
+
const discovered = await discoverOpenCodeModels(runtime);
|
|
335
|
+
return dedupeModels(discovered);
|
|
336
|
+
}
|
|
337
|
+
return modelCatalog[providerType];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function testAdapterEnvironment(
|
|
341
|
+
providerType: AgentProviderType,
|
|
342
|
+
runtime?: AgentRuntimeConfig
|
|
343
|
+
): Promise<AdapterEnvironmentResult> {
|
|
344
|
+
const checks: AdapterEnvironmentCheck[] = [];
|
|
345
|
+
const command =
|
|
346
|
+
providerType === "cursor"
|
|
347
|
+
? (await resolveCursorLaunchConfig(runtime)).command
|
|
348
|
+
: resolveRuntimeCommand(providerType, runtime);
|
|
349
|
+
const cwd = runtime?.cwd?.trim() || process.cwd();
|
|
350
|
+
const health = await checkRuntimeCommandHealth(command, { cwd, timeoutMs: 5_000 });
|
|
351
|
+
|
|
352
|
+
if (!health.available) {
|
|
353
|
+
checks.push({
|
|
354
|
+
code: "command_unavailable",
|
|
355
|
+
level: "error",
|
|
356
|
+
message: `Command is not executable: ${command}`,
|
|
357
|
+
detail: health.error
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
providerType,
|
|
361
|
+
status: "fail",
|
|
362
|
+
testedAt: new Date().toISOString(),
|
|
363
|
+
checks
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
checks.push({
|
|
368
|
+
code: "command_available",
|
|
369
|
+
level: "info",
|
|
370
|
+
message: `Command is executable: ${command}`
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (providerType === "http") {
|
|
374
|
+
return { providerType, status: "pass", testedAt: new Date().toISOString(), checks };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (providerType === "opencode" && !runtime?.model?.trim()) {
|
|
378
|
+
checks.push({
|
|
379
|
+
code: "model_missing",
|
|
380
|
+
level: "error",
|
|
381
|
+
message: "OpenCode requires a model in provider/model format."
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const probe = await runRuntimeProbe(providerType, runtime);
|
|
386
|
+
if (probe.timedOut) {
|
|
387
|
+
checks.push({
|
|
388
|
+
code: "probe_timeout",
|
|
389
|
+
level: "warn",
|
|
390
|
+
message: "Environment probe timed out."
|
|
391
|
+
});
|
|
392
|
+
} else if (probe.ok) {
|
|
393
|
+
checks.push({
|
|
394
|
+
code: "probe_ok",
|
|
395
|
+
level: "info",
|
|
396
|
+
message: "Environment probe succeeded."
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
const detail = `${probe.stderr}\n${probe.stdout}`.trim().slice(0, 500);
|
|
400
|
+
const normalizedDetail = detail.toLowerCase();
|
|
401
|
+
if (
|
|
402
|
+
providerType === "codex" &&
|
|
403
|
+
normalizedDetail.includes("401 unauthorized") &&
|
|
404
|
+
(normalizedDetail.includes("missing bearer") || normalizedDetail.includes("authentication"))
|
|
405
|
+
) {
|
|
406
|
+
checks.push({
|
|
407
|
+
code: "codex_auth_required",
|
|
408
|
+
level: "warn",
|
|
409
|
+
message: "Codex authentication is not ready for this runtime.",
|
|
410
|
+
detail,
|
|
411
|
+
hint: "Run `codex login` locally or provide OPENAI_API_KEY."
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
providerType,
|
|
415
|
+
status: toEnvironmentStatus(checks),
|
|
416
|
+
testedAt: new Date().toISOString(),
|
|
417
|
+
checks
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
checks.push({
|
|
421
|
+
code: "probe_failed",
|
|
422
|
+
level: providerType === "codex" || providerType === "cursor" || providerType === "opencode" ? "warn" : "error",
|
|
423
|
+
message: "Environment probe failed.",
|
|
424
|
+
detail
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
providerType,
|
|
430
|
+
status: toEnvironmentStatus(checks),
|
|
431
|
+
testedAt: new Date().toISOString(),
|
|
432
|
+
checks
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
108
436
|
function createSkippedResult(providerLabel: string, providerKey: string, context: HeartbeatContext): AdapterExecutionResult {
|
|
109
437
|
return {
|
|
110
438
|
status: "skipped",
|
|
@@ -112,6 +440,14 @@ function createSkippedResult(providerLabel: string, providerKey: string, context
|
|
|
112
440
|
tokenInput: 0,
|
|
113
441
|
tokenOutput: 0,
|
|
114
442
|
usdCost: 0,
|
|
443
|
+
outcome: toOutcome({
|
|
444
|
+
kind: "skipped",
|
|
445
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
446
|
+
actions: [{ type: "heartbeat.skip", status: "warn", detail: summarizeWork(context) }],
|
|
447
|
+
blockers: [],
|
|
448
|
+
artifacts: [],
|
|
449
|
+
nextSuggestedState: "todo"
|
|
450
|
+
}),
|
|
115
451
|
nextState: withProviderMetadata(context, providerKey)
|
|
116
452
|
};
|
|
117
453
|
}
|
|
@@ -124,6 +460,79 @@ async function runProviderWork(
|
|
|
124
460
|
const prompt = createPrompt(context);
|
|
125
461
|
const runtime = await executeAgentRuntime(provider, prompt, context.runtime);
|
|
126
462
|
if (runtime.ok) {
|
|
463
|
+
if (!runtime.parsedUsage) {
|
|
464
|
+
const detail = buildMissingStructuredOutputDetail(provider, runtime);
|
|
465
|
+
return {
|
|
466
|
+
status: "failed",
|
|
467
|
+
summary: `${provider} runtime failed: ${detail}`,
|
|
468
|
+
tokenInput: 0,
|
|
469
|
+
tokenOutput: 0,
|
|
470
|
+
usdCost: 0,
|
|
471
|
+
outcome: toOutcome({
|
|
472
|
+
kind: "failed",
|
|
473
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
474
|
+
actions: [{ type: "runtime.execute", status: "error", detail }],
|
|
475
|
+
blockers: [{ code: "missing_structured_output", message: detail, retryable: true }],
|
|
476
|
+
artifacts: [],
|
|
477
|
+
nextSuggestedState: "blocked"
|
|
478
|
+
}),
|
|
479
|
+
trace: {
|
|
480
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
481
|
+
args: runtime.argsUsed,
|
|
482
|
+
cwd: context.runtime?.cwd,
|
|
483
|
+
exitCode: runtime.code,
|
|
484
|
+
elapsedMs: runtime.elapsedMs,
|
|
485
|
+
timedOut: runtime.timedOut,
|
|
486
|
+
failureType: "missing_structured_output",
|
|
487
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
488
|
+
attemptCount: runtime.attemptCount,
|
|
489
|
+
attempts: runtime.attempts,
|
|
490
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
491
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
492
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
493
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
494
|
+
transcript: runtime.transcript
|
|
495
|
+
},
|
|
496
|
+
nextState: context.state
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (provider === "claude_code" && isClaudeRunIncomplete(runtime)) {
|
|
500
|
+
const detail = "Claude run reached max-turns before completing execution for this issue.";
|
|
501
|
+
return {
|
|
502
|
+
status: "failed",
|
|
503
|
+
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${detail}`,
|
|
504
|
+
tokenInput: runtime.parsedUsage?.tokenInput ?? 0,
|
|
505
|
+
tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
|
|
506
|
+
usdCost: runtime.parsedUsage?.usdCost ?? 0,
|
|
507
|
+
outcome: toOutcome({
|
|
508
|
+
kind: "failed",
|
|
509
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
510
|
+
actions: [{ type: "runtime.execute", status: "error", detail }],
|
|
511
|
+
blockers: [{ code: "max_turns_reached", message: detail, retryable: true }],
|
|
512
|
+
artifacts: [],
|
|
513
|
+
nextSuggestedState: "blocked"
|
|
514
|
+
}),
|
|
515
|
+
trace: {
|
|
516
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
517
|
+
args: runtime.argsUsed,
|
|
518
|
+
cwd: context.runtime?.cwd,
|
|
519
|
+
exitCode: runtime.code,
|
|
520
|
+
elapsedMs: runtime.elapsedMs,
|
|
521
|
+
timedOut: runtime.timedOut,
|
|
522
|
+
failureType: "max_turns_reached",
|
|
523
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
524
|
+
usageSource: "structured",
|
|
525
|
+
attemptCount: runtime.attemptCount,
|
|
526
|
+
attempts: runtime.attempts,
|
|
527
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
528
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
529
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
530
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
531
|
+
transcript: runtime.transcript
|
|
532
|
+
},
|
|
533
|
+
nextState: context.state
|
|
534
|
+
};
|
|
535
|
+
}
|
|
127
536
|
const fallbackOutputTokens = Math.max(Math.round(runtime.stdout.length / 4), 80);
|
|
128
537
|
const fallbackInputTokens = Math.max(Math.round(prompt.length / 4), 120);
|
|
129
538
|
const tokenInput = runtime.parsedUsage?.tokenInput ?? fallbackInputTokens;
|
|
@@ -139,43 +548,202 @@ async function runProviderWork(
|
|
|
139
548
|
tokenInput,
|
|
140
549
|
tokenOutput,
|
|
141
550
|
usdCost,
|
|
551
|
+
outcome: toOutcome({
|
|
552
|
+
kind: "completed",
|
|
553
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
554
|
+
actions: [{ type: "runtime.execute", status: "ok", detail: `${provider} runtime completed.` }],
|
|
555
|
+
blockers: [],
|
|
556
|
+
artifacts: [],
|
|
557
|
+
nextSuggestedState: "in_review"
|
|
558
|
+
}),
|
|
142
559
|
trace: {
|
|
143
|
-
command: context.runtime?.command ?? provider,
|
|
560
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
561
|
+
args: runtime.argsUsed,
|
|
562
|
+
cwd: context.runtime?.cwd,
|
|
144
563
|
exitCode: runtime.code,
|
|
145
564
|
elapsedMs: runtime.elapsedMs,
|
|
146
565
|
timedOut: runtime.timedOut,
|
|
147
566
|
failureType: runtime.failureType,
|
|
567
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
568
|
+
usageSource: "structured",
|
|
148
569
|
attemptCount: runtime.attemptCount,
|
|
149
570
|
attempts: runtime.attempts,
|
|
571
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
572
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
150
573
|
stdoutPreview: toPreview(runtime.stdout),
|
|
151
|
-
stderrPreview: toPreview(runtime.stderr)
|
|
574
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
575
|
+
transcript: runtime.transcript
|
|
152
576
|
},
|
|
153
577
|
nextState: withProviderMetadata(context, provider, runtime.elapsedMs, runtime.code)
|
|
154
578
|
};
|
|
155
579
|
}
|
|
156
580
|
const failedUsage = resolveFailedUsage(runtime, prompt, pricing);
|
|
581
|
+
const failureDetail = resolveRuntimeFailureDetail(runtime);
|
|
157
582
|
return {
|
|
158
583
|
status: "failed",
|
|
159
|
-
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${
|
|
584
|
+
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${failureDetail}`,
|
|
160
585
|
tokenInput: failedUsage.tokenInput,
|
|
161
586
|
tokenOutput: failedUsage.tokenOutput,
|
|
162
587
|
usdCost: failedUsage.usdCost,
|
|
588
|
+
outcome: toOutcome({
|
|
589
|
+
kind: "failed",
|
|
590
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
591
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failureDetail }],
|
|
592
|
+
blockers: [{ code: runtime.failureType ?? "runtime_failed", message: failureDetail, retryable: true }],
|
|
593
|
+
artifacts: [],
|
|
594
|
+
nextSuggestedState: "blocked"
|
|
595
|
+
}),
|
|
163
596
|
trace: {
|
|
164
|
-
command: context.runtime?.command ?? provider,
|
|
597
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
598
|
+
args: runtime.argsUsed,
|
|
599
|
+
cwd: context.runtime?.cwd,
|
|
165
600
|
exitCode: runtime.code,
|
|
166
601
|
elapsedMs: runtime.elapsedMs,
|
|
167
602
|
timedOut: runtime.timedOut,
|
|
168
603
|
failureType: runtime.failureType,
|
|
604
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
169
605
|
attemptCount: runtime.attemptCount,
|
|
170
606
|
attempts: runtime.attempts,
|
|
171
607
|
usageSource: failedUsage.source,
|
|
608
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
609
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
172
610
|
stdoutPreview: toPreview(runtime.stdout),
|
|
173
|
-
stderrPreview: toPreview(runtime.stderr)
|
|
611
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
612
|
+
transcript: runtime.transcript
|
|
174
613
|
},
|
|
175
614
|
nextState: context.state
|
|
176
615
|
};
|
|
177
616
|
}
|
|
178
617
|
|
|
618
|
+
async function runCursorWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
|
|
619
|
+
const prompt = createPrompt(context);
|
|
620
|
+
const cursorLaunch = await resolveCursorLaunchConfig(context.runtime);
|
|
621
|
+
const cwd = context.runtime?.cwd?.trim() || process.cwd();
|
|
622
|
+
const resumeState = resolveCursorResumeState(context.state, cwd);
|
|
623
|
+
const runtimeTimeoutMs = context.runtime?.timeoutMs && context.runtime.timeoutMs > 0 ? context.runtime.timeoutMs : 30_000;
|
|
624
|
+
const buildArgs = (resumeSessionId: string | null) => {
|
|
625
|
+
const baseArgs = [...cursorLaunch.prefixArgs, "-p", "--output-format", "stream-json", "--workspace", cwd];
|
|
626
|
+
if (resumeSessionId) {
|
|
627
|
+
baseArgs.push("--resume", resumeSessionId);
|
|
628
|
+
}
|
|
629
|
+
if (context.runtime?.model?.trim()) {
|
|
630
|
+
baseArgs.push("--model", context.runtime.model.trim());
|
|
631
|
+
}
|
|
632
|
+
if (!hasTrustFlag(context.runtime?.args ?? [])) {
|
|
633
|
+
baseArgs.push("--yolo");
|
|
634
|
+
}
|
|
635
|
+
return [...baseArgs, ...(context.runtime?.args ?? [])];
|
|
636
|
+
};
|
|
637
|
+
const runtime = await executePromptRuntime(
|
|
638
|
+
cursorLaunch.command,
|
|
639
|
+
prompt,
|
|
640
|
+
{
|
|
641
|
+
...context.runtime,
|
|
642
|
+
timeoutMs: runtimeTimeoutMs,
|
|
643
|
+
retryCount: 0,
|
|
644
|
+
args: buildArgs(resumeState.resumeSessionId)
|
|
645
|
+
},
|
|
646
|
+
{ provider: "cursor" }
|
|
647
|
+
);
|
|
648
|
+
const initialSessionId = readRuntimeSessionId(
|
|
649
|
+
runtime,
|
|
650
|
+
resumeState.resumeAttempted ? context.state.cursorSession?.sessionId ?? context.state.sessionId ?? null : null
|
|
651
|
+
);
|
|
652
|
+
if (!runtime.ok && resumeState.resumeSessionId && isUnknownSessionError(runtime.stderr, runtime.stdout)) {
|
|
653
|
+
const retry = await executePromptRuntime(
|
|
654
|
+
cursorLaunch.command,
|
|
655
|
+
prompt,
|
|
656
|
+
{
|
|
657
|
+
...context.runtime,
|
|
658
|
+
timeoutMs: runtimeTimeoutMs,
|
|
659
|
+
retryCount: 0,
|
|
660
|
+
args: buildArgs(null)
|
|
661
|
+
},
|
|
662
|
+
{ provider: "cursor" }
|
|
663
|
+
);
|
|
664
|
+
return toProviderResult(context, "cursor", prompt, retry, {
|
|
665
|
+
inputRate: 0.0000015,
|
|
666
|
+
outputRate: 0.000008
|
|
667
|
+
}, {
|
|
668
|
+
currentSessionId: readRuntimeSessionId(retry, null),
|
|
669
|
+
resumedSessionId: resumeState.resumeSessionId,
|
|
670
|
+
resumeAttempted: true,
|
|
671
|
+
clearedStaleSession: !readRuntimeSessionId(retry, null),
|
|
672
|
+
cwd
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return toProviderResult(context, "cursor", prompt, runtime, {
|
|
676
|
+
inputRate: 0.0000015,
|
|
677
|
+
outputRate: 0.000008
|
|
678
|
+
}, {
|
|
679
|
+
currentSessionId: initialSessionId,
|
|
680
|
+
resumedSessionId: resumeState.resumeSessionId,
|
|
681
|
+
resumeAttempted: resumeState.resumeAttempted,
|
|
682
|
+
resumeSkippedReason: resumeState.resumeSkippedReason,
|
|
683
|
+
clearedStaleSession: resumeState.resumeSkippedReason === "cwd_mismatch" && !initialSessionId,
|
|
684
|
+
cwd
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function runOpenCodeWork(context: HeartbeatContext): Promise<AdapterExecutionResult> {
|
|
689
|
+
const prompt = createPrompt(context);
|
|
690
|
+
const model = context.runtime?.model?.trim();
|
|
691
|
+
if (!model) {
|
|
692
|
+
return {
|
|
693
|
+
status: "failed",
|
|
694
|
+
summary: "opencode runtime requires runtimeModel in provider/model format.",
|
|
695
|
+
tokenInput: 0,
|
|
696
|
+
tokenOutput: 0,
|
|
697
|
+
usdCost: 0,
|
|
698
|
+
outcome: toOutcome({
|
|
699
|
+
kind: "blocked",
|
|
700
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
701
|
+
actions: [{ type: "runtime.validate", status: "error", detail: "Missing runtimeModel." }],
|
|
702
|
+
blockers: [{ code: "model_missing", message: "runtimeModel in provider/model format is required.", retryable: false }],
|
|
703
|
+
artifacts: [],
|
|
704
|
+
nextSuggestedState: "blocked"
|
|
705
|
+
}),
|
|
706
|
+
nextState: context.state
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const resumeSessionId = context.state.sessionId?.trim();
|
|
710
|
+
const baseArgs = ["run", "--format", "json", "--model", model];
|
|
711
|
+
if (resumeSessionId) {
|
|
712
|
+
baseArgs.push("--session", resumeSessionId);
|
|
713
|
+
}
|
|
714
|
+
const runtime = await executePromptRuntime(
|
|
715
|
+
context.runtime?.command ?? "opencode",
|
|
716
|
+
prompt,
|
|
717
|
+
{
|
|
718
|
+
...context.runtime,
|
|
719
|
+
args: [...baseArgs, ...(context.runtime?.args ?? [])]
|
|
720
|
+
},
|
|
721
|
+
{ provider: "opencode" }
|
|
722
|
+
);
|
|
723
|
+
const parsed = parseOpenCodeOutput(runtime.stdout);
|
|
724
|
+
if (!runtime.ok && resumeSessionId && isUnknownSessionError(runtime.stderr, runtime.stdout)) {
|
|
725
|
+
const retry = await executePromptRuntime(
|
|
726
|
+
context.runtime?.command ?? "opencode",
|
|
727
|
+
prompt,
|
|
728
|
+
{
|
|
729
|
+
...context.runtime,
|
|
730
|
+
args: ["run", "--format", "json", "--model", model, ...(context.runtime?.args ?? [])]
|
|
731
|
+
},
|
|
732
|
+
{ provider: "opencode" }
|
|
733
|
+
);
|
|
734
|
+
return toProviderResult(context, "opencode", prompt, retry, {
|
|
735
|
+
inputRate: 0.0000015,
|
|
736
|
+
outputRate: 0.000008
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return toProviderResult(context, "opencode", prompt, runtime, {
|
|
740
|
+
inputRate: 0.0000015,
|
|
741
|
+
outputRate: 0.000008
|
|
742
|
+
}, {
|
|
743
|
+
currentSessionId: parsed.sessionId ?? null
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
179
747
|
function resolveFailedUsage(
|
|
180
748
|
runtime: {
|
|
181
749
|
parsedUsage?: {
|
|
@@ -218,6 +786,482 @@ function resolveFailedUsage(
|
|
|
218
786
|
};
|
|
219
787
|
}
|
|
220
788
|
|
|
789
|
+
function toProviderResult(
|
|
790
|
+
context: HeartbeatContext,
|
|
791
|
+
provider: AgentProviderType,
|
|
792
|
+
prompt: string,
|
|
793
|
+
runtime: {
|
|
794
|
+
ok: boolean;
|
|
795
|
+
code: number | null;
|
|
796
|
+
stdout: string;
|
|
797
|
+
stderr: string;
|
|
798
|
+
timedOut: boolean;
|
|
799
|
+
elapsedMs: number;
|
|
800
|
+
attemptCount: number;
|
|
801
|
+
failureType?: "timeout" | "spawn_error" | "nonzero_exit";
|
|
802
|
+
attempts: Array<{
|
|
803
|
+
attempt: number;
|
|
804
|
+
code: number | null;
|
|
805
|
+
timedOut: boolean;
|
|
806
|
+
elapsedMs: number;
|
|
807
|
+
signal: NodeJS.Signals | null;
|
|
808
|
+
spawnErrorCode?: string;
|
|
809
|
+
forcedKill: boolean;
|
|
810
|
+
}>;
|
|
811
|
+
parsedUsage?: {
|
|
812
|
+
tokenInput?: number;
|
|
813
|
+
tokenOutput?: number;
|
|
814
|
+
usdCost?: number;
|
|
815
|
+
summary?: string;
|
|
816
|
+
};
|
|
817
|
+
structuredOutputSource?: "stdout" | "stderr";
|
|
818
|
+
structuredOutputDiagnostics?: {
|
|
819
|
+
stdoutJsonObjectCount: number;
|
|
820
|
+
stderrJsonObjectCount: number;
|
|
821
|
+
stderrStructuredUsageDetected: boolean;
|
|
822
|
+
stdoutBytes: number;
|
|
823
|
+
stderrBytes: number;
|
|
824
|
+
hasAnyOutput: boolean;
|
|
825
|
+
lastStdoutLine?: string;
|
|
826
|
+
lastStderrLine?: string;
|
|
827
|
+
likelyCause:
|
|
828
|
+
| "no_output_from_runtime"
|
|
829
|
+
| "json_missing"
|
|
830
|
+
| "json_on_stderr_only"
|
|
831
|
+
| "schema_or_shape_mismatch";
|
|
832
|
+
claudeStopReason?: string;
|
|
833
|
+
claudeResultSubtype?: string;
|
|
834
|
+
claudeSessionId?: string;
|
|
835
|
+
claudeContract?: {
|
|
836
|
+
commandOverride: boolean;
|
|
837
|
+
commandLooksClaude: boolean;
|
|
838
|
+
commandWasProviderAlias: boolean;
|
|
839
|
+
hasPromptFlag: boolean;
|
|
840
|
+
hasOutputFormatJson: boolean;
|
|
841
|
+
outputFormat: string | null;
|
|
842
|
+
hasMaxTurnsFlag: boolean;
|
|
843
|
+
hasVerboseFlag: boolean;
|
|
844
|
+
hasDangerouslySkipPermissions: boolean;
|
|
845
|
+
hasJsonSchema: boolean;
|
|
846
|
+
missingRequiredArgs: string[];
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
commandUsed?: string;
|
|
850
|
+
argsUsed?: string[];
|
|
851
|
+
transcript?: Array<{
|
|
852
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
853
|
+
label?: string;
|
|
854
|
+
text?: string;
|
|
855
|
+
payload?: string;
|
|
856
|
+
}>;
|
|
857
|
+
},
|
|
858
|
+
pricing: { inputRate: number; outputRate: number },
|
|
859
|
+
sessionUpdate?: ProviderSessionUpdate
|
|
860
|
+
): AdapterExecutionResult {
|
|
861
|
+
if (runtime.ok) {
|
|
862
|
+
if (!runtime.parsedUsage) {
|
|
863
|
+
const detail = buildMissingStructuredOutputDetail(provider, runtime);
|
|
864
|
+
return {
|
|
865
|
+
status: "failed",
|
|
866
|
+
summary: `${provider} runtime failed: ${detail}`,
|
|
867
|
+
tokenInput: 0,
|
|
868
|
+
tokenOutput: 0,
|
|
869
|
+
usdCost: 0,
|
|
870
|
+
outcome: toOutcome({
|
|
871
|
+
kind: "failed",
|
|
872
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
873
|
+
actions: [{ type: "runtime.execute", status: "error", detail }],
|
|
874
|
+
blockers: [{ code: "missing_structured_output", message: detail, retryable: true }],
|
|
875
|
+
artifacts: [],
|
|
876
|
+
nextSuggestedState: "blocked"
|
|
877
|
+
}),
|
|
878
|
+
trace: {
|
|
879
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
880
|
+
args: runtime.argsUsed,
|
|
881
|
+
cwd: context.runtime?.cwd,
|
|
882
|
+
exitCode: runtime.code,
|
|
883
|
+
elapsedMs: runtime.elapsedMs,
|
|
884
|
+
timedOut: runtime.timedOut,
|
|
885
|
+
failureType: "missing_structured_output",
|
|
886
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
887
|
+
attemptCount: runtime.attemptCount,
|
|
888
|
+
attempts: runtime.attempts,
|
|
889
|
+
session: sessionUpdate,
|
|
890
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
891
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
892
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
893
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
894
|
+
transcript: runtime.transcript
|
|
895
|
+
},
|
|
896
|
+
nextState: applyProviderSessionState(context, provider, sessionUpdate)
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const fallbackOutputTokens = Math.max(Math.round(runtime.stdout.length / 4), 80);
|
|
900
|
+
const fallbackInputTokens = Math.max(Math.round(prompt.length / 4), 120);
|
|
901
|
+
const tokenInput = runtime.parsedUsage?.tokenInput ?? fallbackInputTokens;
|
|
902
|
+
const tokenOutput = runtime.parsedUsage?.tokenOutput ?? fallbackOutputTokens;
|
|
903
|
+
const usdCost =
|
|
904
|
+
runtime.parsedUsage?.usdCost ??
|
|
905
|
+
Number((tokenInput * pricing.inputRate + tokenOutput * pricing.outputRate).toFixed(6));
|
|
906
|
+
const summary = runtime.parsedUsage?.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`;
|
|
907
|
+
return {
|
|
908
|
+
status: "ok",
|
|
909
|
+
summary,
|
|
910
|
+
tokenInput,
|
|
911
|
+
tokenOutput,
|
|
912
|
+
usdCost,
|
|
913
|
+
outcome: toOutcome({
|
|
914
|
+
kind: "completed",
|
|
915
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
916
|
+
actions: [{ type: "runtime.execute", status: "ok", detail: `${provider} runtime completed.` }],
|
|
917
|
+
blockers: [],
|
|
918
|
+
artifacts: [],
|
|
919
|
+
nextSuggestedState: "in_review"
|
|
920
|
+
}),
|
|
921
|
+
trace: {
|
|
922
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
923
|
+
args: runtime.argsUsed,
|
|
924
|
+
cwd: context.runtime?.cwd,
|
|
925
|
+
exitCode: runtime.code,
|
|
926
|
+
elapsedMs: runtime.elapsedMs,
|
|
927
|
+
timedOut: runtime.timedOut,
|
|
928
|
+
failureType: runtime.failureType,
|
|
929
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
930
|
+
usageSource: "structured",
|
|
931
|
+
attemptCount: runtime.attemptCount,
|
|
932
|
+
attempts: runtime.attempts,
|
|
933
|
+
session: sessionUpdate,
|
|
934
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
935
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
936
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
937
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
938
|
+
transcript: runtime.transcript
|
|
939
|
+
},
|
|
940
|
+
nextState: applyProviderSessionState(context, provider, sessionUpdate, runtime.elapsedMs, runtime.code)
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const failedUsage = resolveFailedUsage(runtime, prompt, pricing);
|
|
944
|
+
const failureDetail = resolveRuntimeFailureDetail(runtime);
|
|
945
|
+
return {
|
|
946
|
+
status: "failed",
|
|
947
|
+
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${failureDetail}`,
|
|
948
|
+
tokenInput: failedUsage.tokenInput,
|
|
949
|
+
tokenOutput: failedUsage.tokenOutput,
|
|
950
|
+
usdCost: failedUsage.usdCost,
|
|
951
|
+
outcome: toOutcome({
|
|
952
|
+
kind: "failed",
|
|
953
|
+
issueIdsTouched: issueIdsTouched(context),
|
|
954
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failureDetail }],
|
|
955
|
+
blockers: [{ code: runtime.failureType ?? "runtime_failed", message: failureDetail, retryable: true }],
|
|
956
|
+
artifacts: [],
|
|
957
|
+
nextSuggestedState: "blocked"
|
|
958
|
+
}),
|
|
959
|
+
trace: {
|
|
960
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
961
|
+
args: runtime.argsUsed,
|
|
962
|
+
cwd: context.runtime?.cwd,
|
|
963
|
+
exitCode: runtime.code,
|
|
964
|
+
elapsedMs: runtime.elapsedMs,
|
|
965
|
+
timedOut: runtime.timedOut,
|
|
966
|
+
failureType: runtime.failureType,
|
|
967
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
968
|
+
attemptCount: runtime.attemptCount,
|
|
969
|
+
attempts: runtime.attempts,
|
|
970
|
+
usageSource: failedUsage.source,
|
|
971
|
+
session: sessionUpdate,
|
|
972
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
973
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
974
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
975
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
976
|
+
transcript: runtime.transcript
|
|
977
|
+
},
|
|
978
|
+
nextState: applyProviderSessionState(context, provider, sessionUpdate)
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function resolveRuntimeFailureDetail(runtime: {
|
|
983
|
+
stderr: string;
|
|
984
|
+
stdout: string;
|
|
985
|
+
code: number | null;
|
|
986
|
+
failureType?: "timeout" | "spawn_error" | "nonzero_exit";
|
|
987
|
+
attempts: Array<{ spawnErrorCode?: string }>;
|
|
988
|
+
}) {
|
|
989
|
+
const stderr = runtime.stderr.trim();
|
|
990
|
+
if (stderr.length > 0) {
|
|
991
|
+
return stderr;
|
|
992
|
+
}
|
|
993
|
+
const lastAttempt = runtime.attempts[runtime.attempts.length - 1];
|
|
994
|
+
if (runtime.failureType === "spawn_error") {
|
|
995
|
+
if (lastAttempt?.spawnErrorCode) {
|
|
996
|
+
return `failed to launch runtime command (${lastAttempt.spawnErrorCode}). Verify the CLI is installed and on PATH.`;
|
|
997
|
+
}
|
|
998
|
+
return "failed to launch runtime command. Verify the CLI is installed and on PATH.";
|
|
999
|
+
}
|
|
1000
|
+
if (runtime.failureType === "timeout") {
|
|
1001
|
+
return "timed out before completion. Increase runtimeTimeoutSec for this agent/runtime.";
|
|
1002
|
+
}
|
|
1003
|
+
if (runtime.code !== null) {
|
|
1004
|
+
return `process exited with code ${runtime.code} without stderr output.`;
|
|
1005
|
+
}
|
|
1006
|
+
const stdout = runtime.stdout.trim();
|
|
1007
|
+
if (stdout.length > 0) {
|
|
1008
|
+
return `no stderr output; stdout preview: ${toPreview(stdout, 320)}`;
|
|
1009
|
+
}
|
|
1010
|
+
return "runtime exited without diagnostic output.";
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function parseOpenCodeOutput(stdout: string) {
|
|
1014
|
+
let sessionId: string | null = null;
|
|
1015
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1016
|
+
const trimmed = line.trim();
|
|
1017
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) continue;
|
|
1018
|
+
try {
|
|
1019
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
1020
|
+
const maybeSession =
|
|
1021
|
+
(typeof parsed.sessionID === "string" && parsed.sessionID) ||
|
|
1022
|
+
(typeof parsed.sessionId === "string" && parsed.sessionId) ||
|
|
1023
|
+
null;
|
|
1024
|
+
if (maybeSession) {
|
|
1025
|
+
sessionId = maybeSession;
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
// ignore parser noise
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return { sessionId };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function resolveCursorResumeState(state: HeartbeatContext["state"], cwd: string) {
|
|
1035
|
+
const savedSessionId = state.cursorSession?.sessionId?.trim() || state.sessionId?.trim() || null;
|
|
1036
|
+
const savedCwd = state.cursorSession?.cwd?.trim() || state.cwd?.trim() || null;
|
|
1037
|
+
if (!savedSessionId) {
|
|
1038
|
+
return {
|
|
1039
|
+
resumeSessionId: null,
|
|
1040
|
+
resumeAttempted: false,
|
|
1041
|
+
resumeSkippedReason: null
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (savedCwd && resolve(savedCwd) !== resolve(cwd)) {
|
|
1045
|
+
return {
|
|
1046
|
+
resumeSessionId: null,
|
|
1047
|
+
resumeAttempted: false,
|
|
1048
|
+
resumeSkippedReason: "cwd_mismatch"
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
resumeSessionId: savedSessionId,
|
|
1053
|
+
resumeAttempted: true,
|
|
1054
|
+
resumeSkippedReason: null
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function readRuntimeSessionId(
|
|
1059
|
+
runtime: {
|
|
1060
|
+
structuredOutputDiagnostics?: {
|
|
1061
|
+
claudeSessionId?: string;
|
|
1062
|
+
cursorSessionId?: string;
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
fallback: string | null
|
|
1066
|
+
) {
|
|
1067
|
+
return runtime.structuredOutputDiagnostics?.cursorSessionId?.trim() || runtime.structuredOutputDiagnostics?.claudeSessionId?.trim() || fallback;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function isUnknownSessionError(stderr: string, stdout: string) {
|
|
1071
|
+
const haystack = `${stderr}\n${stdout}`.toLowerCase();
|
|
1072
|
+
return (
|
|
1073
|
+
haystack.includes("unknown session") ||
|
|
1074
|
+
haystack.includes("session not found") ||
|
|
1075
|
+
haystack.includes("could not resume")
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function hasTrustFlag(args: string[]) {
|
|
1080
|
+
return args.includes("--trust") || args.includes("--yolo") || args.includes("-f");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function resolveRuntimeCommand(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
|
|
1084
|
+
if (runtime?.command?.trim()) return runtime.command.trim();
|
|
1085
|
+
if (providerType === "claude_code") return "claude";
|
|
1086
|
+
if (providerType === "codex") return "codex";
|
|
1087
|
+
if (providerType === "cursor") return "cursor";
|
|
1088
|
+
if (providerType === "opencode") return "opencode";
|
|
1089
|
+
return providerType;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function runRuntimeProbe(providerType: AgentProviderType, runtime?: AgentRuntimeConfig) {
|
|
1093
|
+
const prompt = "Respond with hello.";
|
|
1094
|
+
if (providerType === "claude_code" || providerType === "codex") {
|
|
1095
|
+
return executeAgentRuntime(providerType, prompt, {
|
|
1096
|
+
...runtime,
|
|
1097
|
+
retryCount: 0,
|
|
1098
|
+
timeoutMs: runtime?.timeoutMs ? Math.min(runtime.timeoutMs, 45_000) : 45_000
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
if (providerType === "cursor") {
|
|
1102
|
+
const cursorLaunch = await resolveCursorLaunchConfig(runtime);
|
|
1103
|
+
const cwd = runtime?.cwd?.trim() || process.cwd();
|
|
1104
|
+
return executePromptRuntime(
|
|
1105
|
+
cursorLaunch.command,
|
|
1106
|
+
prompt,
|
|
1107
|
+
{
|
|
1108
|
+
...runtime,
|
|
1109
|
+
args: [...cursorLaunch.prefixArgs, "-p", "--output-format", "stream-json", "--workspace", cwd, ...(runtime?.args ?? [])],
|
|
1110
|
+
retryCount: 0,
|
|
1111
|
+
timeoutMs: runtime?.timeoutMs ? Math.min(runtime.timeoutMs, 45_000) : 45_000
|
|
1112
|
+
},
|
|
1113
|
+
{ provider: "cursor" }
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (providerType === "opencode") {
|
|
1117
|
+
const model = runtime?.model?.trim();
|
|
1118
|
+
return executePromptRuntime(
|
|
1119
|
+
resolveRuntimeCommand(providerType, runtime),
|
|
1120
|
+
prompt,
|
|
1121
|
+
{
|
|
1122
|
+
...runtime,
|
|
1123
|
+
args: model
|
|
1124
|
+
? ["run", "--format", "json", "--model", model, ...(runtime?.args ?? [])]
|
|
1125
|
+
: ["run", "--format", "json", ...(runtime?.args ?? [])],
|
|
1126
|
+
retryCount: 0,
|
|
1127
|
+
timeoutMs: runtime?.timeoutMs ? Math.min(runtime.timeoutMs, 45_000) : 45_000
|
|
1128
|
+
},
|
|
1129
|
+
{ provider: "opencode" }
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return executePromptRuntime(resolveRuntimeCommand(providerType, runtime), prompt, {
|
|
1133
|
+
...runtime,
|
|
1134
|
+
retryCount: 0,
|
|
1135
|
+
timeoutMs: runtime?.timeoutMs ? Math.min(runtime.timeoutMs, 45_000) : 45_000
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function discoverCursorModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
|
|
1140
|
+
const cursorLaunch = await resolveCursorLaunchConfig(runtime);
|
|
1141
|
+
const probe = await executePromptRuntime(
|
|
1142
|
+
cursorLaunch.command,
|
|
1143
|
+
"models",
|
|
1144
|
+
{
|
|
1145
|
+
...runtime,
|
|
1146
|
+
args: [...cursorLaunch.prefixArgs, "models"],
|
|
1147
|
+
timeoutMs: 8_000,
|
|
1148
|
+
retryCount: 0
|
|
1149
|
+
},
|
|
1150
|
+
{ provider: "cursor" }
|
|
1151
|
+
);
|
|
1152
|
+
if (!probe.ok && probe.stdout.trim().length === 0 && probe.stderr.trim().length === 0) {
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
return parseModelLines(`${probe.stdout}\n${probe.stderr}`);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function discoverOpenCodeModels(runtime?: AgentRuntimeConfig): Promise<AdapterModelOption[]> {
|
|
1159
|
+
const probe = await executePromptRuntime(
|
|
1160
|
+
resolveRuntimeCommand("opencode", runtime),
|
|
1161
|
+
"models",
|
|
1162
|
+
{
|
|
1163
|
+
...runtime,
|
|
1164
|
+
args: ["models"],
|
|
1165
|
+
timeoutMs: 10_000,
|
|
1166
|
+
retryCount: 0
|
|
1167
|
+
},
|
|
1168
|
+
{ provider: "opencode" }
|
|
1169
|
+
);
|
|
1170
|
+
if (!probe.ok && !probe.stdout.trim()) {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
return parseModelLines(probe.stdout).filter((entry) => entry.id.includes("/"));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function parseModelLines(text: string): AdapterModelOption[] {
|
|
1177
|
+
const out: AdapterModelOption[] = [];
|
|
1178
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1179
|
+
for (const line of lines) {
|
|
1180
|
+
if (line.startsWith("{") || line.startsWith("[")) {
|
|
1181
|
+
try {
|
|
1182
|
+
const parsed = JSON.parse(line) as unknown;
|
|
1183
|
+
if (Array.isArray(parsed)) {
|
|
1184
|
+
for (const entry of parsed) {
|
|
1185
|
+
if (typeof entry === "string") {
|
|
1186
|
+
out.push({ id: entry, label: entry });
|
|
1187
|
+
} else if (entry && typeof entry === "object") {
|
|
1188
|
+
const id = (entry as Record<string, unknown>).id;
|
|
1189
|
+
if (typeof id === "string" && id.trim()) out.push({ id, label: id });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
// ignore
|
|
1195
|
+
}
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const first = line.replace(/^[-*]\s+/, "").split(/\s+/)[0];
|
|
1199
|
+
if (first && /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(first)) {
|
|
1200
|
+
out.push({ id: first, label: first });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return dedupeModels(out);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function dedupeModels(models: AdapterModelOption[]) {
|
|
1207
|
+
const seen = new Set<string>();
|
|
1208
|
+
const deduped: AdapterModelOption[] = [];
|
|
1209
|
+
for (const model of models) {
|
|
1210
|
+
const id = model.id.trim();
|
|
1211
|
+
if (!id || seen.has(id)) continue;
|
|
1212
|
+
seen.add(id);
|
|
1213
|
+
deduped.push({ id, label: model.label.trim() || id });
|
|
1214
|
+
}
|
|
1215
|
+
return deduped;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function resolveCursorLaunchConfig(runtime?: AgentRuntimeConfig): Promise<{ command: string; prefixArgs: string[] }> {
|
|
1219
|
+
const configuredCommand = runtime?.command?.trim();
|
|
1220
|
+
if (configuredCommand) {
|
|
1221
|
+
const commandToken = configuredCommand.split(/[\\/]/).pop()?.toLowerCase() ?? configuredCommand.toLowerCase();
|
|
1222
|
+
const configuredArgs = runtime?.args ?? [];
|
|
1223
|
+
const hasAgentSubcommand = configuredArgs[0]?.toLowerCase() === "agent";
|
|
1224
|
+
if (commandToken === "cursor" || commandToken === "cursor.exe") {
|
|
1225
|
+
return { command: configuredCommand, prefixArgs: hasAgentSubcommand ? [] : ["agent"] };
|
|
1226
|
+
}
|
|
1227
|
+
if (commandToken !== "agent" && commandToken !== "agent.exe") {
|
|
1228
|
+
return { command: configuredCommand, prefixArgs: [] };
|
|
1229
|
+
}
|
|
1230
|
+
const configuredHealth = await checkRuntimeCommandHealth(configuredCommand, {
|
|
1231
|
+
cwd: runtime?.cwd?.trim() || process.cwd(),
|
|
1232
|
+
timeoutMs: 1_500
|
|
1233
|
+
});
|
|
1234
|
+
if (configuredHealth.available) {
|
|
1235
|
+
return { command: configuredCommand, prefixArgs: [] };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const candidates: Array<{ command: string; prefixArgs: string[] }> = [
|
|
1239
|
+
{ command: "agent", prefixArgs: [] },
|
|
1240
|
+
{ command: "cursor", prefixArgs: ["agent"] },
|
|
1241
|
+
{ command: join("/Applications", "Cursor.app", "Contents", "Resources", "app", "bin", "cursor"), prefixArgs: ["agent"] },
|
|
1242
|
+
{
|
|
1243
|
+
command: join(homedir(), "Applications", "Cursor.app", "Contents", "Resources", "app", "bin", "cursor"),
|
|
1244
|
+
prefixArgs: ["agent"]
|
|
1245
|
+
}
|
|
1246
|
+
];
|
|
1247
|
+
for (const candidate of candidates) {
|
|
1248
|
+
const health = await checkRuntimeCommandHealth(candidate.command, {
|
|
1249
|
+
cwd: runtime?.cwd?.trim() || process.cwd(),
|
|
1250
|
+
timeoutMs: 1_500
|
|
1251
|
+
});
|
|
1252
|
+
if (health.available) {
|
|
1253
|
+
return candidate;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return { command: "agent", prefixArgs: [] };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function toEnvironmentStatus(checks: AdapterEnvironmentCheck[]): "pass" | "warn" | "fail" {
|
|
1260
|
+
if (checks.some((check) => check.level === "error")) return "fail";
|
|
1261
|
+
if (checks.some((check) => check.level === "warn")) return "warn";
|
|
1262
|
+
return "pass";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
221
1265
|
function createPrompt(context: HeartbeatContext) {
|
|
222
1266
|
const bootstrapPrompt = context.runtime?.bootstrapPrompt?.trim();
|
|
223
1267
|
const companyGoals = context.goalContext?.companyGoals.length
|
|
@@ -249,8 +1293,8 @@ function createPrompt(context: HeartbeatContext) {
|
|
|
249
1293
|
|
|
250
1294
|
const executionDirectives = [
|
|
251
1295
|
"Execution directives:",
|
|
252
|
-
"- You are running inside a
|
|
253
|
-
"- Use
|
|
1296
|
+
"- You are running inside a BopoDev heartbeat for local repository work.",
|
|
1297
|
+
"- Use BopoDev-specific injected skills only (bopodev-control-plane, bopodev-create-agent, para-memory-files) when relevant.",
|
|
254
1298
|
"- Ignore unrelated third-party control-plane skills even if they exist in the runtime environment.",
|
|
255
1299
|
"- Prefer completing assigned issue work in this repository over non-essential coordination tasks.",
|
|
256
1300
|
"- Keep command usage minimal and task-focused; avoid broad repository scans unless strictly required for the assigned issue.",
|
|
@@ -258,6 +1302,8 @@ function createPrompt(context: HeartbeatContext) {
|
|
|
258
1302
|
"- Prefer POSIX/zsh-compatible shell snippets, direct `curl` headers, `jq`, and temp JSON files/heredocs.",
|
|
259
1303
|
"- If control-plane API connectivity fails, report the exact failing command/error once and stop retry loops for the same endpoint.",
|
|
260
1304
|
"- If any command fails, avoid further exploratory commands and still return the required final JSON summary.",
|
|
1305
|
+
"- Do not stop after planning. You must execute concrete steps for assigned issues in this run (file edits, API calls, or other verifiable actions).",
|
|
1306
|
+
"- If you cannot complete concrete execution, set summary to include the blocker explicitly instead of claiming success.",
|
|
261
1307
|
"- Your final output must be only the JSON object below, with no prose before or after it.",
|
|
262
1308
|
"- Do not invent token or cost values; the runtime records usage separately."
|
|
263
1309
|
].join("\n");
|
|
@@ -305,9 +1351,138 @@ function withProviderMetadata(
|
|
|
305
1351
|
};
|
|
306
1352
|
}
|
|
307
1353
|
|
|
1354
|
+
function applyProviderSessionState(
|
|
1355
|
+
context: HeartbeatContext,
|
|
1356
|
+
provider: AgentProviderType,
|
|
1357
|
+
sessionUpdate?: ProviderSessionUpdate,
|
|
1358
|
+
lastRuntimeMs?: number,
|
|
1359
|
+
lastExitCode?: number | null
|
|
1360
|
+
) {
|
|
1361
|
+
const nextState = withProviderMetadata(context, provider, lastRuntimeMs, lastExitCode) as HeartbeatContext["state"] &
|
|
1362
|
+
Record<string, unknown>;
|
|
1363
|
+
if (provider === "cursor") {
|
|
1364
|
+
delete nextState.sessionId;
|
|
1365
|
+
delete nextState.cwd;
|
|
1366
|
+
delete nextState.cursorSession;
|
|
1367
|
+
const nextSessionId = sessionUpdate?.currentSessionId?.trim();
|
|
1368
|
+
const nextCwd = sessionUpdate?.cwd?.trim() || context.runtime?.cwd?.trim() || undefined;
|
|
1369
|
+
if (nextSessionId) {
|
|
1370
|
+
nextState.sessionId = nextSessionId;
|
|
1371
|
+
nextState.cwd = nextCwd;
|
|
1372
|
+
nextState.cursorSession = {
|
|
1373
|
+
sessionId: nextSessionId,
|
|
1374
|
+
...(nextCwd ? { cwd: nextCwd } : {})
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
return nextState;
|
|
1378
|
+
}
|
|
1379
|
+
if (provider === "opencode") {
|
|
1380
|
+
if (sessionUpdate?.currentSessionId?.trim()) {
|
|
1381
|
+
nextState.sessionId = sessionUpdate.currentSessionId.trim();
|
|
1382
|
+
}
|
|
1383
|
+
return nextState;
|
|
1384
|
+
}
|
|
1385
|
+
return nextState;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
308
1388
|
function toPreview(value: string, max = 1600) {
|
|
309
1389
|
if (value.length <= max) {
|
|
310
1390
|
return value;
|
|
311
1391
|
}
|
|
312
1392
|
return `${value.slice(0, max)}\n...[truncated]`;
|
|
313
1393
|
}
|
|
1394
|
+
|
|
1395
|
+
function buildMissingStructuredOutputDetail(
|
|
1396
|
+
provider: string,
|
|
1397
|
+
runtime: {
|
|
1398
|
+
structuredOutputSource?: "stdout" | "stderr";
|
|
1399
|
+
structuredOutputDiagnostics?: {
|
|
1400
|
+
stdoutJsonObjectCount: number;
|
|
1401
|
+
stderrJsonObjectCount: number;
|
|
1402
|
+
stderrStructuredUsageDetected: boolean;
|
|
1403
|
+
stdoutBytes: number;
|
|
1404
|
+
stderrBytes: number;
|
|
1405
|
+
hasAnyOutput: boolean;
|
|
1406
|
+
lastStdoutLine?: string;
|
|
1407
|
+
lastStderrLine?: string;
|
|
1408
|
+
likelyCause:
|
|
1409
|
+
| "no_output_from_runtime"
|
|
1410
|
+
| "json_missing"
|
|
1411
|
+
| "json_on_stderr_only"
|
|
1412
|
+
| "schema_or_shape_mismatch";
|
|
1413
|
+
claudeStopReason?: string;
|
|
1414
|
+
claudeResultSubtype?: string;
|
|
1415
|
+
claudeSessionId?: string;
|
|
1416
|
+
claudeContract?: {
|
|
1417
|
+
commandOverride: boolean;
|
|
1418
|
+
commandLooksClaude: boolean;
|
|
1419
|
+
commandWasProviderAlias: boolean;
|
|
1420
|
+
hasPromptFlag: boolean;
|
|
1421
|
+
hasOutputFormatJson: boolean;
|
|
1422
|
+
outputFormat: string | null;
|
|
1423
|
+
hasMaxTurnsFlag: boolean;
|
|
1424
|
+
hasVerboseFlag: boolean;
|
|
1425
|
+
hasDangerouslySkipPermissions: boolean;
|
|
1426
|
+
hasJsonSchema: boolean;
|
|
1427
|
+
missingRequiredArgs: string[];
|
|
1428
|
+
};
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
) {
|
|
1432
|
+
const hints: string[] = [];
|
|
1433
|
+
const claudeContract = runtime.structuredOutputDiagnostics?.claudeContract;
|
|
1434
|
+
if (provider === "claude_code" && claudeContract) {
|
|
1435
|
+
if (claudeContract.commandWasProviderAlias) {
|
|
1436
|
+
hints.push("runtimeCommand used provider alias 'claude_code' and was normalized to 'claude'");
|
|
1437
|
+
}
|
|
1438
|
+
if (claudeContract.commandOverride && !claudeContract.commandLooksClaude) {
|
|
1439
|
+
hints.push("runtimeCommand override does not look like Claude CLI");
|
|
1440
|
+
}
|
|
1441
|
+
if (claudeContract.missingRequiredArgs.length > 0) {
|
|
1442
|
+
hints.push(`missing Claude structured-output args: ${claudeContract.missingRequiredArgs.join(", ")}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (runtime.structuredOutputSource === "stderr") {
|
|
1446
|
+
hints.push("structured JSON was detected on stderr");
|
|
1447
|
+
}
|
|
1448
|
+
const stdoutJsonObjectCount = runtime.structuredOutputDiagnostics?.stdoutJsonObjectCount ?? 0;
|
|
1449
|
+
const stderrJsonObjectCount = runtime.structuredOutputDiagnostics?.stderrJsonObjectCount ?? 0;
|
|
1450
|
+
const stdoutBytes = runtime.structuredOutputDiagnostics?.stdoutBytes ?? 0;
|
|
1451
|
+
const stderrBytes = runtime.structuredOutputDiagnostics?.stderrBytes ?? 0;
|
|
1452
|
+
const likelyCause = runtime.structuredOutputDiagnostics?.likelyCause ?? "json_missing";
|
|
1453
|
+
const hasAnyOutput = runtime.structuredOutputDiagnostics?.hasAnyOutput ?? false;
|
|
1454
|
+
const lastStdoutLine = runtime.structuredOutputDiagnostics?.lastStdoutLine;
|
|
1455
|
+
const lastStderrLine = runtime.structuredOutputDiagnostics?.lastStderrLine;
|
|
1456
|
+
const base =
|
|
1457
|
+
provider === "claude_code"
|
|
1458
|
+
? "runtime completed without structured heartbeat output. Expected Claude stream-json events with a final result payload."
|
|
1459
|
+
: provider === "cursor"
|
|
1460
|
+
? "runtime completed without structured heartbeat output. Expected Cursor stream-json events with assistant/result payloads."
|
|
1461
|
+
: "runtime completed without structured heartbeat JSON output. Ensure final output includes a single-line JSON object with summary/tokenInput/tokenOutput/usdCost.";
|
|
1462
|
+
const diagnostics = [
|
|
1463
|
+
`likelyCause=${likelyCause}`,
|
|
1464
|
+
`hasAnyOutput=${hasAnyOutput}`,
|
|
1465
|
+
`stdoutBytes=${stdoutBytes}`,
|
|
1466
|
+
`stderrBytes=${stderrBytes}`,
|
|
1467
|
+
`stdoutJsonObjects=${stdoutJsonObjectCount}`,
|
|
1468
|
+
`stderrJsonObjects=${stderrJsonObjectCount}`
|
|
1469
|
+
];
|
|
1470
|
+
if (lastStdoutLine) {
|
|
1471
|
+
diagnostics.push(`lastStdoutLine=${JSON.stringify(lastStdoutLine).slice(0, 180)}`);
|
|
1472
|
+
}
|
|
1473
|
+
if (lastStderrLine) {
|
|
1474
|
+
diagnostics.push(`lastStderrLine=${JSON.stringify(lastStderrLine).slice(0, 180)}`);
|
|
1475
|
+
}
|
|
1476
|
+
return hints.length > 0 ? `${base} Diagnostics: ${[...hints, ...diagnostics].join("; ")}.` : `${base} Diagnostics: ${diagnostics.join("; ")}.`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function isClaudeRunIncomplete(runtime: {
|
|
1480
|
+
structuredOutputDiagnostics?: {
|
|
1481
|
+
claudeStopReason?: string;
|
|
1482
|
+
claudeResultSubtype?: string;
|
|
1483
|
+
};
|
|
1484
|
+
}) {
|
|
1485
|
+
const stopReason = runtime.structuredOutputDiagnostics?.claudeStopReason;
|
|
1486
|
+
const subtype = runtime.structuredOutputDiagnostics?.claudeResultSubtype;
|
|
1487
|
+
return stopReason === "max_turns" || subtype === "error_max_turns";
|
|
1488
|
+
}
|