Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
15fb7cf
feat(scripts): add context-illusion replay miner
durch Jun 10, 2026
29b6147
feat(scripts): add working-set retention counterfactual simulator
durch Jun 10, 2026
c2cf01a
feat(coding-agent): add working-set retention to context assembler
durch Jun 10, 2026
95ba763
fix(coding-agent): supersede stale working-set pins on proven content…
durch Jun 10, 2026
554cda7
feat(coding-agent): carry recovery recipes in compression stub headers
durch Jun 10, 2026
d4e653f
feat(coding-agent): surface working-set pin count in assembly summary
durch Jun 10, 2026
d70028d
feat(ai): show required shape in tool validation errors
durch Jun 10, 2026
f28a04a
fix(coding-agent): auto-recover task store from Lance storage corruption
durch Jun 10, 2026
7813131
fix(coding-agent): seed ingest turn counter on resume; teach real rec…
durch Jun 10, 2026
f6f167e
fix(coding-agent): inject assembled context at message tail for cache…
durch Jun 10, 2026
46fb4ee
fix(coding-agent): stop RNA views substituting for explicit content r…
durch Jun 10, 2026
6b6def4
fix(coding-agent): eliminate bash interceptor false positives
durch Jun 10, 2026
c5677f2
feat(scripts): track failed recall turn-expansions in illusion miner
durch Jun 10, 2026
f4d218b
chore: ignore local sessions file
durch Jun 11, 2026
90d6d3d
chore: update workstream expert prompts and session helper
durch Jun 12, 2026
d811c97
fix(ai): keep Anthropic cache markers off synthetic context
durch Jun 12, 2026
8290e89
Add oh-omp workstream expert system
durch Jun 13, 2026
2bf1d0d
docs: update oh-omp expert system phase gates
durch Jun 13, 2026
9428b96
docs: harden workstream expert builder gates
durch Jun 13, 2026
d267112
chore: make sessions path repo-relative
durch Jun 13, 2026
2bed716
docs: make workstreams deliver PR-ready changes
durch Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ syntax.jsonl
out.jsonl
out.html
pi-*.html
sessions.txt

# Generated files
packages/coding-agent/src/internal-urls/docs-index.generated.ts
450 changes: 450 additions & 0 deletions .oh/workstreams/oh-omp/EXPERT-SYSTEM.md

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions .oh/workstreams/oh-omp/FRAME-model-routing-superseded.md

Large diffs are not rendered by default.

218 changes: 218 additions & 0 deletions .oh/workstreams/oh-omp/FRAME.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]


### Fixed

- Fixed Anthropic prompt-cache marker placement to use source-message eligibility before developer/tool-result messages are flattened into Anthropic user messages. Synthetic developer tail context no longer receives message cache markers, while stable user/tool-result transcript messages remain cache candidates.
## [13.19.0] - 2026-04-05

### Fixed
Expand Down
82 changes: 60 additions & 22 deletions packages/ai/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,11 @@ type CacheControlBlock = {
cache_control?: AnthropicCacheControl | null;
};

type AnthropicMessageConversion = {
messages: MessageParam[];
cacheCandidateMessageIndexes: number[];
};

