bopodev-agent-sdk 0.1.25 → 0.1.27
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/README.md +27 -0
- package/dist/adapters/codex/src/server/quota.d.ts +2 -0
- package/dist/agent-sdk/src/adapters.d.ts +12 -1
- package/dist/agent-sdk/src/provider-failures/anthropic-api.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/claude-code.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/codex.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/common.d.ts +7 -0
- package/dist/agent-sdk/src/provider-failures/cursor.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/gemini-cli.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/http.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/index.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/openai-api.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/opencode.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/shell.d.ts +5 -0
- package/dist/agent-sdk/src/provider-failures/types.d.ts +20 -0
- package/dist/agent-sdk/src/quota.d.ts +4 -0
- package/dist/agent-sdk/src/runtime-core.d.ts +1 -1
- package/dist/agent-sdk/src/runtime-http.d.ts +4 -2
- package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -1
- package/dist/agent-sdk/src/runtime.d.ts +13 -0
- package/dist/agent-sdk/src/types.d.ts +19 -1
- package/dist/contracts/src/index.d.ts +426 -11
- package/package.json +2 -2
- package/src/adapters.ts +477 -58
- package/src/provider-failures/anthropic-api.ts +20 -0
- package/src/provider-failures/claude-code.ts +20 -0
- package/src/provider-failures/codex.ts +23 -0
- package/src/provider-failures/common.ts +86 -0
- package/src/provider-failures/cursor.ts +20 -0
- package/src/provider-failures/gemini-cli.ts +20 -0
- package/src/provider-failures/http.ts +12 -0
- package/src/provider-failures/index.ts +54 -0
- package/src/provider-failures/openai-api.ts +20 -0
- package/src/provider-failures/opencode.ts +20 -0
- package/src/provider-failures/shell.ts +12 -0
- package/src/provider-failures/types.ts +28 -0
- package/src/runtime-core.ts +7 -1
- package/src/runtime-http.ts +51 -6
- package/src/runtime-parsers.ts +1 -0
- package/src/runtime.ts +299 -1
- package/src/types.ts +20 -1
package/src/adapters.ts
CHANGED
|
@@ -8,10 +8,16 @@ import type {
|
|
|
8
8
|
AgentAdapter,
|
|
9
9
|
AgentProviderType,
|
|
10
10
|
AgentRuntimeConfig,
|
|
11
|
-
HeartbeatContext
|
|
11
|
+
HeartbeatContext,
|
|
12
|
+
HeartbeatPromptMode
|
|
12
13
|
} from "./types";
|
|
13
14
|
import { ExecutionOutcomeSchema, type ExecutionOutcome } from "bopodev-contracts";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
checkRuntimeCommandHealth,
|
|
17
|
+
containsUsageLimitHardStopFailure,
|
|
18
|
+
executeAgentRuntime,
|
|
19
|
+
executePromptRuntime
|
|
20
|
+
} from "./runtime-core";
|
|
15
21
|
import {
|
|
16
22
|
parseClaudeStreamOutput,
|
|
17
23
|
parseCursorStreamOutput,
|
|
@@ -24,6 +30,10 @@ import {
|
|
|
24
30
|
resolveDirectApiCredentials,
|
|
25
31
|
type DirectApiProvider
|
|
26
32
|
} from "./runtime-http";
|
|
33
|
+
import {
|
|
34
|
+
classifyProviderFailure as classifyProviderFailureByProvider,
|
|
35
|
+
normalizeProviderFailureDetail as normalizeProviderFailureDetailByProvider
|
|
36
|
+
} from "./provider-failures";
|
|
27
37
|
import { homedir } from "node:os";
|
|
28
38
|
import { basename, join, resolve } from "node:path";
|
|
29
39
|
|
|
@@ -42,8 +52,39 @@ function toOutcome(outcome: ExecutionOutcome): ExecutionOutcome {
|
|
|
42
52
|
return ExecutionOutcomeSchema.parse(outcome);
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
function
|
|
46
|
-
return
|
|
55
|
+
function isProviderUsageLimitedRuntimeFailure(runtime: { stdout: string; stderr: string }, detail?: string) {
|
|
56
|
+
return containsUsageLimitHardStopFailure(`${detail ?? ""}\n${runtime.stderr}\n${runtime.stdout}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildProviderUsageLimitedDispositionHint(
|
|
60
|
+
provider: string,
|
|
61
|
+
detail: string
|
|
62
|
+
): NonNullable<AdapterExecutionResult["dispositionHint"]> {
|
|
63
|
+
const normalizedDetail = detail.replace(/\s+/g, " ").trim();
|
|
64
|
+
const message = normalizedDetail ? `${provider} usage limit reached: ${normalizedDetail}` : `${provider} usage limit reached.`;
|
|
65
|
+
return {
|
|
66
|
+
kind: "provider_usage_limited",
|
|
67
|
+
persistStatus: "skipped",
|
|
68
|
+
pauseAgent: true,
|
|
69
|
+
notifyBoard: true,
|
|
70
|
+
message
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeProviderFailureDetail(provider: AgentProviderType, detail: string) {
|
|
75
|
+
return normalizeProviderFailureDetailByProvider(provider, detail);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function classifyProviderFailure(
|
|
79
|
+
provider: AgentProviderType,
|
|
80
|
+
input: {
|
|
81
|
+
detail: string;
|
|
82
|
+
stderr?: string;
|
|
83
|
+
stdout?: string;
|
|
84
|
+
failureType?: string | null;
|
|
85
|
+
}
|
|
86
|
+
): ReturnType<typeof classifyProviderFailureByProvider> {
|
|
87
|
+
return classifyProviderFailureByProvider(provider, input);
|
|
47
88
|
}
|
|
48
89
|
|
|
49
90
|
type RuntimeParsedUsage = {
|
|
@@ -130,6 +171,53 @@ function usageTokenInputTotal(usage: RuntimeParsedUsage | undefined) {
|
|
|
130
171
|
return Math.max(0, usage.tokenInput ?? 0);
|
|
131
172
|
}
|
|
132
173
|
|
|
174
|
+
function resolveFinalRunOutputContractDetail(input: {
|
|
175
|
+
provider: string;
|
|
176
|
+
runtime: {
|
|
177
|
+
structuredOutputDiagnostics?: {
|
|
178
|
+
finalRunOutputError?: string;
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
}) {
|
|
182
|
+
const detail = input.runtime.structuredOutputDiagnostics?.finalRunOutputError?.trim();
|
|
183
|
+
return detail || `${input.provider} runtime did not return a valid final JSON object.`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createContractInvalidResult(input: {
|
|
187
|
+
context: HeartbeatContext;
|
|
188
|
+
provider: AgentProviderType;
|
|
189
|
+
summary: string;
|
|
190
|
+
tokenInput: number;
|
|
191
|
+
tokenOutput: number;
|
|
192
|
+
usdCost: number;
|
|
193
|
+
usage?: AdapterNormalizedUsage;
|
|
194
|
+
pricingProviderType?: string | null;
|
|
195
|
+
pricingModelId?: string | null;
|
|
196
|
+
trace: NonNullable<AdapterExecutionResult["trace"]>;
|
|
197
|
+
nextState: HeartbeatContext["state"];
|
|
198
|
+
}): AdapterExecutionResult {
|
|
199
|
+
return {
|
|
200
|
+
status: "failed",
|
|
201
|
+
summary: input.summary,
|
|
202
|
+
tokenInput: input.tokenInput,
|
|
203
|
+
tokenOutput: input.tokenOutput,
|
|
204
|
+
usdCost: input.usdCost,
|
|
205
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
206
|
+
pricingProviderType: input.pricingProviderType,
|
|
207
|
+
pricingModelId: input.pricingModelId,
|
|
208
|
+
outcome: toOutcome({
|
|
209
|
+
kind: "failed",
|
|
210
|
+
issueIdsTouched: issueIdsTouched(input.context),
|
|
211
|
+
actions: [{ type: "runtime.contract", status: "error", detail: input.summary }],
|
|
212
|
+
blockers: [{ code: "contract_invalid", message: input.summary, retryable: true }],
|
|
213
|
+
artifacts: [],
|
|
214
|
+
nextSuggestedState: "blocked"
|
|
215
|
+
}),
|
|
216
|
+
trace: input.trace,
|
|
217
|
+
nextState: input.nextState
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
133
221
|
function hasUsageMetrics(usage: RuntimeParsedUsage | undefined) {
|
|
134
222
|
if (!usage) {
|
|
135
223
|
return false;
|
|
@@ -372,12 +460,46 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
372
460
|
nextState: context.state
|
|
373
461
|
};
|
|
374
462
|
}
|
|
463
|
+
if (!runtime.finalRunOutput) {
|
|
464
|
+
const usage = toNormalizedUsage(runtime.parsedUsage);
|
|
465
|
+
const detail = resolveFinalRunOutputContractDetail({ provider: this.providerType, runtime });
|
|
466
|
+
return createContractInvalidResult({
|
|
467
|
+
context,
|
|
468
|
+
provider: this.providerType,
|
|
469
|
+
summary: `${this.providerType} runtime failed contract validation: ${detail}`,
|
|
470
|
+
tokenInput: usageTokenInputTotal(runtime.parsedUsage),
|
|
471
|
+
tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
|
|
472
|
+
usdCost: runtime.parsedUsage?.usdCost ?? 0,
|
|
473
|
+
...(usage ? { usage } : {}),
|
|
474
|
+
pricingProviderType: resolveCanonicalPricingProviderKey(this.providerType),
|
|
475
|
+
pricingModelId: context.runtime?.model?.trim() || null,
|
|
476
|
+
trace: {
|
|
477
|
+
command: runtime.commandUsed ?? context.runtime.command,
|
|
478
|
+
args: runtime.argsUsed,
|
|
479
|
+
cwd: context.runtime?.cwd,
|
|
480
|
+
exitCode: runtime.code,
|
|
481
|
+
elapsedMs: runtime.elapsedMs,
|
|
482
|
+
timedOut: runtime.timedOut,
|
|
483
|
+
failureType: "contract_invalid",
|
|
484
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
485
|
+
attemptCount: runtime.attemptCount,
|
|
486
|
+
attempts: runtime.attempts,
|
|
487
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
488
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
489
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
490
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
491
|
+
transcript: runtime.transcript
|
|
492
|
+
},
|
|
493
|
+
nextState: context.state
|
|
494
|
+
});
|
|
495
|
+
}
|
|
375
496
|
return {
|
|
376
497
|
status: "ok",
|
|
377
498
|
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime finished in ${runtime.elapsedMs}ms.`,
|
|
378
499
|
tokenInput: runtime.parsedUsage?.tokenInput ?? 0,
|
|
379
500
|
tokenOutput: runtime.parsedUsage?.tokenOutput ?? 0,
|
|
380
501
|
usdCost: runtime.parsedUsage?.usdCost ?? 0,
|
|
502
|
+
finalRunOutput: runtime.finalRunOutput,
|
|
381
503
|
outcome: toOutcome({
|
|
382
504
|
kind: "completed",
|
|
383
505
|
issueIdsTouched: issueIdsTouched(context),
|
|
@@ -409,23 +531,27 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
409
531
|
}
|
|
410
532
|
|
|
411
533
|
const failedUsage = resolveFailedUsage(runtime);
|
|
412
|
-
const
|
|
413
|
-
|
|
534
|
+
const failure = classifyProviderFailure(this.providerType, {
|
|
535
|
+
detail: resolveRuntimeFailureDetail(runtime, this.providerType),
|
|
536
|
+
stdout: runtime.stdout,
|
|
537
|
+
stderr: runtime.stderr,
|
|
538
|
+
failureType: runtime.failureType
|
|
539
|
+
});
|
|
414
540
|
return {
|
|
415
541
|
status: "failed",
|
|
416
|
-
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime failed: ${
|
|
542
|
+
summary: runtime.parsedUsage?.summary ?? `${this.providerType} runtime failed: ${failure.detail}`,
|
|
417
543
|
tokenInput: failedUsage.tokenInput,
|
|
418
544
|
tokenOutput: failedUsage.tokenOutput,
|
|
419
545
|
usdCost: failedUsage.usdCost,
|
|
420
546
|
outcome: toOutcome({
|
|
421
547
|
kind: "failed",
|
|
422
548
|
issueIdsTouched: issueIdsTouched(context),
|
|
423
|
-
actions: [{ type: "runtime.execute", status: "error", detail:
|
|
549
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
|
|
424
550
|
blockers: [
|
|
425
551
|
{
|
|
426
|
-
code:
|
|
427
|
-
message:
|
|
428
|
-
retryable:
|
|
552
|
+
code: failure.blockerCode,
|
|
553
|
+
message: failure.detail,
|
|
554
|
+
retryable: failure.retryable
|
|
429
555
|
}
|
|
430
556
|
],
|
|
431
557
|
artifacts: [],
|
|
@@ -449,6 +575,9 @@ export class GenericHeartbeatAdapter implements AgentAdapter {
|
|
|
449
575
|
stderrPreview: toPreview(runtime.stderr),
|
|
450
576
|
transcript: runtime.transcript
|
|
451
577
|
},
|
|
578
|
+
...(failure.providerUsageLimited
|
|
579
|
+
? { dispositionHint: buildProviderUsageLimitedDispositionHint(this.providerType, failure.detail) }
|
|
580
|
+
: {}),
|
|
452
581
|
nextState: context.state
|
|
453
582
|
};
|
|
454
583
|
}
|
|
@@ -838,12 +967,51 @@ export async function runDirectApiWork(
|
|
|
838
967
|
const prompt = createPrompt(context);
|
|
839
968
|
const runtime = await executeDirectApiRuntime(provider, prompt, context.runtime);
|
|
840
969
|
if (runtime.ok) {
|
|
970
|
+
if (!runtime.finalRunOutput) {
|
|
971
|
+
return createContractInvalidResult({
|
|
972
|
+
context,
|
|
973
|
+
provider,
|
|
974
|
+
summary: `${provider} runtime failed contract validation: ${runtime.summary ?? "Missing final JSON object."}`,
|
|
975
|
+
tokenInput: runtime.tokenInput ?? 0,
|
|
976
|
+
tokenOutput: runtime.tokenOutput ?? 0,
|
|
977
|
+
usdCost: runtime.usdCost ?? 0,
|
|
978
|
+
usage: {
|
|
979
|
+
inputTokens: runtime.tokenInput ?? 0,
|
|
980
|
+
cachedInputTokens: 0,
|
|
981
|
+
outputTokens: runtime.tokenOutput ?? 0,
|
|
982
|
+
...(runtime.usdCost !== undefined ? { costUsd: runtime.usdCost } : {}),
|
|
983
|
+
...(runtime.summary ? { summary: runtime.summary } : {})
|
|
984
|
+
},
|
|
985
|
+
pricingProviderType: runtime.provider,
|
|
986
|
+
pricingModelId: runtime.model,
|
|
987
|
+
trace: {
|
|
988
|
+
command: runtime.endpoint,
|
|
989
|
+
cwd: context.runtime?.cwd,
|
|
990
|
+
exitCode: runtime.statusCode,
|
|
991
|
+
elapsedMs: runtime.elapsedMs,
|
|
992
|
+
failureType: "contract_invalid",
|
|
993
|
+
usageSource: "structured",
|
|
994
|
+
attemptCount: runtime.attemptCount,
|
|
995
|
+
attempts: runtime.attempts.map((attempt) => ({
|
|
996
|
+
attempt: attempt.attempt,
|
|
997
|
+
code: attempt.statusCode || null,
|
|
998
|
+
timedOut: attempt.failureType === "timeout",
|
|
999
|
+
elapsedMs: attempt.elapsedMs,
|
|
1000
|
+
signal: null,
|
|
1001
|
+
forcedKill: false
|
|
1002
|
+
})),
|
|
1003
|
+
stdoutPreview: runtime.responsePreview
|
|
1004
|
+
},
|
|
1005
|
+
nextState: context.state
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
841
1008
|
return {
|
|
842
1009
|
status: "ok",
|
|
843
1010
|
summary: runtime.summary ?? `${provider} runtime finished in ${runtime.elapsedMs}ms.`,
|
|
844
1011
|
tokenInput: runtime.tokenInput ?? 0,
|
|
845
1012
|
tokenOutput: runtime.tokenOutput ?? 0,
|
|
846
1013
|
usdCost: runtime.usdCost ?? 0,
|
|
1014
|
+
finalRunOutput: runtime.finalRunOutput,
|
|
847
1015
|
pricingProviderType: runtime.provider,
|
|
848
1016
|
pricingModelId: runtime.model,
|
|
849
1017
|
outcome: toOutcome({
|
|
@@ -875,12 +1043,15 @@ export async function runDirectApiWork(
|
|
|
875
1043
|
nextState: withProviderMetadata(context, provider, runtime.elapsedMs, runtime.statusCode)
|
|
876
1044
|
};
|
|
877
1045
|
}
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
1046
|
+
const failure = classifyProviderFailure(provider, {
|
|
1047
|
+
detail: runtime.error ?? "direct API request failed",
|
|
1048
|
+
stderr: runtime.error,
|
|
1049
|
+
stdout: runtime.responsePreview ?? "",
|
|
1050
|
+
failureType: runtime.failureType
|
|
1051
|
+
});
|
|
881
1052
|
return {
|
|
882
1053
|
status: "failed",
|
|
883
|
-
summary: `${provider} runtime failed: ${
|
|
1054
|
+
summary: `${provider} runtime failed: ${failure.detail}`,
|
|
884
1055
|
tokenInput: 0,
|
|
885
1056
|
tokenOutput: 0,
|
|
886
1057
|
usdCost: 0,
|
|
@@ -889,11 +1060,11 @@ export async function runDirectApiWork(
|
|
|
889
1060
|
outcome: toOutcome({
|
|
890
1061
|
kind: "failed",
|
|
891
1062
|
issueIdsTouched: issueIdsTouched(context),
|
|
892
|
-
actions: [{ type: "runtime.execute", status: "error", detail:
|
|
1063
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
|
|
893
1064
|
blockers: [{
|
|
894
|
-
code:
|
|
895
|
-
message:
|
|
896
|
-
retryable:
|
|
1065
|
+
code: failure.blockerCode,
|
|
1066
|
+
message: failure.detail,
|
|
1067
|
+
retryable: failure.retryable
|
|
897
1068
|
}],
|
|
898
1069
|
artifacts: [],
|
|
899
1070
|
nextSuggestedState: "blocked"
|
|
@@ -917,6 +1088,9 @@ export async function runDirectApiWork(
|
|
|
917
1088
|
stderrPreview: runtime.error,
|
|
918
1089
|
stdoutPreview: runtime.responsePreview
|
|
919
1090
|
},
|
|
1091
|
+
...(failure.providerUsageLimited
|
|
1092
|
+
? { dispositionHint: buildProviderUsageLimitedDispositionHint(provider, failure.detail) }
|
|
1093
|
+
: {}),
|
|
920
1094
|
nextState: context.state
|
|
921
1095
|
};
|
|
922
1096
|
}
|
|
@@ -1042,6 +1216,40 @@ export async function runProviderWork(
|
|
|
1042
1216
|
nextState: context.state
|
|
1043
1217
|
};
|
|
1044
1218
|
}
|
|
1219
|
+
if (!runtime.finalRunOutput) {
|
|
1220
|
+
const usage = toNormalizedUsage(runtime.parsedUsage);
|
|
1221
|
+
const detail = resolveFinalRunOutputContractDetail({ provider, runtime });
|
|
1222
|
+
return createContractInvalidResult({
|
|
1223
|
+
context,
|
|
1224
|
+
provider,
|
|
1225
|
+
summary: `${provider} runtime failed contract validation: ${detail}`,
|
|
1226
|
+
tokenInput: usageTokenInputTotal(runtime.parsedUsage),
|
|
1227
|
+
tokenOutput: runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0,
|
|
1228
|
+
usdCost: runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0,
|
|
1229
|
+
...(usage ? { usage } : {}),
|
|
1230
|
+
pricingProviderType,
|
|
1231
|
+
pricingModelId,
|
|
1232
|
+
trace: {
|
|
1233
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
1234
|
+
args: runtime.argsUsed,
|
|
1235
|
+
cwd: context.runtime?.cwd,
|
|
1236
|
+
exitCode: runtime.code,
|
|
1237
|
+
elapsedMs: runtime.elapsedMs,
|
|
1238
|
+
timedOut: runtime.timedOut,
|
|
1239
|
+
failureType: "contract_invalid",
|
|
1240
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
1241
|
+
usageSource: "structured",
|
|
1242
|
+
attemptCount: runtime.attemptCount,
|
|
1243
|
+
attempts: runtime.attempts,
|
|
1244
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
1245
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
1246
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
1247
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
1248
|
+
transcript: runtime.transcript
|
|
1249
|
+
},
|
|
1250
|
+
nextState: context.state
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1045
1253
|
if (provider === "claude_code" && isClaudeRunIncomplete(runtime)) {
|
|
1046
1254
|
const detail = "Claude run reached max-turns before completing execution for this issue.";
|
|
1047
1255
|
const usage = toNormalizedUsage(runtime.parsedUsage);
|
|
@@ -1095,6 +1303,7 @@ export async function runProviderWork(
|
|
|
1095
1303
|
tokenInput,
|
|
1096
1304
|
tokenOutput,
|
|
1097
1305
|
usdCost,
|
|
1306
|
+
finalRunOutput: runtime.finalRunOutput,
|
|
1098
1307
|
usage,
|
|
1099
1308
|
pricingProviderType,
|
|
1100
1309
|
pricingModelId,
|
|
@@ -1128,11 +1337,15 @@ export async function runProviderWork(
|
|
|
1128
1337
|
};
|
|
1129
1338
|
}
|
|
1130
1339
|
const failedUsage = resolveFailedUsage(runtime);
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1340
|
+
const failure = classifyProviderFailure(provider, {
|
|
1341
|
+
detail: resolveRuntimeFailureDetail(runtime, provider),
|
|
1342
|
+
stdout: runtime.stdout,
|
|
1343
|
+
stderr: runtime.stderr,
|
|
1344
|
+
failureType: runtime.failureType
|
|
1345
|
+
});
|
|
1133
1346
|
return {
|
|
1134
1347
|
status: "failed",
|
|
1135
|
-
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${
|
|
1348
|
+
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${failure.detail}`,
|
|
1136
1349
|
tokenInput: failedUsage.tokenInput,
|
|
1137
1350
|
tokenOutput: failedUsage.tokenOutput,
|
|
1138
1351
|
usdCost: failedUsage.usdCost,
|
|
@@ -1142,12 +1355,12 @@ export async function runProviderWork(
|
|
|
1142
1355
|
outcome: toOutcome({
|
|
1143
1356
|
kind: "failed",
|
|
1144
1357
|
issueIdsTouched: issueIdsTouched(context),
|
|
1145
|
-
actions: [{ type: "runtime.execute", status: "error", detail:
|
|
1358
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
|
|
1146
1359
|
blockers: [
|
|
1147
1360
|
{
|
|
1148
|
-
code:
|
|
1149
|
-
message:
|
|
1150
|
-
retryable:
|
|
1361
|
+
code: failure.blockerCode,
|
|
1362
|
+
message: failure.detail,
|
|
1363
|
+
retryable: failure.retryable
|
|
1151
1364
|
}
|
|
1152
1365
|
],
|
|
1153
1366
|
artifacts: [],
|
|
@@ -1171,6 +1384,9 @@ export async function runProviderWork(
|
|
|
1171
1384
|
stderrPreview: toPreview(runtime.stderr),
|
|
1172
1385
|
transcript: runtime.transcript
|
|
1173
1386
|
},
|
|
1387
|
+
...(failure.providerUsageLimited
|
|
1388
|
+
? { dispositionHint: buildProviderUsageLimitedDispositionHint(provider, failure.detail) }
|
|
1389
|
+
: {}),
|
|
1174
1390
|
nextState: context.state
|
|
1175
1391
|
};
|
|
1176
1392
|
}
|
|
@@ -1220,7 +1436,7 @@ export async function runCursorWork(
|
|
|
1220
1436
|
if (
|
|
1221
1437
|
!runtime.ok &&
|
|
1222
1438
|
resumeState.resumeSessionId &&
|
|
1223
|
-
!
|
|
1439
|
+
!isProviderUsageLimitedRuntimeFailure(runtime) &&
|
|
1224
1440
|
isUnknownSessionError(runtime.stderr, runtime.stdout)
|
|
1225
1441
|
) {
|
|
1226
1442
|
const retry = withResolvedRuntimeUsage(
|
|
@@ -1331,7 +1547,12 @@ export async function runOpenCodeWork(context: HeartbeatContext): Promise<Adapte
|
|
|
1331
1547
|
{ provider: "opencode" }
|
|
1332
1548
|
);
|
|
1333
1549
|
const parsed = parseOpenCodeOutput(runtime.stdout);
|
|
1334
|
-
if (
|
|
1550
|
+
if (
|
|
1551
|
+
!runtime.ok &&
|
|
1552
|
+
resumeSessionId &&
|
|
1553
|
+
!isProviderUsageLimitedRuntimeFailure(runtime) &&
|
|
1554
|
+
isUnknownSessionError(runtime.stderr, runtime.stdout)
|
|
1555
|
+
) {
|
|
1335
1556
|
const retry = await executePromptRuntime(
|
|
1336
1557
|
context.runtime?.command ?? "opencode",
|
|
1337
1558
|
prompt,
|
|
@@ -1426,7 +1647,7 @@ export async function runGeminiCliWork(
|
|
|
1426
1647
|
if (
|
|
1427
1648
|
!runtime.ok &&
|
|
1428
1649
|
resumeState.resumeSessionId &&
|
|
1429
|
-
!
|
|
1650
|
+
!isProviderUsageLimitedRuntimeFailure(runtime) &&
|
|
1430
1651
|
isGeminiUnknownSessionError(runtime.stdout, runtime.stderr)
|
|
1431
1652
|
) {
|
|
1432
1653
|
const retry = withResolvedRuntimeUsage(
|
|
@@ -1535,6 +1756,7 @@ export function toProviderResult(
|
|
|
1535
1756
|
forcedKill: boolean;
|
|
1536
1757
|
}>;
|
|
1537
1758
|
parsedUsage?: RuntimeParsedUsage;
|
|
1759
|
+
finalRunOutput?: AdapterExecutionResult["finalRunOutput"];
|
|
1538
1760
|
structuredOutputSource?: "stdout" | "stderr";
|
|
1539
1761
|
structuredOutputDiagnostics?: {
|
|
1540
1762
|
stdoutJsonObjectCount: number;
|
|
@@ -1550,6 +1772,8 @@ export function toProviderResult(
|
|
|
1550
1772
|
| "json_missing"
|
|
1551
1773
|
| "json_on_stderr_only"
|
|
1552
1774
|
| "schema_or_shape_mismatch";
|
|
1775
|
+
finalRunOutputStatus?: "valid" | "missing" | "malformed" | "schema_mismatch";
|
|
1776
|
+
finalRunOutputError?: string;
|
|
1553
1777
|
claudeStopReason?: string;
|
|
1554
1778
|
claudeResultSubtype?: string;
|
|
1555
1779
|
claudeSessionId?: string;
|
|
@@ -1626,6 +1850,41 @@ export function toProviderResult(
|
|
|
1626
1850
|
nextState: applyProviderSessionState(context, provider, sessionUpdate)
|
|
1627
1851
|
};
|
|
1628
1852
|
}
|
|
1853
|
+
if (!runtime.finalRunOutput) {
|
|
1854
|
+
const usage = toNormalizedUsage(runtime.parsedUsage);
|
|
1855
|
+
const detail = resolveFinalRunOutputContractDetail({ provider, runtime });
|
|
1856
|
+
return createContractInvalidResult({
|
|
1857
|
+
context,
|
|
1858
|
+
provider,
|
|
1859
|
+
summary: `${provider} runtime failed contract validation: ${detail}`,
|
|
1860
|
+
tokenInput: usageTokenInputTotal(runtime.parsedUsage),
|
|
1861
|
+
tokenOutput: runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0,
|
|
1862
|
+
usdCost: runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0,
|
|
1863
|
+
...(usage ? { usage } : {}),
|
|
1864
|
+
pricingProviderType,
|
|
1865
|
+
pricingModelId,
|
|
1866
|
+
trace: {
|
|
1867
|
+
command: runtime.commandUsed ?? context.runtime?.command ?? provider,
|
|
1868
|
+
args: runtime.argsUsed,
|
|
1869
|
+
cwd: context.runtime?.cwd,
|
|
1870
|
+
exitCode: runtime.code,
|
|
1871
|
+
elapsedMs: runtime.elapsedMs,
|
|
1872
|
+
timedOut: runtime.timedOut,
|
|
1873
|
+
failureType: "contract_invalid",
|
|
1874
|
+
timeoutSource: runtime.timedOut ? "runtime" : null,
|
|
1875
|
+
usageSource: "structured",
|
|
1876
|
+
attemptCount: runtime.attemptCount,
|
|
1877
|
+
attempts: runtime.attempts,
|
|
1878
|
+
session: sessionUpdate,
|
|
1879
|
+
structuredOutputSource: runtime.structuredOutputSource,
|
|
1880
|
+
structuredOutputDiagnostics: runtime.structuredOutputDiagnostics,
|
|
1881
|
+
stdoutPreview: toPreview(runtime.stdout),
|
|
1882
|
+
stderrPreview: toPreview(runtime.stderr),
|
|
1883
|
+
transcript: runtime.transcript
|
|
1884
|
+
},
|
|
1885
|
+
nextState: applyProviderSessionState(context, provider, sessionUpdate)
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1629
1888
|
const tokenOutput = runtime.parsedUsage?.outputTokens ?? runtime.parsedUsage?.tokenOutput ?? 0;
|
|
1630
1889
|
const usdCost = runtime.parsedUsage?.costUsd ?? runtime.parsedUsage?.usdCost ?? 0;
|
|
1631
1890
|
const usage = toNormalizedUsage(runtime.parsedUsage);
|
|
@@ -1636,6 +1895,7 @@ export function toProviderResult(
|
|
|
1636
1895
|
tokenInput: usageTokenInputTotal(runtime.parsedUsage),
|
|
1637
1896
|
tokenOutput,
|
|
1638
1897
|
usdCost,
|
|
1898
|
+
finalRunOutput: runtime.finalRunOutput,
|
|
1639
1899
|
usage,
|
|
1640
1900
|
pricingProviderType,
|
|
1641
1901
|
pricingModelId,
|
|
@@ -1670,11 +1930,15 @@ export function toProviderResult(
|
|
|
1670
1930
|
};
|
|
1671
1931
|
}
|
|
1672
1932
|
const failedUsage = resolveFailedUsage(runtime);
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1933
|
+
const failure = classifyProviderFailure(provider, {
|
|
1934
|
+
detail: resolveRuntimeFailureDetail(runtime, provider),
|
|
1935
|
+
stdout: runtime.stdout,
|
|
1936
|
+
stderr: runtime.stderr,
|
|
1937
|
+
failureType: runtime.failureType
|
|
1938
|
+
});
|
|
1675
1939
|
return {
|
|
1676
1940
|
status: "failed",
|
|
1677
|
-
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${
|
|
1941
|
+
summary: runtime.parsedUsage?.summary ?? `${provider} runtime failed: ${failure.detail}`,
|
|
1678
1942
|
tokenInput: failedUsage.tokenInput,
|
|
1679
1943
|
tokenOutput: failedUsage.tokenOutput,
|
|
1680
1944
|
usdCost: failedUsage.usdCost,
|
|
@@ -1684,12 +1948,12 @@ export function toProviderResult(
|
|
|
1684
1948
|
outcome: toOutcome({
|
|
1685
1949
|
kind: "failed",
|
|
1686
1950
|
issueIdsTouched: issueIdsTouched(context),
|
|
1687
|
-
actions: [{ type: "runtime.execute", status: "error", detail:
|
|
1951
|
+
actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
|
|
1688
1952
|
blockers: [
|
|
1689
1953
|
{
|
|
1690
|
-
code:
|
|
1691
|
-
message:
|
|
1692
|
-
retryable:
|
|
1954
|
+
code: failure.blockerCode,
|
|
1955
|
+
message: failure.detail,
|
|
1956
|
+
retryable: failure.retryable
|
|
1693
1957
|
}
|
|
1694
1958
|
],
|
|
1695
1959
|
artifacts: [],
|
|
@@ -1714,6 +1978,9 @@ export function toProviderResult(
|
|
|
1714
1978
|
stderrPreview: toPreview(runtime.stderr),
|
|
1715
1979
|
transcript: runtime.transcript
|
|
1716
1980
|
},
|
|
1981
|
+
...(failure.providerUsageLimited
|
|
1982
|
+
? { dispositionHint: buildProviderUsageLimitedDispositionHint(provider, failure.detail) }
|
|
1983
|
+
: {}),
|
|
1717
1984
|
nextState: applyProviderSessionState(context, provider, sessionUpdate)
|
|
1718
1985
|
};
|
|
1719
1986
|
}
|
|
@@ -1724,29 +1991,99 @@ export function resolveRuntimeFailureDetail(runtime: {
|
|
|
1724
1991
|
code: number | null;
|
|
1725
1992
|
failureType?: "timeout" | "spawn_error" | "nonzero_exit";
|
|
1726
1993
|
attempts: Array<{ spawnErrorCode?: string }>;
|
|
1727
|
-
}) {
|
|
1994
|
+
}, provider?: AgentProviderType) {
|
|
1728
1995
|
const stderr = runtime.stderr.trim();
|
|
1996
|
+
const normalize = (detail: string) => (provider ? normalizeProviderFailureDetail(provider, detail) : detail);
|
|
1729
1997
|
if (stderr.length > 0) {
|
|
1730
|
-
return stderr;
|
|
1998
|
+
return normalize(extractStructuredRuntimeErrorDetail(stderr) ?? stderr);
|
|
1731
1999
|
}
|
|
1732
2000
|
const lastAttempt = runtime.attempts[runtime.attempts.length - 1];
|
|
1733
2001
|
if (runtime.failureType === "spawn_error") {
|
|
1734
2002
|
if (lastAttempt?.spawnErrorCode) {
|
|
1735
|
-
return `failed to launch runtime command (${lastAttempt.spawnErrorCode}). Verify the CLI is installed and on PATH
|
|
2003
|
+
return normalize(`failed to launch runtime command (${lastAttempt.spawnErrorCode}). Verify the CLI is installed and on PATH.`);
|
|
1736
2004
|
}
|
|
1737
|
-
return "failed to launch runtime command. Verify the CLI is installed and on PATH.";
|
|
2005
|
+
return normalize("failed to launch runtime command. Verify the CLI is installed and on PATH.");
|
|
1738
2006
|
}
|
|
1739
2007
|
if (runtime.failureType === "timeout") {
|
|
1740
|
-
return "timed out before completion. Increase runtimeTimeoutSec for this agent/runtime.";
|
|
2008
|
+
return normalize("timed out before completion. Increase runtimeTimeoutSec for this agent/runtime.");
|
|
1741
2009
|
}
|
|
1742
2010
|
if (runtime.code !== null) {
|
|
1743
|
-
return `process exited with code ${runtime.code} without stderr output
|
|
2011
|
+
return normalize(`process exited with code ${runtime.code} without stderr output.`);
|
|
1744
2012
|
}
|
|
1745
2013
|
const stdout = runtime.stdout.trim();
|
|
1746
2014
|
if (stdout.length > 0) {
|
|
1747
|
-
|
|
2015
|
+
const structuredStdoutDetail = extractStructuredRuntimeErrorDetail(stdout);
|
|
2016
|
+
if (structuredStdoutDetail) {
|
|
2017
|
+
return normalize(structuredStdoutDetail);
|
|
2018
|
+
}
|
|
2019
|
+
return normalize(`no stderr output; stdout preview: ${toPreview(stdout, 320)}`);
|
|
1748
2020
|
}
|
|
1749
|
-
return "runtime exited without diagnostic output.";
|
|
2021
|
+
return normalize("runtime exited without diagnostic output.");
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function extractStructuredRuntimeErrorDetail(text: string) {
|
|
2025
|
+
const normalized = text.trim();
|
|
2026
|
+
if (!normalized) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
const candidatePayloads = collectJsonObjectCandidates(normalized);
|
|
2030
|
+
for (const candidate of candidatePayloads) {
|
|
2031
|
+
try {
|
|
2032
|
+
const parsed = JSON.parse(candidate) as unknown;
|
|
2033
|
+
const detail = extractErrorDetailFromUnknown(parsed);
|
|
2034
|
+
if (detail) {
|
|
2035
|
+
return detail;
|
|
2036
|
+
}
|
|
2037
|
+
} catch {
|
|
2038
|
+
// ignore malformed JSON fragments
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function collectJsonObjectCandidates(text: string) {
|
|
2045
|
+
const candidates: string[] = [];
|
|
2046
|
+
if (text.startsWith("{") && text.endsWith("}")) {
|
|
2047
|
+
candidates.push(text);
|
|
2048
|
+
}
|
|
2049
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2050
|
+
const trimmed = line.trim();
|
|
2051
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
2052
|
+
candidates.push(trimmed);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return candidates;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function extractErrorDetailFromUnknown(value: unknown): string | null {
|
|
2059
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
const record = value as Record<string, unknown>;
|
|
2063
|
+
const directCandidates = [
|
|
2064
|
+
record.detail,
|
|
2065
|
+
record.message,
|
|
2066
|
+
record.summary,
|
|
2067
|
+
record.reason,
|
|
2068
|
+
record.error,
|
|
2069
|
+
record.description
|
|
2070
|
+
];
|
|
2071
|
+
for (const candidate of directCandidates) {
|
|
2072
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
2073
|
+
return candidate.trim();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const nestedError = record.error;
|
|
2077
|
+
if (nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)) {
|
|
2078
|
+
const nestedRecord = nestedError as Record<string, unknown>;
|
|
2079
|
+
const nestedCandidates = [nestedRecord.detail, nestedRecord.message, nestedRecord.reason, nestedRecord.description];
|
|
2080
|
+
for (const candidate of nestedCandidates) {
|
|
2081
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
2082
|
+
return candidate.trim();
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return null;
|
|
1750
2087
|
}
|
|
1751
2088
|
|
|
1752
2089
|
export function parseOpenCodeOutput(stdout: string) {
|
|
@@ -2280,8 +2617,66 @@ export function toEnvironmentStatus(checks: AdapterEnvironmentCheck[]): "pass" |
|
|
|
2280
2617
|
return "pass";
|
|
2281
2618
|
}
|
|
2282
2619
|
|
|
2620
|
+
function resolveHeartbeatPromptModeForPrompt(context: HeartbeatContext): HeartbeatPromptMode {
|
|
2621
|
+
return context.promptMode === "compact" ? "compact" : "full";
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/** Max chars per memory section (tacit notes, durable facts block, daily notes block). Env overrides; compact defaults to 8000. */
|
|
2625
|
+
function resolveMemorySectionMaxChars(mode: HeartbeatPromptMode): number | null {
|
|
2626
|
+
const raw = process.env.BOPO_HEARTBEAT_PROMPT_MEMORY_MAX_CHARS?.trim();
|
|
2627
|
+
if (raw) {
|
|
2628
|
+
const n = Number.parseInt(raw, 10);
|
|
2629
|
+
if (Number.isFinite(n) && n > 0) {
|
|
2630
|
+
return n;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (mode === "compact") {
|
|
2634
|
+
return 8000;
|
|
2635
|
+
}
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
function clipPromptText(text: string, max: number | null): string {
|
|
2640
|
+
if (!max || text.length <= max) {
|
|
2641
|
+
return text;
|
|
2642
|
+
}
|
|
2643
|
+
return `${text.slice(0, max)}\n…(truncated for prompt size)`;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
const HEARTBEAT_JSON_SCHEMA_FOOTER = `At the end of your response, output exactly one JSON object on a single line and nothing else. Use this exact schema:
|
|
2647
|
+
{"employee_comment":"markdown update to the manager","results":["short concrete outcome"],"errors":[],"artifacts":[{"kind":"file","path":"relative/path"}]}`;
|
|
2648
|
+
|
|
2649
|
+
function buildIdleMicroPrompt(context: HeartbeatContext): string {
|
|
2650
|
+
const bootstrapPrompt = context.runtime?.bootstrapPrompt?.trim();
|
|
2651
|
+
return `${bootstrapPrompt ? `${bootstrapPrompt}\n\n` : ""}Idle heartbeat (micro prompt): agent ${context.agentId} (${context.agent.name}) has no assigned issues this run. Summarize readiness in \`employee_comment\`; leave \`results\` empty unless you completed verifiable work. Use \`BOPODEV_*\` for control-plane API calls when needed.
|
|
2652
|
+
|
|
2653
|
+
${HEARTBEAT_JSON_SCHEMA_FOOTER}
|
|
2654
|
+
`;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function formatAttachmentLine(
|
|
2658
|
+
attachment: NonNullable<HeartbeatContext["workItems"][number]["attachments"]>[number],
|
|
2659
|
+
mode: HeartbeatPromptMode,
|
|
2660
|
+
apiBase: string
|
|
2661
|
+
): string {
|
|
2662
|
+
const base = apiBase.replace(/\/$/, "");
|
|
2663
|
+
const apiUrl = attachment.downloadPath ? `${base}${attachment.downloadPath}` : null;
|
|
2664
|
+
if (mode === "compact" && apiUrl) {
|
|
2665
|
+
return ` - ${attachment.fileName} | api: ${apiUrl} | path: ${attachment.absolutePath} | relative: ${attachment.relativePath}`;
|
|
2666
|
+
}
|
|
2667
|
+
const apiSuffix = apiUrl ? ` | api: ${apiUrl}` : "";
|
|
2668
|
+
return ` - ${attachment.fileName} | path: ${attachment.absolutePath} | relative: ${attachment.relativePath}${apiSuffix}`;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2283
2671
|
export function createPrompt(context: HeartbeatContext) {
|
|
2672
|
+
const isCommentOrderRunEarly = context.wakeContext?.reason === "issue_comment_recipient";
|
|
2673
|
+
if (context.idleMicroPrompt && context.workItems.length === 0 && !isCommentOrderRunEarly) {
|
|
2674
|
+
return buildIdleMicroPrompt(context);
|
|
2675
|
+
}
|
|
2284
2676
|
const bootstrapPrompt = context.runtime?.bootstrapPrompt?.trim();
|
|
2677
|
+
const promptMode = resolveHeartbeatPromptModeForPrompt(context);
|
|
2678
|
+
const isCompact = promptMode === "compact";
|
|
2679
|
+
const memoryCap = resolveMemorySectionMaxChars(promptMode);
|
|
2285
2680
|
const companyGoals = context.goalContext?.companyGoals.length
|
|
2286
2681
|
? context.goalContext.companyGoals.map((goal) => `- ${goal}`).join("\n")
|
|
2287
2682
|
: "- No active company goals";
|
|
@@ -2292,6 +2687,8 @@ export function createPrompt(context: HeartbeatContext) {
|
|
|
2292
2687
|
? context.goalContext.agentGoals.map((goal) => `- ${goal}`).join("\n")
|
|
2293
2688
|
: "- No active agent goals";
|
|
2294
2689
|
const isCommentOrderRun = context.wakeContext?.reason === "issue_comment_recipient";
|
|
2690
|
+
const controlPlaneApiBaseUrl =
|
|
2691
|
+
context.runtime?.env?.BOPODEV_API_BASE_URL?.trim() || context.runtime?.env?.BOPODEV_API_URL?.trim() || "";
|
|
2295
2692
|
const workItems = context.workItems.length
|
|
2296
2693
|
? context.workItems
|
|
2297
2694
|
.map((item) =>
|
|
@@ -2302,14 +2699,18 @@ export function createPrompt(context: HeartbeatContext) {
|
|
|
2302
2699
|
item.childIssueIds?.length ? ` Sub-issues: ${item.childIssueIds.join(", ")}` : null,
|
|
2303
2700
|
item.status ? ` Status: ${item.status}` : null,
|
|
2304
2701
|
item.priority ? ` Priority: ${item.priority}` : null,
|
|
2305
|
-
|
|
2702
|
+
isCompact
|
|
2703
|
+
? ` Body: (omitted — fetch with GET ${controlPlaneApiBaseUrl || "$BOPODEV_API_BASE_URL"}/issues/${item.issueId})`
|
|
2704
|
+
: item.body
|
|
2705
|
+
? ` Body: ${item.body}`
|
|
2706
|
+
: null,
|
|
2306
2707
|
item.labels?.length ? ` Labels: ${item.labels.join(", ")}` : null,
|
|
2307
2708
|
item.tags?.length ? ` Tags: ${item.tags.join(", ")}` : null,
|
|
2308
2709
|
item.attachments?.length
|
|
2309
2710
|
? [
|
|
2310
2711
|
" Attachments:",
|
|
2311
2712
|
...item.attachments.map((attachment) =>
|
|
2312
|
-
|
|
2713
|
+
formatAttachmentLine(attachment, promptMode, controlPlaneApiBaseUrl || "http://127.0.0.1:4020")
|
|
2313
2714
|
)
|
|
2314
2715
|
].join("\n")
|
|
2315
2716
|
: null
|
|
@@ -2339,22 +2740,36 @@ export function createPrompt(context: HeartbeatContext) {
|
|
|
2339
2740
|
].join("\n")
|
|
2340
2741
|
: "";
|
|
2341
2742
|
const memoryContext = context.memoryContext;
|
|
2342
|
-
const
|
|
2743
|
+
const memoryTacitNotesRaw = memoryContext?.tacitNotes?.trim()
|
|
2343
2744
|
? memoryContext.tacitNotes.trim()
|
|
2344
2745
|
: "No tacit notes were recorded yet.";
|
|
2345
|
-
const
|
|
2746
|
+
const memoryTacitNotes = clipPromptText(memoryTacitNotesRaw, memoryCap);
|
|
2747
|
+
const memoryDurableFactsRaw =
|
|
2346
2748
|
memoryContext?.durableFacts && memoryContext.durableFacts.length > 0
|
|
2347
2749
|
? memoryContext.durableFacts.map((fact) => `- ${fact}`).join("\n")
|
|
2348
2750
|
: "- No durable facts available.";
|
|
2349
|
-
const
|
|
2751
|
+
const memoryDurableFacts = clipPromptText(memoryDurableFactsRaw, memoryCap);
|
|
2752
|
+
const memoryDailyNotesRaw =
|
|
2350
2753
|
memoryContext?.dailyNotes && memoryContext.dailyNotes.length > 0
|
|
2351
2754
|
? memoryContext.dailyNotes.map((note) => `- ${note}`).join("\n")
|
|
2352
2755
|
: "- No recent daily notes.";
|
|
2353
|
-
const
|
|
2354
|
-
context.runtime?.env?.BOPODEV_API_BASE_URL?.trim() || context.runtime?.env?.BOPODEV_API_URL?.trim() || "";
|
|
2756
|
+
const memoryDailyNotes = clipPromptText(memoryDailyNotesRaw, memoryCap);
|
|
2355
2757
|
const hasControlPlaneHeaders = Boolean(context.runtime?.env?.BOPODEV_REQUEST_HEADERS_JSON?.trim());
|
|
2356
2758
|
const safeControlPlaneCurl =
|
|
2357
2759
|
'curl -sS -H "x-company-id: $BOPODEV_COMPANY_ID" -H "x-actor-type: $BOPODEV_ACTOR_TYPE" -H "x-actor-id: $BOPODEV_ACTOR_ID" -H "x-actor-companies: $BOPODEV_ACTOR_COMPANIES" -H "x-actor-permissions: $BOPODEV_ACTOR_PERMISSIONS" "$BOPODEV_API_BASE_URL/agents"';
|
|
2760
|
+
const compactHydration =
|
|
2761
|
+
isCompact &&
|
|
2762
|
+
(context.workItems.length > 0 ||
|
|
2763
|
+
(context.wakeContext?.issueIds && context.wakeContext.issueIds.length > 0))
|
|
2764
|
+
? [
|
|
2765
|
+
"Context hydration (compact prompt mode):",
|
|
2766
|
+
"- Load full issue description and attachment list (with `downloadPath` for each file) via GET `$BOPODEV_API_BASE_URL`/issues/{issueId} before substantive work.",
|
|
2767
|
+
"- Use the same actor headers as in the control-plane section below.",
|
|
2768
|
+
`- Example: curl -sS -H "x-company-id: $BOPODEV_COMPANY_ID" -H "x-actor-type: $BOPODEV_ACTOR_TYPE" -H "x-actor-id: $BOPODEV_ACTOR_ID" -H "x-actor-companies: $BOPODEV_ACTOR_COMPANIES" -H "x-actor-permissions: $BOPODEV_ACTOR_PERMISSIONS" "${controlPlaneApiBaseUrl || "$BOPODEV_API_BASE_URL"}/issues/<issueId>"`,
|
|
2769
|
+
""
|
|
2770
|
+
].join("\n")
|
|
2771
|
+
: "";
|
|
2772
|
+
|
|
2358
2773
|
const controlPlaneDirectives = [
|
|
2359
2774
|
"Control-plane API directives:",
|
|
2360
2775
|
controlPlaneApiBaseUrl
|
|
@@ -2387,21 +2802,26 @@ export function createPrompt(context: HeartbeatContext) {
|
|
|
2387
2802
|
"- If payload files are required, write under `agents/<agent-id>/tmp/` (or OS temp via `mktemp`) and do not treat cleanup command failures as task blockers.",
|
|
2388
2803
|
"- If control-plane API connectivity fails, report the exact failing command/error once and stop retry loops for the same endpoint.",
|
|
2389
2804
|
"- For write_todos status values, only use: todo, in_progress, blocked, in_review, done, canceled (US spelling, not cancelled).",
|
|
2390
|
-
"- If any command fails, avoid further exploratory commands and still return the required final JSON
|
|
2805
|
+
"- If any command fails, avoid further exploratory commands and still return the required final JSON object.",
|
|
2391
2806
|
"- Do not use emojis in issue comments, summaries, or status messages.",
|
|
2392
2807
|
isCommentOrderRun
|
|
2393
2808
|
? "- Do not stop after planning. Execute concrete steps only for the triggering comment order."
|
|
2394
2809
|
: "- Do not stop after planning. You must execute concrete steps for assigned issues in this run (file edits, API calls, or other verifiable actions).",
|
|
2395
|
-
"- If you cannot complete concrete execution,
|
|
2810
|
+
"- If you cannot complete concrete execution, explain the blocker plainly in `employee_comment` and add it to `errors` instead of claiming success.",
|
|
2396
2811
|
"- Treat file memory as source of truth for long-term context: append raw observations to daily notes first, then promote stable patterns to durable facts.",
|
|
2397
2812
|
"- Avoid writing duplicate durable facts when existing memory already contains the same lesson.",
|
|
2398
|
-
"- Your final output must be
|
|
2813
|
+
"- Your final output must be exactly one JSON object and nothing else.",
|
|
2814
|
+
"- Do not include any fields besides `employee_comment`, `results`, `errors`, and `artifacts`.",
|
|
2815
|
+
"- `employee_comment` must be markdown written like a concise employee updating a manager with concrete actions, outcome, and blocker or next step when relevant.",
|
|
2816
|
+
"- `results` must list concrete completed outcomes as short strings.",
|
|
2817
|
+
"- `errors` must list concrete blockers or failures as short strings and be empty on clean success.",
|
|
2818
|
+
"- `artifacts` must contain objects like {\"kind\":\"file\",\"path\":\"relative/path\"}.",
|
|
2399
2819
|
"- Do not invent token or cost values; the runtime records usage separately."
|
|
2400
2820
|
].join("\n");
|
|
2401
2821
|
|
|
2402
2822
|
return `${bootstrapPrompt ? `${bootstrapPrompt}\n\n` : ""}You are ${context.agent.name} (${context.agent.role}), agent ${context.agentId}.
|
|
2403
2823
|
Heartbeat run ${context.heartbeatRunId}.
|
|
2404
|
-
|
|
2824
|
+
${isCompact ? "Prompt profile: compact (issue bodies are not inlined—use GET /issues/:id to hydrate).\n" : ""}
|
|
2405
2825
|
Company:
|
|
2406
2826
|
- Name: ${context.company.name}
|
|
2407
2827
|
- Mission: ${context.company.mission ?? "No mission set"}
|
|
@@ -2414,7 +2834,7 @@ ${projectGoals}
|
|
|
2414
2834
|
Agent goals:
|
|
2415
2835
|
${agentGoals}
|
|
2416
2836
|
|
|
2417
|
-
${isCommentOrderRun ? "Linked issue context (read-only):" : "Assigned issues:"}
|
|
2837
|
+
${compactHydration}${isCommentOrderRun ? "Linked issue context (read-only):" : "Assigned issues:"}
|
|
2418
2838
|
${workItems}
|
|
2419
2839
|
|
|
2420
2840
|
${wakeContextLines}
|
|
@@ -2434,8 +2854,7 @@ ${executionDirectives}
|
|
|
2434
2854
|
|
|
2435
2855
|
${controlPlaneDirectives}
|
|
2436
2856
|
|
|
2437
|
-
|
|
2438
|
-
{"summary":"brief outcome and any blocker"}
|
|
2857
|
+
${HEARTBEAT_JSON_SCHEMA_FOOTER}
|
|
2439
2858
|
`;
|
|
2440
2859
|
}
|
|
2441
2860
|
|