Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
cac705d
feat(agent): define the provider replay checkpoint contract (stage 1)
kojiwakayama Aug 29, 2026
b94dd10
fix(agent): type the replay checkpoint fixture and publish its error …
kojiwakayama Aug 29, 2026
2eddfb1
fix(agent): harden the replay contract per review findings
kojiwakayama Aug 29, 2026
0a8dcc7
Guard provider replay delivery scope
kwakayama Aug 29, 2026
7a5086b
fix(agent): reject provider replay checkpoints anchored to segmented …
kwakayama Aug 29, 2026
371ac15
docs(changelog): drop provider replay checkpoint entry with no behavi…
kwakayama Aug 29, 2026
66ccbac
refactor(runtime): drop redundant provider replay checkpoint re-parse
kwakayama Aug 29, 2026
05cd09e
Honor internal runtime import boundaries
kojiwakayama Aug 29, 2026
e0c4d45
Anchor provider replay only by exact message id
kojiwakayama Aug 29, 2026
4e0c853
Clarify split provider metadata failures
kojiwakayama Aug 29, 2026
5e21bde
Validate replay checkpoints before reuse
kojiwakayama Aug 29, 2026
4a64ac4
Stabilize remote project path expectations
kojiwakayama Aug 29, 2026
1e3442e
Keep replay preservation out of the chat monolith
kojiwakayama Aug 29, 2026
c194223
Keep opaque replay validation on the public transcript boundary
kojiwakayama Aug 29, 2026
1956a0c
Keep provider replay anchors intact through preparation
kojiwakayama Aug 29, 2026
d86cf1c
Keep replay preservation within the chat size budget
kojiwakayama Aug 29, 2026
62529ae
Prevent late workflow reads from escaping teardown
kojiwakayama Aug 29, 2026
0cbbd7a
Keep message preparation under the chat ratchet
kojiwakayama Aug 29, 2026
fe7c870
fix(agent): forward replay checkpoint ids through hosted preparation
kwakayama Aug 29, 2026
32642a3
Keep replay checkpoints inside typed contracts
kojiwakayama Aug 29, 2026
0d5aae3
fix(chat): carry providerExecuted through provider message conversion
kwakayama Aug 29, 2026
b981fc5
docs(api-reference): regenerate agent reference
kwakayama Aug 29, 2026
9231c65
fix(agent): normalize transcript text when validating replay checkpoints
kwakayama Aug 29, 2026
a35bd2c
revert(chat): stop carrying providerExecuted through message preparation
kwakayama Aug 29, 2026
3307af2
fix(typecheck): use the augmented field's value in the consumer fixture
kwakayama Aug 29, 2026
66c97aa
Keep replay checkpoints authoritative through preparation
kojiwakayama Aug 29, 2026
39b70ca
Separate split provider metadata from replay checkpoints
kojiwakayama Aug 29, 2026
f6c9a97
docs(api-reference): regenerate for the replay checkpoint contract co…
kwakayama Aug 29, 2026
1e974a9
fix(agent): accept every supported provider-result content shape
kwakayama Aug 29, 2026
83474bb
test(errors): record the provider-metadata-split-unsupported slug
kwakayama Aug 29, 2026
78213bd
Publish the split provider metadata error contract
kojiwakayama Aug 29, 2026
fe150c8
Fail closed on invalid replay result state
kojiwakayama Aug 29, 2026
835a216
Keep replay API reference current
kojiwakayama Aug 29, 2026
e2d8fda
Remove duplicate generated error entry
kojiwakayama Aug 29, 2026
8a6c961
Preserve checkpoint history through direct message prep
kojiwakayama Aug 29, 2026
73def1a
Preserve provider replay across durable resume boundaries
kojiwakayama Aug 29, 2026
a8185f7
fix(agent): validate provider result caller and raw message group count
kwakayama Aug 29, 2026
c1ef871
Preserve provider replay across durable resume boundaries
kojiwakayama Aug 29, 2026
7cf18a9
fix(agent): reject outer provider tool-result error block types
kwakayama Aug 29, 2026
a5d851d
test(chat): cover tool-pair repair and split replay metadata branches
kwakayama Aug 29, 2026
988879c
chore(docs): regenerate api reference source line anchors
kwakayama Aug 29, 2026
f09f2a5
fix(agent): close replay validation gaps
kojiwakayama Aug 29, 2026
4e1723f
docs(agent): refresh replay source anchors
kojiwakayama Aug 29, 2026
e4996dd
fix(agent): validate replay before transcript matching
kojiwakayama Aug 29, 2026
16a2aa9
Merge remote-tracking branch 'origin/main' into HEAD
kojiwakayama Aug 29, 2026
9dfa6d2
fix(agent): close replay fail-open paths
kojiwakayama Aug 29, 2026
5e6cd1e
fix(agent): keep replay stage 1 off the always-on message path
Aug 29, 2026
36f17e6
fix(agent): dedupe shared-id anchor groups and classify hosted runtim…
Aug 29, 2026
72744a7
test(agent): pin persisted provider-executed turns at the prompt boun…
Aug 29, 2026
3edc793
fix(agent): preserve replay across hosted preparation
kojiwakayama Aug 29, 2026
bbc4a5e
Merge remote-tracking branch 'origin/feat/522-provider-replay-stage1'…
kojiwakayama Aug 29, 2026
1ae3786
fix(lint): re-pin message-prep size ceiling after gated repair call site
Aug 29, 2026
02aac17
fix(agent): retain trailing replay anchors
kojiwakayama Aug 29, 2026
2637f89
Merge remote-tracking branch 'origin/feat/522-provider-replay-stage1'…
kojiwakayama Aug 29, 2026
b3cdb37
fix(chat): scope tool deduplication by turn
kojiwakayama Aug 29, 2026
9711d56
Merge origin/main into feat/522-provider-replay-stage1
Aug 29, 2026
fae901a
fix(agent): keep trailing replay anchors only for delivered checkpoints
Aug 29, 2026
fea6b72
Merge remote-tracking branch 'origin/feat/522-provider-replay-stage1'…
Aug 29, 2026
a480d0e
fix(lint): re-pin message-prep size ceiling after turn-scoped dedupe
Aug 29, 2026
1b59b59
fix(agent): distribute split replay metadata
kojiwakayama Aug 29, 2026
71bf634
Merge remote-tracking branch 'origin/feat/522-provider-replay-stage1'…
kojiwakayama Aug 29, 2026
1eaddaf
fix(agent): reject duplicate replay tool ids
kojiwakayama Aug 29, 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
241 changes: 121 additions & 120 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

