agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
188 lines • 9.17 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.prepareAnthropicReplay = exports.thinkingReplayBinding = void 0;
const replay_js_1 = require("../../thinking/replay.js");
const replayHistory_js_1 = require("../../thinking/replayHistory.js");
const canonicalJson_js_1 = require("../../lib/canonicalJson.js");
const anthropicMessageProjection_js_1 = require("./anthropicMessageProjection.js");
const anthropicReplayLayout_js_1 = require("./anthropicReplayLayout.js");
const anthropicModels_js_1 = require("./anthropicModels.js");
/** Cache annotations are ignored only at wire-owned positions, never in args. */
function withoutCache(value) {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return value;
const { cache_control: _cache, ...rest } = value;
return rest;
}
function systemOf(system) {
if (!Array.isArray(system))
return system ?? '';
const blocks = system.map(withoutCache);
if (blocks.every((b) => typeof b === 'object' && b !== null && 'type' in b && b.type === 'text' && 'text' in b)) {
return blocks.map((b) => b.text).join('');
}
return blocks;
}
function toolsOf(tools) {
return (tools ?? [])
.map(withoutCache)
.sort((a, b) => (0, canonicalJson_js_1.canonicalJson)(a).localeCompare((0, canonicalJson_js_1.canonicalJson)(b)));
}
function messagesWithoutThinking(messages) {
const kept = [];
for (const m of messages) {
if (typeof m.content === 'string') {
kept.push(m);
continue;
}
const content = m.content
.filter((b) => b.type !== 'thinking' && b.type !== 'redacted_thinking')
.map(withoutCache);
if (content.length > 0)
kept.push({ role: m.role, content });
}
return kept;
}
function thinkingSequence(messages) {
return messages.flatMap((m) => typeof m.content === 'string'
? []
: m.content.flatMap((b) => b.type === 'thinking' || b.type === 'redacted_thinking'
? [
(0, anthropicReplayLayout_js_1.replayDigest)(b.type === 'thinking' ? [b.type, b.signature] : [b.type, b.data], 'thinking-sequence-v1'),
]
: []));
}
/** Capture actual serialized request values BEFORE awaiting transport. */
function thinkingReplayBinding(body, model) {
const system = (0, canonicalJson_js_1.canonicalJson)(systemOf(body.system));
const tools = (0, canonicalJson_js_1.canonicalJson)(toolsOf(body.tools));
const messages = (0, canonicalJson_js_1.canonicalJson)(messagesWithoutThinking(body.messages));
const priorThinking = thinkingSequence(body.messages);
return (id) => ({
model,
salt: id,
system: (0, anthropicReplayLayout_js_1.replayDigest)(system, id),
tools: (0, anthropicReplayLayout_js_1.replayDigest)(tools, id),
messages: (0, anthropicReplayLayout_js_1.replayDigest)(messages, id),
priorThinking: [...priorThinking],
});
}
exports.thinkingReplayBinding = thinkingReplayBinding;
/** A supplied tool result is not completion. Only a witnessed end_turn is. */
function completedThrough(messages) {
const pending = new Set();
let completed = -1;
for (let i = 0; i < messages.length; i++) {
const m = messages[i];
if (m.role === 'assistant') {
for (const tool of m.toolCalls ?? [])
pending.add(tool.id);
if (m.replay?.version === 1 &&
m.replay.protocol === 'anthropic-messages' &&
typeof m.replay.id === 'string' &&
m.replay.id.length > 0 &&
m.replay.stopReason === 'end_turn' &&
pending.size === 0 &&
!m.toolCalls?.length)
completed = i;
}
else if (m.role === 'tool' && m.toolCallId !== undefined)
pending.delete(m.toolCallId);
}
return completed;
}
function suffixOf(current, original) {
return (current.length <= original.length &&
current.every((value, i) => value === original[original.length - current.length + i]));
}
function inconsistency(request, index, model) {
const message = request.messages[index];
const blocks = message.thinkingBlocks ?? [];
if (blocks.some((b) => typeof b.signature !== 'string' || b.signature.length === 0))
return 'missing-signature';
const replay = message.replay;
if (replay?.version !== 1 || replay.protocol !== 'anthropic-messages')
return undefined;
if (replay.thinkingRetired)
return blocks.length > 0 ? 'thinking-retired' : undefined;
if (replay.lossy)
return 'content-changed';
if ((0, anthropicReplayLayout_js_1.restoreAnthropicReplay)(message) === undefined)
return 'content-changed';
const binding = replay.binding;
// Cross-model signatures are server-owned. Locally assert the prefix rule
// only for a documented bound family replaying its own blocks; unknown
// aliases, legacy models and cross-family/account compatibility stay unknown.
if (binding === undefined ||
(0, anthropicModels_js_1.anthropicModelFacts)(model)?.thinkingPrefixBinding !== true ||
(0, anthropicModels_js_1.anthropicModelFamily)(model) !== (0, anthropicModels_js_1.anthropicModelFamily)(binding.model))
return undefined;
const before = (0, anthropicMessageProjection_js_1.toAnthropicMessages)(request.messages.slice(0, index));
if ((0, anthropicReplayLayout_js_1.replayDigest)((0, canonicalJson_js_1.canonicalJson)(systemOf(request.systemPrompt)), binding.salt) !== binding.system ||
(0, anthropicReplayLayout_js_1.replayDigest)((0, canonicalJson_js_1.canonicalJson)(toolsOf(request.tools?.map(anthropicMessageProjection_js_1.toAnthropicTool))), binding.salt) !==
binding.tools ||
(0, anthropicReplayLayout_js_1.replayDigest)((0, canonicalJson_js_1.canonicalJson)(messagesWithoutThinking(before)), binding.salt) !== binding.messages)
return 'prefix-changed';
if (!suffixOf(thinkingSequence(before), binding.priorThinking))
return 'sequence-changed';
return undefined;
}
/** Pure and idempotent. No caller mutation; all refusals precede any report. */
function prepareAnthropicReplay(request, model, provider) {
const completed = completedThrough(request.messages);
let prepared = request;
const omissions = [];
let retiredThrough = -1;
const retire = (index, reason) => {
const message = prepared.messages[index];
if (message.role !== 'assistant' || !message.thinkingBlocks?.length)
return;
const fingerprint = (0, replayHistory_js_1.replayMessageFingerprint)(message);
for (let b = 0; b < message.thinkingBlocks.length; b++)
omissions.push({
messageIndex: index,
blockIndex: b,
reason,
...(message.replay?.id && { replayId: message.replay.id }),
messageFingerprint: fingerprint,
});
const messages = [...prepared.messages];
messages[index] = (0, anthropicReplayLayout_js_1.omitAnthropicThinking)(message);
prepared = { ...prepared, messages };
};
for (let i = 0; i < request.messages.length; i++) {
const message = prepared.messages[i];
if (message.role !== 'assistant')
continue;
const hadThinking = message.replay?.version === 1 &&
message.replay.protocol === 'anthropic-messages' &&
message.replay.thinkingRetired !== true &&
Array.isArray(message.replay.layout) &&
message.replay.layout.some((part) => part.type === 'thinking');
if (!message.thinkingBlocks?.length && !hadThinking)
continue;
const reason = inconsistency(prepared, i, model);
if (reason === undefined)
continue;
// Unknown terminal status is not invented from a user nudge or tool result.
if (i > completed)
throw new replay_js_1.UnsafeThinkingReplayError(provider, model, i, reason);
// The caller already removed everything from this completed turn. We
// remove nothing and must not claim an omission or rewrite its evidence.
if (!message.thinkingBlocks?.length)
continue;
// Never create a middle gap, even if later blocks predate local evidence.
// For bound models, extend a permitted removal BACK to the oldest prefix,
// not forward into an active turn. Every extra removal is reported. All
// these earlier indices are <= completed; later exact content is retained.
if ((0, anthropicModels_js_1.anthropicModelFacts)(model)?.thinkingPrefixBinding === true) {
for (let earlier = retiredThrough + 1; earlier < i; earlier++)
retire(earlier, 'sequence-changed');
retiredThrough = i;
}
retire(i, reason);
}
return { provider, model, request: prepared, omissions };
}
exports.prepareAnthropicReplay = prepareAnthropicReplay;
//# sourceMappingURL=anthropicThinkingReplay.js.map