@pi-oxide/pi-host-web 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -48
- package/pi_host_web.d.ts +21 -5
- package/pi_host_web.js +24 -1
- package/pi_host_web_bg.wasm +0 -0
- package/sdk/agent.ts +0 -274
- package/sdk/artifacts.ts +0 -35
- package/sdk/context.ts +0 -4
- package/sdk/errors.ts +0 -24
- package/sdk/events.ts +0 -52
- package/sdk/index.d.ts +0 -29
- package/sdk/index.js +0 -111
- package/sdk/index.ts +0 -53
- package/sdk/init.ts +0 -58
- package/sdk/internal/engine.ts +0 -614
- package/sdk/internal/events.ts +0 -241
- package/sdk/internal/providers/anthropic.ts +0 -440
- package/sdk/internal/providers/openai.ts +0 -177
- package/sdk/internal/providers/types.ts +0 -64
- package/sdk/internal/stores/indexedDb.ts +0 -24
- package/sdk/internal/stores/persistence.ts +0 -71
- package/sdk/internal/tools/artifact.ts +0 -24
- package/sdk/internal/tools/browser.ts +0 -449
- package/sdk/internal/tools/browserRuntime.ts +0 -48
- package/sdk/internal/tools/liveRuntime.ts +0 -151
- package/sdk/internal/tools/registry.ts +0 -174
- package/sdk/internal/tools/service.ts +0 -157
- package/sdk/model.ts +0 -35
- package/sdk/react/index.ts +0 -1
- package/sdk/react/useAgent.ts +0 -334
- package/sdk/snapshot.ts +0 -25
- package/sdk/stores.ts +0 -72
- package/sdk/tools.ts +0 -47
- package/sdk/types.ts +0 -252
package/sdk/internal/events.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// EventMapper — converts raw WASM AgentEvent[] into semantic SDK events.
|
|
2
|
-
// Accumulates RunState (messages, toolCalls, artifacts, usage).
|
|
3
|
-
// Emits all status states: idle, loading, thinking, calling_model, running_tool, saving, completed, aborted, failed.
|
|
4
|
-
// Artifact events from turn_end markers (new API: artifacts tracked via tool result details).
|
|
5
|
-
// Usage accumulation from model responses.
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
AgentEvent as RawAgentEvent,
|
|
9
|
-
AgentMessage as WasmAgentMessage,
|
|
10
|
-
Content,
|
|
11
|
-
} from "../../pi_host_web.js";
|
|
12
|
-
import type {
|
|
13
|
-
AgentMessage,
|
|
14
|
-
AgentContentBlock,
|
|
15
|
-
AgentToolRun,
|
|
16
|
-
AgentArtifactRef,
|
|
17
|
-
AgentStatus,
|
|
18
|
-
AgentRunResult,
|
|
19
|
-
TokenUsage,
|
|
20
|
-
} from "../types.ts";
|
|
21
|
-
|
|
22
|
-
export interface SemanticEvent {
|
|
23
|
-
type: string;
|
|
24
|
-
payload: unknown;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface RunState {
|
|
28
|
-
messages: AgentMessage[];
|
|
29
|
-
toolCalls: AgentToolRun[];
|
|
30
|
-
artifacts: AgentArtifactRef[];
|
|
31
|
-
usage: TokenUsage;
|
|
32
|
-
text: string;
|
|
33
|
-
currentMessage: AgentMessage | null;
|
|
34
|
-
currentTool: AgentToolRun | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class EventMapper {
|
|
38
|
-
createRunState(): RunState {
|
|
39
|
-
return {
|
|
40
|
-
messages: [],
|
|
41
|
-
toolCalls: [],
|
|
42
|
-
artifacts: [],
|
|
43
|
-
usage: { input: 0, output: 0, cache_read: 0, cache_write: 0, total_tokens: 0 },
|
|
44
|
-
text: "",
|
|
45
|
-
currentMessage: null,
|
|
46
|
-
currentTool: null,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
map(rawEvent: RawAgentEvent, state: RunState): SemanticEvent[] {
|
|
51
|
-
const events: SemanticEvent[] = [];
|
|
52
|
-
|
|
53
|
-
switch (rawEvent.type) {
|
|
54
|
-
case "agent_start": {
|
|
55
|
-
events.push({ type: "status", payload: { state: "loading", message: "Agent starting..." } as AgentStatus });
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
case "turn_start": {
|
|
60
|
-
events.push({ type: "status", payload: { state: "thinking", message: "Thinking..." } as AgentStatus });
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
case "message_start": {
|
|
65
|
-
const msg = this.convertWasmMessage(rawEvent.message);
|
|
66
|
-
state.currentMessage = msg;
|
|
67
|
-
state.messages.push(msg);
|
|
68
|
-
events.push({ type: "messageStart", payload: msg });
|
|
69
|
-
events.push({ type: "status", payload: { state: "thinking" } as AgentStatus });
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
case "message_update": {
|
|
74
|
-
const delta = rawEvent.delta;
|
|
75
|
-
if (delta.kind === "text_delta" && delta.text) {
|
|
76
|
-
state.text += delta.text;
|
|
77
|
-
events.push({ type: "text", payload: delta.text });
|
|
78
|
-
} else if (delta.kind === "tool_call_start" && delta.tool_call) {
|
|
79
|
-
// Track tool call in current message
|
|
80
|
-
} else if (delta.kind === "thinking_delta") {
|
|
81
|
-
events.push({ type: "status", payload: { state: "thinking", message: "Thinking..." } as AgentStatus });
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case "message_end": {
|
|
87
|
-
const msg = this.convertWasmMessage(rawEvent.message);
|
|
88
|
-
state.currentMessage = msg;
|
|
89
|
-
// Update the last message in state
|
|
90
|
-
const idx = state.messages.findIndex((m) => m.id === msg.id);
|
|
91
|
-
if (idx >= 0) {
|
|
92
|
-
state.messages[idx] = msg;
|
|
93
|
-
} else {
|
|
94
|
-
state.messages.push(msg);
|
|
95
|
-
}
|
|
96
|
-
events.push({ type: "messageEnd", payload: msg });
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
case "tool_execution_start": {
|
|
101
|
-
const tool: AgentToolRun = {
|
|
102
|
-
id: rawEvent.tool_call_id,
|
|
103
|
-
name: rawEvent.tool_name,
|
|
104
|
-
title: rawEvent.tool_name,
|
|
105
|
-
input: rawEvent.args ?? {},
|
|
106
|
-
status: "running",
|
|
107
|
-
startedAt: Date.now(),
|
|
108
|
-
};
|
|
109
|
-
state.currentTool = tool;
|
|
110
|
-
state.toolCalls.push(tool);
|
|
111
|
-
events.push({ type: "toolStart", payload: tool });
|
|
112
|
-
events.push({ type: "status", payload: { state: "running_tool", message: `Running ${rawEvent.tool_name}...` } as AgentStatus });
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
case "tool_execution_update": {
|
|
117
|
-
const tool = state.toolCalls.find((t) => t.id === rawEvent.tool_call_id);
|
|
118
|
-
if (tool) {
|
|
119
|
-
tool.output = (tool.output ?? "") + rawEvent.chunk;
|
|
120
|
-
events.push({ type: "toolUpdate", payload: tool });
|
|
121
|
-
}
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
case "tool_execution_end": {
|
|
126
|
-
const tool = state.toolCalls.find((t) => t.id === rawEvent.tool_call_id);
|
|
127
|
-
if (tool) {
|
|
128
|
-
tool.status = rawEvent.is_error ? "failed" : "completed";
|
|
129
|
-
tool.endedAt = Date.now();
|
|
130
|
-
// Extract output from result
|
|
131
|
-
const resultText = rawEvent.result.content
|
|
132
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
133
|
-
.map((c) => c.text)
|
|
134
|
-
.join("\n");
|
|
135
|
-
tool.output = resultText;
|
|
136
|
-
events.push({ type: "toolEnd", payload: tool });
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
case "tool_execution_cancelled": {
|
|
142
|
-
const tool = state.toolCalls.find((t) => t.id === rawEvent.tool_call_id);
|
|
143
|
-
if (tool) {
|
|
144
|
-
tool.status = "cancelled";
|
|
145
|
-
tool.endedAt = Date.now();
|
|
146
|
-
events.push({ type: "toolEnd", payload: tool });
|
|
147
|
-
}
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
case "turn_end": {
|
|
152
|
-
// Extract final message
|
|
153
|
-
const finalMsg = this.convertWasmMessage(rawEvent.message);
|
|
154
|
-
const idx = state.messages.findIndex((m) => m.id === finalMsg.id);
|
|
155
|
-
if (idx >= 0) {
|
|
156
|
-
state.messages[idx] = finalMsg;
|
|
157
|
-
} else {
|
|
158
|
-
state.messages.push(finalMsg);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Extract tool results
|
|
162
|
-
for (const tr of rawEvent.tool_results) {
|
|
163
|
-
const tool = state.toolCalls.find((t) => t.id === tr.tool_call_id);
|
|
164
|
-
if (tool) {
|
|
165
|
-
tool.status = tr.is_error ? "failed" : "completed";
|
|
166
|
-
tool.endedAt = Date.now();
|
|
167
|
-
const resultText = tr.content
|
|
168
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
169
|
-
.map((c) => c.text)
|
|
170
|
-
.join("\n");
|
|
171
|
-
tool.output = resultText;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
events.push({ type: "status", payload: { state: "completed" } as AgentStatus });
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
case "save_point": {
|
|
180
|
-
events.push({ type: "status", payload: { state: "saving", message: "Saving session..." } as AgentStatus });
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
case "settled": {
|
|
185
|
-
events.push({ type: "status", payload: { state: "completed" } as AgentStatus });
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
case "queue_update": {
|
|
190
|
-
// Debug channel — not a primary semantic event
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
case "agent_end": {
|
|
195
|
-
events.push({ type: "status", payload: { state: "idle" } as AgentStatus });
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return events;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
buildRunResult(state: RunState, turnResult: { aborted: boolean }): AgentRunResult {
|
|
204
|
-
if (turnResult.aborted) {
|
|
205
|
-
return {
|
|
206
|
-
status: "aborted",
|
|
207
|
-
text: state.text,
|
|
208
|
-
toolCalls: state.toolCalls,
|
|
209
|
-
artifacts: state.artifacts,
|
|
210
|
-
usage: state.usage,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
status: "completed",
|
|
216
|
-
message: state.currentMessage ?? undefined,
|
|
217
|
-
text: state.text,
|
|
218
|
-
toolCalls: state.toolCalls,
|
|
219
|
-
artifacts: state.artifacts,
|
|
220
|
-
usage: state.usage,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private convertWasmMessage(msg: WasmAgentMessage): AgentMessage {
|
|
225
|
-
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
226
|
-
return {
|
|
227
|
-
id,
|
|
228
|
-
role: msg.role,
|
|
229
|
-
content: msg.content.map((c) => this.convertContent(c)),
|
|
230
|
-
timestamp: Date.now(),
|
|
231
|
-
tool_call_id: msg.role === "tool_result" ? (msg as unknown as { tool_call_id: string }).tool_call_id : undefined,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private convertContent(c: Content): AgentContentBlock {
|
|
236
|
-
if (c.type === "text") return { type: "text", text: c.text };
|
|
237
|
-
if (c.type === "tool_call") return { type: "tool_call", id: c.id, name: c.name, arguments: c.arguments };
|
|
238
|
-
if (c.type === "image") return { type: "image", mimeType: c.media_type, data: c.data };
|
|
239
|
-
return { type: "text", text: "" };
|
|
240
|
-
}
|
|
241
|
-
}
|
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic Messages API adapter.
|
|
3
|
-
*
|
|
4
|
-
* Converts between Rust agent core message format and the Anthropic Messages API.
|
|
5
|
-
* Works with any Anthropic-compatible endpoint (including Fireworks.ai).
|
|
6
|
-
*
|
|
7
|
-
* This adapter does NOT stream. It makes a single request and returns the full
|
|
8
|
-
* response as chunks + final result, matching the existing AgentHost pattern.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ToolDefinition } from "../../../pi_host_web.js";
|
|
12
|
-
import type {
|
|
13
|
-
AgentMessageShape,
|
|
14
|
-
ContentBlock,
|
|
15
|
-
LlmRequest,
|
|
16
|
-
ProviderResult,
|
|
17
|
-
TokenUsage,
|
|
18
|
-
} from "./types.ts";
|
|
19
|
-
|
|
20
|
-
// --- Anthropic API types ---
|
|
21
|
-
|
|
22
|
-
interface AnthropicMessage {
|
|
23
|
-
role: "user" | "assistant";
|
|
24
|
-
content: string | AnthropicContentBlock[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type AnthropicContentBlock =
|
|
28
|
-
| { type: "text"; text: string }
|
|
29
|
-
| {
|
|
30
|
-
type: "tool_use";
|
|
31
|
-
id: string;
|
|
32
|
-
name: string;
|
|
33
|
-
input: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
| {
|
|
36
|
-
type: "tool_result";
|
|
37
|
-
tool_use_id: string;
|
|
38
|
-
content: string | AnthropicContentBlock[];
|
|
39
|
-
is_error?: boolean;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
interface AnthropicTool {
|
|
43
|
-
name: string;
|
|
44
|
-
description: string;
|
|
45
|
-
input_schema: object;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface AnthropicResponse {
|
|
49
|
-
id: string;
|
|
50
|
-
type: "message";
|
|
51
|
-
role: "assistant";
|
|
52
|
-
content: AnthropicContentBlock[];
|
|
53
|
-
model: string;
|
|
54
|
-
stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | null;
|
|
55
|
-
usage: {
|
|
56
|
-
input_tokens: number;
|
|
57
|
-
output_tokens: number;
|
|
58
|
-
cache_creation_input_tokens?: number;
|
|
59
|
-
cache_read_input_tokens?: number;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface AnthropicError {
|
|
64
|
-
type: "error";
|
|
65
|
-
error: { type: string; message: string };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// --- Conversion: Rust messages -> Anthropic messages ---
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Convert Rust agent messages to Anthropic Messages API format.
|
|
72
|
-
*
|
|
73
|
-
* Anthropic requires that multiple tool_result responses to a single assistant
|
|
74
|
-
* message with multiple tool_use blocks be grouped into ONE user message
|
|
75
|
-
* containing an array of tool_result blocks. Sending separate consecutive
|
|
76
|
-
* user messages each with a single tool_result block triggers an API error:
|
|
77
|
-
* "messages: Unexpected role change from user to user"
|
|
78
|
-
*
|
|
79
|
-
* See: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#handling-tool-use-and-tool-result-content-blocks
|
|
80
|
-
*/
|
|
81
|
-
export function convertMessages(
|
|
82
|
-
messages: AgentMessageShape[],
|
|
83
|
-
): AnthropicMessage[] {
|
|
84
|
-
const result: AnthropicMessage[] = [];
|
|
85
|
-
|
|
86
|
-
let i = 0;
|
|
87
|
-
while (i < messages.length) {
|
|
88
|
-
const msg = messages[i];
|
|
89
|
-
|
|
90
|
-
switch (msg.role) {
|
|
91
|
-
case "user": {
|
|
92
|
-
const text = extractText(msg.content);
|
|
93
|
-
result.push({ role: "user", content: text });
|
|
94
|
-
i++;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
case "assistant": {
|
|
98
|
-
const blocks: AnthropicContentBlock[] = [];
|
|
99
|
-
for (const block of msg.content) {
|
|
100
|
-
if (block.type === "text" && block.text !== undefined) {
|
|
101
|
-
blocks.push({ type: "text", text: block.text });
|
|
102
|
-
} else if (block.type === "tool_call" && block.id && block.name) {
|
|
103
|
-
blocks.push({
|
|
104
|
-
type: "tool_use",
|
|
105
|
-
id: block.id,
|
|
106
|
-
name: block.name,
|
|
107
|
-
input: block.arguments ?? {},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
result.push({ role: "assistant", content: blocks });
|
|
112
|
-
i++;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case "tool_result": {
|
|
116
|
-
// Gather consecutive tool_result messages into a single user message.
|
|
117
|
-
// Anthropic requires all tool_results for a given assistant turn to be
|
|
118
|
-
// in one user message with an array of tool_result content blocks.
|
|
119
|
-
const toolResults: AnthropicContentBlock[] = [];
|
|
120
|
-
while (i < messages.length && messages[i].role === "tool_result") {
|
|
121
|
-
const tr = messages[i] as Extract<
|
|
122
|
-
AgentMessageShape,
|
|
123
|
-
{ role: "tool_result" }
|
|
124
|
-
>;
|
|
125
|
-
const text = extractText(tr.content);
|
|
126
|
-
toolResults.push({
|
|
127
|
-
type: "tool_result",
|
|
128
|
-
tool_use_id: tr.tool_call_id,
|
|
129
|
-
content: text,
|
|
130
|
-
is_error: tr.is_error,
|
|
131
|
-
});
|
|
132
|
-
i++;
|
|
133
|
-
}
|
|
134
|
-
result.push({ role: "user", content: toolResults });
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// --- Conversion: Rust tool definitions -> Anthropic tools ---
|
|
144
|
-
|
|
145
|
-
export function convertTools(tools: ToolDefinition[]): AnthropicTool[] {
|
|
146
|
-
return tools.map((t) => ({
|
|
147
|
-
name: t.name,
|
|
148
|
-
description: `${t.label}: ${t.description}`,
|
|
149
|
-
input_schema: t.parameters,
|
|
150
|
-
}));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- Conversion: Anthropic response -> Rust LlmResult ---
|
|
154
|
-
|
|
155
|
-
export function convertResponse(
|
|
156
|
-
resp: AnthropicResponse,
|
|
157
|
-
providerName: string,
|
|
158
|
-
modelId: string,
|
|
159
|
-
): { llmResult: object; chunks: object[] } {
|
|
160
|
-
const content: ContentBlock[] = [];
|
|
161
|
-
|
|
162
|
-
for (const block of resp.content) {
|
|
163
|
-
if (block.type === "text") {
|
|
164
|
-
content.push({ type: "text", text: block.text });
|
|
165
|
-
} else if (block.type === "tool_use") {
|
|
166
|
-
content.push({
|
|
167
|
-
type: "tool_call",
|
|
168
|
-
id: block.id,
|
|
169
|
-
name: block.name,
|
|
170
|
-
arguments: block.input,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const stopReason = resp.stop_reason === "tool_use" ? "tool_use" : "end_turn";
|
|
176
|
-
|
|
177
|
-
const usage: TokenUsage = {
|
|
178
|
-
input: resp.usage.input_tokens,
|
|
179
|
-
output: resp.usage.output_tokens,
|
|
180
|
-
cache_read: resp.usage.cache_read_input_tokens ?? 0,
|
|
181
|
-
cache_write: resp.usage.cache_creation_input_tokens ?? 0,
|
|
182
|
-
total_tokens: resp.usage.input_tokens + resp.usage.output_tokens,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const assistantMsg = {
|
|
186
|
-
content,
|
|
187
|
-
api: providerName,
|
|
188
|
-
provider: providerName,
|
|
189
|
-
model: modelId,
|
|
190
|
-
stop_reason: stopReason,
|
|
191
|
-
error_message: null,
|
|
192
|
-
timestamp: Date.now(),
|
|
193
|
-
usage,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// Build streaming chunks: a Start chunk + TextDelta chunks for each text block
|
|
197
|
-
const chunks: object[] = [];
|
|
198
|
-
chunks.push({
|
|
199
|
-
kind: "start",
|
|
200
|
-
content: [{ type: "text", text: "" }],
|
|
201
|
-
api: providerName,
|
|
202
|
-
provider: providerName,
|
|
203
|
-
model: modelId,
|
|
204
|
-
stop_reason: stopReason,
|
|
205
|
-
error_message: null,
|
|
206
|
-
timestamp: 0,
|
|
207
|
-
usage: {
|
|
208
|
-
input: 0,
|
|
209
|
-
output: 0,
|
|
210
|
-
cache_read: 0,
|
|
211
|
-
cache_write: 0,
|
|
212
|
-
total_tokens: 0,
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
for (const block of resp.content) {
|
|
217
|
-
if (block.type === "text" && block.text.length > 0) {
|
|
218
|
-
chunks.push({ kind: "text_delta", text: block.text });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
llmResult: { Ok: assistantMsg },
|
|
224
|
-
chunks,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- Main call ---
|
|
229
|
-
|
|
230
|
-
export interface AnthropicConfig {
|
|
231
|
-
apiKey: string;
|
|
232
|
-
baseUrl: string;
|
|
233
|
-
model: string;
|
|
234
|
-
maxTokens?: number;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Call the Anthropic Messages API and return a ProviderResult.
|
|
239
|
-
*/
|
|
240
|
-
export async function callAnthropic(
|
|
241
|
-
request: LlmRequest,
|
|
242
|
-
config: AnthropicConfig,
|
|
243
|
-
): Promise<ProviderResult> {
|
|
244
|
-
const log: string[] = [];
|
|
245
|
-
const providerName = "anthropic";
|
|
246
|
-
|
|
247
|
-
const body = {
|
|
248
|
-
model: config.model,
|
|
249
|
-
max_tokens: config.maxTokens ?? 4096,
|
|
250
|
-
system: request.system_prompt,
|
|
251
|
-
messages: convertMessages(request.messages),
|
|
252
|
-
tools: convertTools(request.tools),
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
log.push(
|
|
256
|
-
`anthropic_request: model=${config.model}, messages=${body.messages.length}, tools=${body.tools.length}`,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
let resp: Response;
|
|
260
|
-
try {
|
|
261
|
-
resp = await fetch(`${config.baseUrl}/v1/messages`, {
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: {
|
|
264
|
-
"Content-Type": "application/json",
|
|
265
|
-
"x-api-key": config.apiKey,
|
|
266
|
-
"anthropic-version": "2023-06-01",
|
|
267
|
-
},
|
|
268
|
-
body: JSON.stringify(body),
|
|
269
|
-
});
|
|
270
|
-
} catch (err) {
|
|
271
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
272
|
-
log.push(`anthropic_network_error: ${msg}`);
|
|
273
|
-
return {
|
|
274
|
-
llmResult: {
|
|
275
|
-
Err: { error: { code: "network_error", message: msg }, aborted: false },
|
|
276
|
-
},
|
|
277
|
-
chunks: [],
|
|
278
|
-
log,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!resp.ok) {
|
|
283
|
-
let errorBody: AnthropicError | null = null;
|
|
284
|
-
try {
|
|
285
|
-
errorBody = (await resp.json()) as AnthropicError;
|
|
286
|
-
} catch {
|
|
287
|
-
// ignore parse failure
|
|
288
|
-
}
|
|
289
|
-
const code = `http_${resp.status}`;
|
|
290
|
-
const message =
|
|
291
|
-
errorBody?.error?.message ?? `HTTP ${resp.status}: ${resp.statusText}`;
|
|
292
|
-
log.push(`anthropic_error: ${code} - ${message}`);
|
|
293
|
-
return {
|
|
294
|
-
llmResult: {
|
|
295
|
-
Err: { error: { code, message }, aborted: false },
|
|
296
|
-
},
|
|
297
|
-
chunks: [],
|
|
298
|
-
log,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const data = (await resp.json()) as AnthropicResponse;
|
|
303
|
-
log.push(
|
|
304
|
-
`anthropic_response: stop_reason=${data.stop_reason}, content_blocks=${data.content.length}`,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const { llmResult, chunks } = convertResponse(
|
|
308
|
-
data,
|
|
309
|
-
providerName,
|
|
310
|
-
config.model,
|
|
311
|
-
);
|
|
312
|
-
return { llmResult, chunks, log };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// --- SDK factory ---
|
|
316
|
-
|
|
317
|
-
import type { AgentModel, ModelRequest, ModelResponse } from "../../types.ts";
|
|
318
|
-
import { createAgentError } from "../../errors.ts";
|
|
319
|
-
|
|
320
|
-
export function anthropic(config: {
|
|
321
|
-
apiKey: string;
|
|
322
|
-
model: string;
|
|
323
|
-
baseUrl?: string;
|
|
324
|
-
maxTokens?: number;
|
|
325
|
-
}): AgentModel {
|
|
326
|
-
const anthropicConfig: AnthropicConfig = {
|
|
327
|
-
apiKey: config.apiKey,
|
|
328
|
-
baseUrl: config.baseUrl ?? "https://api.anthropic.com",
|
|
329
|
-
model: config.model,
|
|
330
|
-
maxTokens: config.maxTokens,
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
id: config.model,
|
|
335
|
-
contextWindow: 200000,
|
|
336
|
-
maxTokens: config.maxTokens ?? 4096,
|
|
337
|
-
capabilities: {
|
|
338
|
-
vision: config.model.includes("vision") || config.model.startsWith("claude-3-5"),
|
|
339
|
-
jsonMode: true,
|
|
340
|
-
functionCalling: true,
|
|
341
|
-
streaming: true,
|
|
342
|
-
},
|
|
343
|
-
async generate(request: ModelRequest): Promise<ModelResponse> {
|
|
344
|
-
const llmRequest = {
|
|
345
|
-
system_prompt: request.instructions,
|
|
346
|
-
messages: request.messages.map((msg): AgentMessageShape => {
|
|
347
|
-
const content = msg.content.map((c): ContentBlock => {
|
|
348
|
-
if (c.type === "text") return { type: "text", text: c.text };
|
|
349
|
-
if (c.type === "tool_call") return { type: "tool_call", id: c.id, name: c.name, arguments: c.arguments as Record<string, unknown> };
|
|
350
|
-
if (c.type === "image") return { type: "image", media_type: c.mimeType, data: c.data };
|
|
351
|
-
return { type: "text", text: "" };
|
|
352
|
-
});
|
|
353
|
-
const timestamp = msg.timestamp ?? Date.now();
|
|
354
|
-
if (msg.role === "user") {
|
|
355
|
-
return { role: "user", content, timestamp };
|
|
356
|
-
}
|
|
357
|
-
if (msg.role === "assistant") {
|
|
358
|
-
return {
|
|
359
|
-
role: "assistant",
|
|
360
|
-
content,
|
|
361
|
-
api: "sdk",
|
|
362
|
-
provider: "sdk",
|
|
363
|
-
model: request.tools[0]?.name ?? "sdk-model",
|
|
364
|
-
stop_reason: "end_turn",
|
|
365
|
-
error_message: null,
|
|
366
|
-
timestamp,
|
|
367
|
-
usage: { input: 0, output: 0, cache_read: 0, cache_write: 0, total_tokens: 0 },
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
// tool_result
|
|
371
|
-
return {
|
|
372
|
-
role: "tool_result",
|
|
373
|
-
tool_call_id: msg.tool_call_id ?? "",
|
|
374
|
-
tool_name: msg.content.find((c) => c.type === "text")?.text?.slice(0, 50) ?? "unknown",
|
|
375
|
-
content,
|
|
376
|
-
details: {},
|
|
377
|
-
is_error: false,
|
|
378
|
-
timestamp,
|
|
379
|
-
};
|
|
380
|
-
}),
|
|
381
|
-
tools: request.tools.map((t) => ({
|
|
382
|
-
name: t.name,
|
|
383
|
-
label: t.name,
|
|
384
|
-
description: t.description,
|
|
385
|
-
parameters: t.inputSchema as object,
|
|
386
|
-
execution_mode: "parallel" as const,
|
|
387
|
-
})),
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
const result = await callAnthropic(llmRequest, anthropicConfig);
|
|
392
|
-
|
|
393
|
-
if ("Err" in result.llmResult) {
|
|
394
|
-
const err = (result.llmResult as { Err: { error: { code: string; message: string } } }).Err.error;
|
|
395
|
-
throw createAgentError(
|
|
396
|
-
err.code === "network_error" ? "model_unavailable" :
|
|
397
|
-
err.code.startsWith("http_401") ? "model_auth_failed" :
|
|
398
|
-
err.code.startsWith("http_429") ? "model_rate_limited" :
|
|
399
|
-
"model_unavailable",
|
|
400
|
-
err.message,
|
|
401
|
-
{ recoverable: err.code === "http_429" },
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const ok = (result.llmResult as { Ok: object }).Ok as {
|
|
406
|
-
content: Array<{ type: string; text?: string; id?: string; name?: string; arguments?: unknown }>;
|
|
407
|
-
stop_reason: string;
|
|
408
|
-
usage?: { input: number; output: number; cache_read: number; cache_write: number; total_tokens: number };
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
return {
|
|
412
|
-
content: ok.content.map((b) => {
|
|
413
|
-
if (b.type === "text") return { type: "text", text: b.text ?? "" };
|
|
414
|
-
if (b.type === "tool_call") return { type: "tool_call", id: b.id ?? "", name: b.name ?? "", arguments: b.arguments ?? {} };
|
|
415
|
-
return { type: "text", text: "" };
|
|
416
|
-
}),
|
|
417
|
-
stopReason: ok.stop_reason === "tool_use" ? "tool_call" : "end",
|
|
418
|
-
usage: ok.usage,
|
|
419
|
-
model: config.model,
|
|
420
|
-
raw: result,
|
|
421
|
-
};
|
|
422
|
-
} catch (e) {
|
|
423
|
-
if (e && typeof e === "object" && "code" in e) throw e;
|
|
424
|
-
throw createAgentError("model_unavailable", e instanceof Error ? e.message : String(e), { cause: e, recoverable: false });
|
|
425
|
-
}
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// --- Helpers ---
|
|
431
|
-
|
|
432
|
-
function extractText(content: ContentBlock[]): string {
|
|
433
|
-
return content
|
|
434
|
-
.filter(
|
|
435
|
-
(b): b is typeof b & { text: string } =>
|
|
436
|
-
b.type === "text" && b.text !== undefined,
|
|
437
|
-
)
|
|
438
|
-
.map((b) => b.text)
|
|
439
|
-
.join("\n");
|
|
440
|
-
}
|