diff --git a/src/context.ts b/src/context.ts index 55f4c39..b9e3655 100644 --- a/src/context.ts +++ b/src/context.ts @@ -7,7 +7,14 @@ import type { Database } from "bun:sqlite"; import { getWorkflowById } from "./db"; import type { AdapterConfig, BundleArtifactRecord, Profile, ReplayGrade, TaskRecord, RuntimeConfig } from "./types"; -const MAX_PAYLOAD_CHARS = 4000; +/** + * Default cap on characters used when summarizing the task payload for the + * prompt. Profiles can override via `context_policy.max_payload_chars` when + * they need to ship larger evidence payloads (e.g., curated PR diffs for + * review tasks). Kept conservative to preserve prior behavior for profiles + * that do not opt in. + */ +export const DEFAULT_MAX_PAYLOAD_CHARS = 4000; function sanitizeRelativeArtifactPath(value: string): string { return value @@ -40,8 +47,8 @@ function truncateText(value: string, maxChars: number): string { return `${value.slice(0, Math.max(0, maxChars - 16))}\n[truncated]\n`; } -function summarizeJson(value: Record): string { - return truncateText(JSON.stringify(value, null, 2), MAX_PAYLOAD_CHARS); +function summarizeJson(value: Record, maxChars: number = DEFAULT_MAX_PAYLOAD_CHARS): string { + return truncateText(JSON.stringify(value, null, 2), maxChars); } function buildBundleTaskSnapshot(task: TaskRecord): Record { @@ -200,7 +207,7 @@ function buildGoalLoopContextLines(config: RuntimeConfig, task: TaskRecord): str } function buildPromptText(config: RuntimeConfig, profile: Profile, task: TaskRecord): string { - const payloadBlock = summarizeJson(task.payload); + const payloadBlock = summarizeJson(task.payload, profile.context_policy.max_payload_chars); const phaseSkills = Array.isArray(task.payload.phase_skills) ? task.payload.phase_skills.filter((value): value is string => typeof value === "string" && value.trim().length > 0) : []; diff --git a/src/runtime.test.ts b/src/runtime.test.ts index 69cb432..a4719c5 100644 --- a/src/runtime.test.ts +++ b/src/runtime.test.ts @@ -792,6 +792,56 @@ test("goal loop context no longer inlines oversized artifact content", async () expect(context).not.toContain("tail"); }); +test("payload truncates to default 4000 chars when context_policy does not override max_payload_chars", () => { + const config = testConfig(); + const longString = "x".repeat(8000); + const profile = testProfile(); + const context = assembleContext(config, profile, testTaskRecord({ + payload: { evidence: longString } + })); + + expect(context).toContain("[truncated]"); + // Default cap is 4000; the payload block should not contain the full 8000-char value. + expect(context).not.toContain("x".repeat(8000)); +}); + +test("payload respects context_policy.max_payload_chars override when profile sets it higher", () => { + const config = testConfig(); + const longString = "y".repeat(8000); + const profile = testProfile({ + context_policy: { + include_recent_task_memory: true, + max_prompt_chars: 32000, + max_payload_chars: 12000 + } + }); + const context = assembleContext(config, profile, testTaskRecord({ + payload: { evidence: longString } + })); + + // With a 12000-char payload cap, the full 8000-char value fits without truncation. + expect(context).toContain("y".repeat(8000)); + expect(context).not.toContain("[truncated]"); +}); + +test("payload respects context_policy.max_payload_chars override when profile sets it lower", () => { + const config = testConfig(); + const longString = "z".repeat(2000); + const profile = testProfile({ + context_policy: { + include_recent_task_memory: true, + max_prompt_chars: 16000, + max_payload_chars: 1000 + } + }); + const context = assembleContext(config, profile, testTaskRecord({ + payload: { evidence: longString } + })); + + expect(context).toContain("[truncated]"); + expect(context).not.toContain("z".repeat(2000)); +}); + test("artifact-writing context requires declared managed artifact paths in artifact_paths", () => { const config = testConfig(); const context = assembleContext(config, testProfile(), testTaskRecord({ diff --git a/src/types.ts b/src/types.ts index 7b8785d..ab47176 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,6 +82,13 @@ export type Profile = { context_policy: { include_recent_task_memory: boolean; max_prompt_chars: number; + /** + * Maximum characters used when summarizing the task payload for the prompt. + * Optional; defaults to the runtime-internal `DEFAULT_MAX_PAYLOAD_CHARS` + * (currently 4000) when unset. Profiles that need to ship larger evidence + * payloads (curated diffs, structured review excerpts) can raise this. + */ + max_payload_chars?: number; }; result_schema: Record; integration_policies: Record;