function applyCacheControlToLastBlock<T extends CacheControlBlock>(
blocks: T[],
cacheControl: AnthropicCacheControl,
Expand All @@ -1146,7 +1151,11 @@ function applyCacheControlToLastTextBlock(
applyCacheControlToLastBlock(blocks, cacheControl);
}

function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?: AnthropicCacheControl): void {
function applyPromptCaching(
params: MessageCreateParamsStreaming,
cacheControl?: AnthropicCacheControl,
cacheCandidateMessageIndexes?: number[],
): void {
if (!cacheControl) return;

// Skip if cache_control breakpoints were already placed externally on messages.
Expand Down Expand Up @@ -1174,9 +1183,9 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:

if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;

const userIndexes = params.messages
.map((message, index) => (message.role === "user" ? index : -1))
.filter(index => index >= 0);
const userIndexes =
cacheCandidateMessageIndexes ??
params.messages.map((message, index) => (message.role === "user" ? index : -1)).filter(index => index >= 0);

if (userIndexes.length >= 2) {
const penultimateUserIndex = userIndexes[userIndexes.length - 2];
Expand Down Expand Up @@ -1358,9 +1367,10 @@ function buildParams(
options?: AnthropicOptions,
): MessageCreateParamsStreaming {
const { cacheControl } = getCacheControl(baseUrl, options?.cacheRetention);
const convertedMessages = convertAnthropicMessagesWithMetadata(context.messages, model, useClaudeCompatibleShape);
const params: AnthropicSamplingParams = {
model: model.id,
messages: convertAnthropicMessages(context.messages, model, useClaudeCompatibleShape),
messages: convertedMessages.messages,
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
stream: true,
};
Expand Down Expand Up @@ -1432,19 +1442,26 @@ function buildParams(
}
disableThinkingIfToolChoiceForced(params);
ensureMaxTokensForThinking(params, model);
applyPromptCaching(params, cacheControl);
applyPromptCaching(params, cacheControl, convertedMessages.cacheCandidateMessageIndexes);
enforceCacheControlLimit(params, 4);
normalizeCacheControlTtlOrdering(params);

return params;
}

export function convertAnthropicMessages(
function convertAnthropicMessagesWithMetadata(
messages: Message[],
model: Model<"anthropic-messages">,
useClaudeCompatibleShape: boolean,
): MessageParam[] {
): AnthropicMessageConversion {
const params: MessageParam[] = [];
const cacheCandidateMessageIndexes: number[] = [];

const pushUserParam = (message: MessageParam, cacheCandidate: boolean): void => {
const index = params.length;
params.push(message);
if (cacheCandidate) cacheCandidateMessageIndexes.push(index);
};

const transformedMessages = transformMessages(messages, model, normalizeToolCallId);

Expand All @@ -1454,12 +1471,16 @@ export function convertAnthropicMessages(
if (msg.role === "user" || msg.role === "developer") {
if (!msg.content) continue;

const cacheCandidate = msg.role === "user" && !msg.synthetic;
if (typeof msg.content === "string") {
if (msg.content.trim().length > 0) {
params.push({
role: "user",
content: msg.content.toWellFormed(),
});
pushUserParam(
{
role: "user",
content: msg.content.toWellFormed(),
},
cacheCandidate,
);
}
} else {
const blocks: ContentBlockParam[] = msg.content.map(item => {
Expand All @@ -1486,10 +1507,13 @@ export function convertAnthropicMessages(
return true;
});
if (filteredBlocks.length === 0) continue;
params.push({
role: "user",
content: filteredBlocks,
});
pushUserParam(
{
role: "user",
content: filteredBlocks,
},
cacheCandidate,
);
}
} else if (msg.role === "assistant") {
const blocks: ContentBlockParam[] = [];
Expand Down Expand Up @@ -1583,19 +1607,33 @@ export function convertAnthropicMessages(
// Skip the messages we've already processed
i = j - 1;

// Add a single user message with all tool results
params.push({
role: "user",
content: toolResults,
});
// Add a single user message with all tool results. Tool results are stable
// transcript facts, so they remain eligible cache breakpoints even though
// Anthropic transports them as user messages.
pushUserParam(
{
role: "user",
content: toolResults,
},
true,
);
}
}

if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
// Synthetic fallback required by Anthropic, not a stable transcript boundary.
params.push({ role: "user", content: "Continue." });
}

return params;
return { messages: params, cacheCandidateMessageIndexes };
}

export function convertAnthropicMessages(
messages: Message[],
model: Model<"anthropic-messages">,
useClaudeCompatibleShape: boolean,
): MessageParam[] {
return convertAnthropicMessagesWithMetadata(messages, model, useClaudeCompatibleShape).messages;
}

function convertTools(tools: Tool[], useClaudeCompatibleShape: boolean): Anthropic.Messages.Tool[] {
Expand Down
42 changes: 41 additions & 1 deletion packages/ai/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,39 @@ function compileSchema(schema: object): import("ajv").ValidateFunction {

const MAX_TYPE_COERCION_PASSES = 5;

/**
* Render a compact required-shape skeleton from a JSON schema: required
* properties only, two levels deep. Appended to validation errors when a
* required property is missing, so the model sees what a valid call looks
* like at the moment of failure instead of only the missing property name.
*/
function renderRequiredSkeleton(schema: unknown, depth = 0): string | null {
if (depth > 2 || typeof schema !== "object" || schema === null) return null;
const s = schema as {
type?: string;
properties?: Record<string, unknown>;
required?: string[];
items?: unknown;
};
if (s.type === "object" && s.properties) {
const required = s.required ?? [];
if (required.length === 0) return "{}";
const fields = required
.map(name => {
const prop = s.properties?.[name] as { type?: string } | undefined;
const nested = renderRequiredSkeleton(prop, depth + 1);
return `"${name}": ${nested ?? prop?.type ?? "any"}`;
})
.join(", ");
return `{ ${fields} }`;
}
if (s.type === "array") {
const item = renderRequiredSkeleton(s.items, depth + 1);
return `[${item ?? "…"}]`;
}
return null;
}

/**
* Finds a tool by name and validates the tool call arguments against its TypeBox schema
* @param tools Array of tool definitions
Expand Down Expand Up @@ -689,9 +722,16 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
}
: originalArgs;

// When a required property is missing, show the full required shape —
// the dominant failure mode is models omitting a container property
// (e.g. task's `tasks` array) while confidently filling sibling fields.
const hasMissingRequired = validate.errors?.some((err: any) => err.keyword === "required") ?? false;
const skeleton = hasMissingRequired ? renderRequiredSkeleton(tool.parameters) : null;
const skeletonHint = skeleton ? `\n\nRequired shape:\n${skeleton}` : "";

const errorMessage = `Validation failed for tool "${
toolCall.name
}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;
}":\n${errors}${skeletonHint}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;

throw new Error(errorMessage);
}
101 changes: 101 additions & 0 deletions packages/ai/test/anthropic-alignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,107 @@ describe("Anthropic request fingerprint alignment", () => {
expect(payload.system?.some(block => block.text === claudeCodeSystemInstruction)).toBe(false);
expect(payload.tools?.[0]?.name).toBe("Read");
});
it("does not place message cache markers on synthetic developer tail context", async () => {
const payload = (await captureAnthropicPayload(
ANTHROPIC_MODEL,
{
systemPrompt: "Stay concise.",
messages: [
{ role: "user", content: "stable real user", timestamp: Date.now() },
{
role: "assistant",
content: [{ type: "text", text: "stable assistant" }],
api: "anthropic",
provider: "anthropic",
model: ANTHROPIC_MODEL.id,
usage: {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
},
{
role: "developer",
content: '<recalled-context now="volatile">tail</recalled-context>',
timestamp: Date.now(),
},
{ role: "developer", content: "[Assembly: volatile summary]", timestamp: Date.now() },
],
},
{ isOAuth: false },
)) as { messages: Array<{ role: string; content: string | Array<Record<string, unknown>> }> };

const messages = payload.messages;
const stableUser = messages.find(message => JSON.stringify(message.content).includes("stable real user"));
const recalledContext = messages.find(message => JSON.stringify(message.content).includes("recalled-context"));
const assemblySummary = messages.find(message => JSON.stringify(message.content).includes("[Assembly:"));

expect(stableUser?.content).toEqual([
{ type: "text", text: "stable real user", cache_control: { type: "ephemeral" } },
]);
expect(JSON.stringify(recalledContext?.content)).not.toContain("cache_control");
expect(JSON.stringify(assemblySummary?.content)).not.toContain("cache_control");
});

it("allows tool-result transcript messages to receive message cache markers", async () => {
const payload = (await captureAnthropicPayload(
ANTHROPIC_MODEL,
{
systemPrompt: "Stay concise.",
messages: [
{ role: "user", content: "read a file", timestamp: Date.now() },
{
role: "assistant",
content: [{ type: "toolCall", id: "toolu_123", name: "Read", arguments: { path: "README.md" } }],
api: "anthropic",
provider: "anthropic",
model: ANTHROPIC_MODEL.id,
usage: {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
},
{
role: "toolResult",
toolCallId: "toolu_123",
toolName: "Read",
content: [{ type: "text", text: "stable tool result" }],
isError: false,
timestamp: Date.now(),
},
{ role: "developer", content: "[Assembly: volatile summary]", timestamp: Date.now() },
],
},
{ isOAuth: false },
)) as { messages: Array<{ role: string; content: string | Array<Record<string, unknown>> }> };

const toolResultMessage = payload.messages.find(message =>
JSON.stringify(message.content).includes("stable tool result"),
);
const assemblySummary = payload.messages.find(message => JSON.stringify(message.content).includes("[Assembly:"));

expect(toolResultMessage?.content).toEqual([
{
type: "tool_result",
tool_use_id: "toolu_123",
content: "stable tool result",
is_error: false,
cache_control: { type: "ephemeral" },
},
]);
expect(JSON.stringify(assemblySummary?.content)).not.toContain("cache_control");
});

it("preserves valid caller metadata.user_id for OAuth requests", async () => {
const userId = generateClaudeCloakingUserId();
Expand Down
Loading
Loading