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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/README.md +27 -0
  4. package/dist/adapters/codex/src/server/quota.d.ts +2 -0
  5. package/dist/agent-sdk/src/adapters.d.ts +12 -1
  6. package/dist/agent-sdk/src/provider-failures/anthropic-api.d.ts +5 -0
  7. package/dist/agent-sdk/src/provider-failures/claude-code.d.ts +5 -0
  8. package/dist/agent-sdk/src/provider-failures/codex.d.ts +5 -0
  9. package/dist/agent-sdk/src/provider-failures/common.d.ts +7 -0
  10. package/dist/agent-sdk/src/provider-failures/cursor.d.ts +5 -0
  11. package/dist/agent-sdk/src/provider-failures/gemini-cli.d.ts +5 -0
  12. package/dist/agent-sdk/src/provider-failures/http.d.ts +5 -0
  13. package/dist/agent-sdk/src/provider-failures/index.d.ts +5 -0
  14. package/dist/agent-sdk/src/provider-failures/openai-api.d.ts +5 -0
  15. package/dist/agent-sdk/src/provider-failures/opencode.d.ts +5 -0
  16. package/dist/agent-sdk/src/provider-failures/shell.d.ts +5 -0
  17. package/dist/agent-sdk/src/provider-failures/types.d.ts +20 -0
  18. package/dist/agent-sdk/src/quota.d.ts +4 -0
  19. package/dist/agent-sdk/src/runtime-core.d.ts +1 -1
  20. package/dist/agent-sdk/src/runtime-http.d.ts +4 -2
  21. package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -1
  22. package/dist/agent-sdk/src/runtime.d.ts +13 -0
  23. package/dist/agent-sdk/src/types.d.ts +19 -1
  24. package/dist/contracts/src/index.d.ts +426 -11
  25. package/package.json +2 -2
  26. package/src/adapters.ts +477 -58
  27. package/src/provider-failures/anthropic-api.ts +20 -0
  28. package/src/provider-failures/claude-code.ts +20 -0
  29. package/src/provider-failures/codex.ts +23 -0
  30. package/src/provider-failures/common.ts +86 -0
  31. package/src/provider-failures/cursor.ts +20 -0
  32. package/src/provider-failures/gemini-cli.ts +20 -0
  33. package/src/provider-failures/http.ts +12 -0
  34. package/src/provider-failures/index.ts +54 -0
  35. package/src/provider-failures/openai-api.ts +20 -0
  36. package/src/provider-failures/opencode.ts +20 -0
  37. package/src/provider-failures/shell.ts +12 -0
  38. package/src/provider-failures/types.ts +28 -0
  39. package/src/runtime-core.ts +7 -1
  40. package/src/runtime-http.ts +51 -6
  41. package/src/runtime-parsers.ts +1 -0
  42. package/src/runtime.ts +299 -1
  43. 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 { checkRuntimeCommandHealth, containsRateLimitFailure, executeAgentRuntime, executePromptRuntime } from "./runtime-core";
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 isRateLimitedRuntimeFailure(runtime: { stdout: string; stderr: string }, detail?: string) {
46
- return containsRateLimitFailure(`${detail ?? ""}\n${runtime.stderr}\n${runtime.stdout}`);
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 failureDetail = resolveRuntimeFailureDetail(runtime);
413
- const rateLimitedFailure = isRateLimitedRuntimeFailure(runtime, failureDetail);
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: ${failureDetail}`,
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: failureDetail }],
549
+ actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
424
550
  blockers: [
425
551
  {
426
- code: runtime.failureType ?? "runtime_failed",
427
- message: failureDetail,
428
- retryable: !rateLimitedFailure
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 failureDetail = runtime.error ?? "direct API request failed";
879
- const rateLimitedFailure =
880
- runtime.failureType === "rate_limit" || containsRateLimitFailure(`${failureDetail}\n${runtime.responsePreview ?? ""}`);
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: ${failureDetail}`,
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: failureDetail }],
1063
+ actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
893
1064
  blockers: [{
894
- code: runtime.failureType ?? "runtime_failed",
895
- message: failureDetail,
896
- retryable: runtime.failureType !== "auth" && runtime.failureType !== "bad_response" && !rateLimitedFailure
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 failureDetail = resolveRuntimeFailureDetail(runtime);
1132
- const rateLimitedFailure = isRateLimitedRuntimeFailure(runtime, failureDetail);
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: ${failureDetail}`,
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: failureDetail }],
1358
+ actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
1146
1359
  blockers: [
1147
1360
  {
1148
- code: runtime.failureType ?? "runtime_failed",
1149
- message: failureDetail,
1150
- retryable: !rateLimitedFailure
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
- !isRateLimitedRuntimeFailure(runtime) &&
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 (!runtime.ok && resumeSessionId && !isRateLimitedRuntimeFailure(runtime) && isUnknownSessionError(runtime.stderr, runtime.stdout)) {
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
- !isRateLimitedRuntimeFailure(runtime) &&
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 failureDetail = resolveRuntimeFailureDetail(runtime);
1674
- const rateLimitedFailure = isRateLimitedRuntimeFailure(runtime, failureDetail);
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: ${failureDetail}`,
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: failureDetail }],
1951
+ actions: [{ type: "runtime.execute", status: "error", detail: failure.detail }],
1688
1952
  blockers: [
1689
1953
  {
1690
- code: runtime.failureType ?? "runtime_failed",
1691
- message: failureDetail,
1692
- retryable: !rateLimitedFailure
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
- return `no stderr output; stdout preview: ${toPreview(stdout, 320)}`;
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
- item.body ? ` Body: ${item.body}` : null,
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
- ` - ${attachment.fileName} | path: ${attachment.absolutePath} | relative: ${attachment.relativePath}`
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 memoryTacitNotes = memoryContext?.tacitNotes?.trim()
2743
+ const memoryTacitNotesRaw = memoryContext?.tacitNotes?.trim()
2343
2744
  ? memoryContext.tacitNotes.trim()
2344
2745
  : "No tacit notes were recorded yet.";
2345
- const memoryDurableFacts =
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 memoryDailyNotes =
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 controlPlaneApiBaseUrl =
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 summary.",
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, set summary to include the blocker explicitly instead of claiming success.",
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 only the JSON object below, with no prose before or after it.",
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
- At the end of your response, output exactly one JSON object on a single line and nothing else:
2438
- {"summary":"brief outcome and any blocker"}
2857
+ ${HEARTBEAT_JSON_SCHEMA_FOOTER}
2439
2858
  `;
2440
2859
  }
2441
2860