118 changes: 59 additions & 59 deletions docs/api-reference/veryfront/chat.md

Large diffs are not rendered by default.

279 changes: 140 additions & 139 deletions docs/api-reference/veryfront/errors.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions docs/guides/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,13 @@ Default model has no matching provider credential.
- **HTTP status:** 400
- **What to do:** Set the agent's model to a provider you have a key for, or use model: "auto"

### provider-replay-checkpoint-invalid

Provider replay checkpoint is invalid.

- **HTTP status:** 500
- **What to do:** Verify the trusted source that resolved the run's provider replay checkpoints; do not retry with the same replay state

## General

Raised anywhere; these are not specific to one subsystem.
Expand Down
10 changes: 10 additions & 0 deletions extensions/ext-llm-anthropic/src/anthropic-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ function createSettledLegacyMcpPrompt(toolCallId: string) {
}

describe("anthropic-provider", () => {
it("keeps canonical provider identity when the runtime label is customized", () => {
const runtime = createAnthropicModelRuntime(
{ name: "private-anthropic-gateway" },
"claude-sonnet-4-6",
);

assertEquals(runtime.provider, "private-anthropic-gateway");
assertEquals(runtime.modelProvider, "anthropic");
});

it("creates an Anthropic-compatible language runtime without SDK helpers for generate", async () => {
let requestedUrl = "";
let requestedInit: RequestInit | undefined;
Expand Down
1 change: 1 addition & 0 deletions extensions/ext-llm-anthropic/src/anthropic-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export function createAnthropicModelRuntime(

return {
provider: providerName,
modelProvider: "anthropic",
modelId,
specificationVersion: "v3",
supportedUrls: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2988,6 +2988,73 @@ describe("ext-llm-anthropic/anthropic-request-builder", () => {
}]);
});

it("replays checkpoint-shaped signed thinking blocks byte-exact on the resume turn", () => {
// Shape delivered by a provider replay checkpoint (veryfront-issue-inbox#522):
// on Opus 4.7+/Sonnet 5/Fable 5 the thinking text comes back empty and the
// signature is the only carrier, so the rebuilt turn must echo the raw
// blocks unchanged beside the tool_use they accompanied.
const rawAssistantContent = [{
type: "thinking",
thinking: "",
signature: "sig-checkpoint-secret",
}, {
type: "redacted_thinking",
data: "redacted-checkpoint-secret",
}, {
type: "tool_use",
id: "local_lookup_2",
name: "local_lookup",
input: { query: "resume" },
}];
const prompt = [{
role: "user",
content: [{ type: "text", text: "Look this up" }],
}, {
role: "assistant",
content: [{
type: "reasoning",
text: "",
}, {
type: "tool-call",
toolCallId: "local_lookup_2",
toolName: "local_lookup",
input: { query: "resume" },
}],
providerMetadata: { anthropic: { rawAssistantMessages: [rawAssistantContent] } },
}, {
role: "tool",
content: [{
type: "tool-result",
toolCallId: "local_lookup_2",
toolName: "local_lookup",
output: { type: "json", value: { matches: 2 } },
}],
}] as unknown as RuntimePromptMessage[];

const body = buildAnthropicMessagesRequest(
"claude-sonnet-4-6",
"anthropic",
{ prompt, maxOutputTokens: 64 },
false,
createWarningCollector(),
);

assertEquals(body.messages, [{
role: "user",
content: [{ type: "text", text: "Look this up" }],
}, {
role: "assistant",
content: rawAssistantContent,
}, {
role: "user",
content: [{
type: "tool_result",
tool_use_id: "local_lookup_2",
content: '{"matches":2}',
}],
}]);
});

it("replays hidden signed thinking with canonical assistant content on resume", () => {
const rawAssistantContent = [{
type: "thinking",
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint/ban-chat-antipatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const FILE_SIZE_CEILINGS: Record<string, number> = {
// reconciliation). Not React components, so this map does not subject them
// to the antipattern ratchets above — it only pins their size.
"src/chat/conversation.ts": 557,
"src/chat/message-prep.ts": 2016,
"src/chat/message-prep.ts": 1823,
"src/chat/tool-replay-reconciliation.ts": 291,
"src/chat/message-part-parsing.ts": 264,
"src/chat/part-field-access.ts": 65,
Expand Down
4 changes: 3 additions & 1 deletion scripts/test/run-deno-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ if (import.meta.main) {
for (const [index, files] of batches.entries()) {
if (batches.length > 1) {
console.log(
`[test-suite] ${suite} batch ${index + 1}/${batches.length} (${files.length} files)`,
`[test-suite] ${suite} batch ${
index + 1
}/${batches.length} (${files.length} files)`,
);
}
const status = await new Deno.Command("deno", {
Expand Down
19 changes: 19 additions & 0 deletions scripts/typecheck/fixtures/chat-message-prep-augmentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Consumer fixture — documented `veryfront/chat/message-prep` declaration merging.
//
// Never executed. It exists so the consumer `tsc --noEmit` gate proves the
// published options contract remains an augmentable interface for downstream
// TypeScript consumers.
import type { PrepareProviderModelMessagesFromUiMessagesOptions } from "veryfront/chat/message-prep";

declare module "veryfront/chat/message-prep" {
interface PrepareProviderModelMessagesFromUiMessagesOptions {
downstreamTraceId?: string;
}
}

const options: PrepareProviderModelMessagesFromUiMessagesOptions = {
downstreamTraceId: "consumer-owned-field",
};

// The annotation is the proof: augmentation must widen the interface with a string.
export const upperTraceId: string | undefined = options.downstreamTraceId?.toUpperCase();
1 change: 1 addition & 0 deletions scripts/typecheck/tsconfig.consumer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"veryfront/ui": ["./npm/esm/src/react/components/ui/index.d.ts"],
"veryfront/ui/icons": ["./npm/esm/src/react/components/ui/icons/index.d.ts"],
"veryfront/chat": ["./npm/esm/src/chat/index.d.ts"],
"veryfront/chat/message-prep": ["./npm/esm/src/chat/message-prep.d.ts"],
"veryfront/chat/types": ["./npm/esm/src/chat/types.d.ts"],
"veryfront/chat/uploads": ["./npm/esm/src/chat/upload-handler.d.ts"],
"veryfront/schedule": ["./npm/esm/src/schedule/index.d.ts"],
Expand Down
52 changes: 52 additions & 0 deletions src/agent/ag-ui/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,4 +1075,56 @@ describe("agent/ag-ui-encoder tool-input lifecycle", () => {
// emitted it, so a normal tool failure must not emit a second.
assertEquals(events.map((entry) => entry.event), ["ToolCallResult"]);
});

it("never carries reasoning signatures or redacted data into AG-UI payloads", () => {
// Signed reasoning blocks are replayed through the private provider replay
// channel (veryfront-issue-inbox#522); the AG-UI transcript boundary must
// stay signature-free so signed material cannot reach display text.
const state = createAgUiEncoderState({ nowMs: null, epochMs: null });
const signature = "sig-secret-3c2b1a";
const redactedData = "redacted-secret-4d5e6f";

const events = [
...mapRuntimeStreamEventToAgUiEvents(state, {
type: "message-start",
messageId: "assistant-signed",
}),
...mapRuntimeStreamEventToAgUiEvents(state, {
type: "reasoning-start",
id: "reasoning-signed",
}),
...mapRuntimeStreamEventToAgUiEvents(state, {
type: "reasoning-delta",
id: "reasoning-signed",
delta: "",
}),
...mapRuntimeStreamEventToAgUiEvents(state, {
type: "reasoning-end",
id: "reasoning-signed",
signature,
redactedData,
}),
];

assertEquals(
events.map((entry) => entry.event),
["ReasoningMessageStart", "ReasoningMessageContent", "ReasoningMessageEnd"],
"reasoning lifecycle only",
);
const serialized = JSON.stringify(events);
assertEquals(serialized.includes(signature), false, "signature never leaves the runtime");
assertEquals(
serialized.includes(redactedData),
false,
"redacted data never leaves the runtime",
);
assertEquals(
events.at(-1),
{
event: "ReasoningMessageEnd",
payload: { messageId: "assistant-signed:reasoning:0" },
},
"reasoning end carries only its message anchor",
);
});
});
84 changes: 84 additions & 0 deletions src/agent/ag-ui/host-support.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { AG_UI_MAX_REQUEST_BODY_BYTES } from "./request-shared.ts";
import { DEFAULT_LIMITS } from "#veryfront/security/input-validation/types.ts";
import {
createAgUiRunErrorEvent,
createAgUiSseErrorResponse,
Expand Down Expand Up @@ -167,6 +168,89 @@ describe("agent/ag-ui-host-support", () => {
);
});

it("accepts private provider replay state outside the forwarded props budget", async () => {
const request = new Request("http://localhost/api/ag-ui", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{
id: "msg-1",
role: "user",
parts: [{ type: "text", text: "hello" }],
}],
forwardedProps: {
traceId: "trace-1",
},
serverResolvedProviderReplayCheckpoints: [{
version: 1,
messageId: "assistant-message-1",
provider: "anthropic",
providerBlocks: [{
type: "provider-block",
provider: "anthropic",
block: {
type: "thinking",
thinking: "x".repeat(220_000),
signature: "sig-private-large",
},
}],
providerBlockPositions: [0],
totalPartCount: 1,
}],
}),
});

const parsed = await parseAgUiRequest(request);

assertEquals(parsed.forwardedProps, { traceId: "trace-1" });
assertEquals(
Array.isArray(parsed.serverResolvedProviderReplayCheckpoints),
true,
"large replay payload must not be counted against forwardedProps",
);
});

it("rejects public AG-UI provider replay state above the generic request body limit", async () => {
const request = new Request("http://localhost/api/ag-ui", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{
id: "msg-1",
role: "user",
parts: [{ type: "text", text: "hello" }],
}],
serverResolvedProviderReplayCheckpoints: [{
version: 1,
messageId: "assistant-message-1",
provider: "anthropic",
providerBlocks: [{
type: "provider-block",
provider: "anthropic",
block: {
type: "thinking",
thinking: "x".repeat(1_049_600),
signature: "sig-private-large",
},
}],
providerBlockPositions: [0],
totalPartCount: 1,
}],
}),
});

const result = await parseAgUiRequestOrError(request);

assertInstanceOf(result, Response);
assertEquals(result.status, 413);
const body = await result.json();
assertEquals(body.error, "Invalid AG-UI request");
assertEquals(body.details, [{
path: [],
message: `Request body exceeds ${DEFAULT_LIMITS.maxBodySize} bytes`,
}]);
});

it("returns a 400 Response from parseAgUiRequestOrError for malformed JSON bodies", async () => {
const request = new Request("http://localhost/api/ag-ui", {
method: "POST",
Expand Down
1 change: 1 addition & 0 deletions src/agent/ag-ui/host-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const getAgUiRequestSchema = defineSchema((v) =>
(value) => value === undefined || isWithinJsonSizeLimit(value, MAX_FORWARDED_PROPS_BYTES),
{ message: "forwardedProps must be less than 192 KB" },
),
serverResolvedProviderReplayCheckpoints: v.unknown().optional(),
Comment thread
kwakayama marked this conversation as resolved.
model: v.string().optional(),
maxOutputTokens: v.number().int().positive().optional(),
})
Expand Down
2 changes: 2 additions & 0 deletions src/agent/hosted/ag-ui-chat-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export async function buildParsedHostedAgUiRequest(
model: chatContext.model,
allowDelegation: chatContext.allowDelegation,
forwardedProps: input.agUiInput.forwardedProps,
serverResolvedProviderReplayCheckpoints:
input.agUiInput.serverResolvedProviderReplayCheckpoints,
runtimeOverrides: chatContext.runtimeOverrides,
durableRootRun: undefined,
persistLatestUserMessageBeforeDurableRun: true,
Expand Down
Loading
Loading