-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathhistory.ts
More file actions
190 lines (177 loc) · 6.42 KB
/
Copy pathhistory.ts
File metadata and controls
190 lines (177 loc) · 6.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import type Anthropic from "@anthropic-ai/sdk";
import { isStructuredOutputRetryFeedback } from "../../constants.ts";
import { normalizeToolName } from "../../tool.ts";
import type { ChatItem } from "../../types.ts";
import { ensureToolInputObject } from "../shared/tools.ts";
import type { AnthropicToolMap } from "./utils.ts";
const supportedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"];
// TODO: drop signature after 10 minutes or whatever
// Mapping between thinking response and signature since signature is meaningless cross-provider and we technically only need to include thinking for the one step
const signatureMap = new Map<string, string>();
export function rememberAnthropicReasoningSignature(content: string, signature: string) {
signatureMap.set(content, signature);
}
/**
* Marks the tail of the conversation as a cache breakpoint, so the next call in
* an agent turn reads this turn's prefix instead of reprocessing it. Caching is a
* prefix match, so this must be the final mutation of the history.
*/
export function applyAnthropicCacheBreakpoint(
history: Anthropic.Messages.MessageParam[],
cacheControl: Anthropic.Messages.CacheControlEphemeral,
) {
const content = history.at(-1)?.content;
if (!content || typeof content === "string") return;
const block = content.at(-1);
// Thinking blocks reject a breakpoint; the system/tools prefix still caches without one here.
if (!block || block.type === "thinking" || block.type === "redacted_thinking") return;
block.cache_control = cacheControl;
}
export async function getAnthropicHistory(options: {
history: ChatItem[];
normalizedTools: AnthropicToolMap[];
signal: AbortSignal;
}): Promise<Anthropic.Messages.MessageParam[]> {
const anthropicHistory: Anthropic.Messages.MessageParam[] = [];
let anthropicToolFileBuffer: Anthropic.Messages.MessageParam[] = [];
// Put all of the history in place
for (const historyItem of options.history) {
switch (historyItem.type) {
case "input_text": {
// first, flush tool buffer
anthropicHistory.push(...anthropicToolFileBuffer);
anthropicToolFileBuffer = [];
// next, append message
anthropicHistory.push({
role: "user",
content: [{ type: "text", text: historyItem.content }],
});
break;
}
case "output_text": {
// first, flush tool buffer
anthropicHistory.push(...anthropicToolFileBuffer);
anthropicToolFileBuffer = [];
// next, append message
anthropicHistory.push({
role: isStructuredOutputRetryFeedback(historyItem.content) ? "user" : "assistant",
content: [{ type: "text", text: historyItem.content }],
});
break;
}
case "context_summary": {
anthropicHistory.push(...anthropicToolFileBuffer);
anthropicToolFileBuffer = [];
anthropicHistory.push({
role: "user",
content: [{ type: "text", text: historyItem.content }],
});
break;
}
case "tool_use": {
const tool = options.normalizedTools.find((tool) => tool.original.name === historyItem.kind);
let input: unknown;
try {
const content = historyItem.content ? JSON.parse(historyItem.content) : {};
input = tool?.compatibility ? tool.compatibility.toProvider(content) : content;
} catch {
// A turn whose arguments never parsed still has to replay as a legal tool_use block.
// Throwing here would make the whole conversation unsendable to any model.
input = historyItem.content;
}
anthropicHistory.push({
role: "assistant",
content: [{
type: "tool_use",
id: historyItem.tool_use_id,
name: tool?.anthropic.name ?? normalizeToolName(historyItem.kind),
input: ensureToolInputObject(input),
}],
});
break;
}
case "tool_result_text": {
anthropicHistory.push({
role: "user",
content: [{
type: "tool_result",
tool_use_id: historyItem.tool_use_id,
content: historyItem.content,
is_error: historyItem.content.startsWith("Error: "),
}],
});
break;
}
case "output_reasoning": {
// first, flush tool buffer
anthropicHistory.push(...anthropicToolFileBuffer);
anthropicToolFileBuffer = [];
// next append reasoning
const signature = signatureMap.get(historyItem.content);
if (signature) {
anthropicHistory.push({
role: "assistant",
content: [{
type: "thinking",
thinking: historyItem.content,
signature,
}],
});
} else {
// no-op :( nothing we can do
}
break;
}
case "input_file":
case "tool_result_file": {
const pushBuffer = historyItem.type === "input_file" ? anthropicHistory : anthropicToolFileBuffer;
if (supportedImageMimeTypes.includes(historyItem.kind)) {
pushBuffer.push({
role: "user",
content: [{
type: "image",
source: {
type: "url",
url: historyItem.content,
},
}],
});
} else if (historyItem.kind === "application/pdf") {
pushBuffer.push({
role: "user",
content: [
{
type: "document",
source: {
type: "url",
url: historyItem.content,
},
},
],
});
} else if (historyItem.kind.startsWith("text/")) {
const req = await fetch(historyItem.content, { signal: options.signal });
const text = await req.text();
pushBuffer.push({
role: "user",
content: [
{
type: "text",
text: `<ant-file>${text}</ant-file>`,
},
],
});
} else {
throw new Error(`Anthropic models don't support the following media type: ${historyItem.kind}`);
}
break;
}
default:
historyItem satisfies never;
}
}
// Flush remaining toolFileBuffer
anthropicHistory.push(...anthropicToolFileBuffer);
anthropicToolFileBuffer = [];
return anthropicHistory;
}