@vietor/agent-core 0.7.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/LICENSE +21 -0
- package/README.md +849 -0
- package/dist/create-session.d.ts +4 -0
- package/dist/create-session.js +51 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.js +200 -0
- package/dist/llm/base.d.ts +10 -0
- package/dist/llm/base.js +12 -0
- package/dist/llm/client.d.ts +4 -0
- package/dist/llm/client.js +61 -0
- package/dist/llm/completions.d.ts +8 -0
- package/dist/llm/completions.js +88 -0
- package/dist/llm/messages.d.ts +50 -0
- package/dist/llm/messages.js +28 -0
- package/dist/llm/responses.d.ts +14 -0
- package/dist/llm/responses.js +130 -0
- package/dist/llm/types.d.ts +40 -0
- package/dist/llm/types.js +1 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +53 -0
- package/dist/mcp/manager.d.ts +18 -0
- package/dist/mcp/manager.js +155 -0
- package/dist/mcp/types.d.ts +26 -0
- package/dist/mcp/types.js +1 -0
- package/dist/runtime/agent.d.ts +56 -0
- package/dist/runtime/agent.js +253 -0
- package/dist/runtime/events.d.ts +69 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/prompts.d.ts +5 -0
- package/dist/runtime/prompts.js +45 -0
- package/dist/runtime/session-messages.d.ts +43 -0
- package/dist/runtime/session-messages.js +174 -0
- package/dist/runtime/session.d.ts +102 -0
- package/dist/runtime/session.js +375 -0
- package/dist/runtime/sub-agent-runner.d.ts +18 -0
- package/dist/runtime/sub-agent-runner.js +26 -0
- package/dist/runtime/timeline.d.ts +21 -0
- package/dist/runtime/timeline.js +146 -0
- package/dist/runtime/todo-store.d.ts +8 -0
- package/dist/runtime/todo-store.js +15 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +43 -0
- package/dist/tools/ask-user.d.ts +3 -0
- package/dist/tools/ask-user.js +26 -0
- package/dist/tools/file-edit.d.ts +2 -0
- package/dist/tools/file-edit.js +43 -0
- package/dist/tools/file-read.d.ts +2 -0
- package/dist/tools/file-read.js +93 -0
- package/dist/tools/file-write.d.ts +2 -0
- package/dist/tools/file-write.js +25 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +31 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +67 -0
- package/dist/tools/registry.d.ts +30 -0
- package/dist/tools/registry.js +107 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +57 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/skill.js +30 -0
- package/dist/tools/sub-agent.d.ts +7 -0
- package/dist/tools/sub-agent.js +81 -0
- package/dist/tools/todo-write.d.ts +3 -0
- package/dist/tools/todo-write.js +72 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/web-fetch.d.ts +2 -0
- package/dist/tools/web-fetch.js +104 -0
- package/dist/util/async.d.ts +19 -0
- package/dist/util/async.js +93 -0
- package/dist/util/constants.d.ts +25 -0
- package/dist/util/constants.js +27 -0
- package/dist/util/emitter.d.ts +5 -0
- package/dist/util/emitter.js +15 -0
- package/dist/util/file.d.ts +3 -0
- package/dist/util/file.js +19 -0
- package/dist/util/html.d.ts +1 -0
- package/dist/util/html.js +14 -0
- package/dist/util/index.d.ts +7 -0
- package/dist/util/index.js +7 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +31 -0
- package/dist/util/ripgrep.d.ts +10 -0
- package/dist/util/ripgrep.js +34 -0
- package/dist/util/subprocess.d.ts +15 -0
- package/dist/util/subprocess.js +113 -0
- package/dist/util/text.d.ts +15 -0
- package/dist/util/text.js +72 -0
- package/package.json +52 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Session } from "./runtime/session.js";
|
|
2
|
+
import type { SessionOptions } from "./runtime/session.js";
|
|
3
|
+
export declare const SYSTEM_PROMPT_BOUNDARY = "\n\n---\n<!-- SYSTEM_PROMPT_BOUNDARY -->\n\n";
|
|
4
|
+
export declare function createSession(opts: SessionOptions): Promise<Session>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createLLM } from "./llm/client.js";
|
|
2
|
+
import { Session } from "./runtime/session.js";
|
|
3
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
4
|
+
import { MCPServerManager } from "./mcp/manager.js";
|
|
5
|
+
import { TOOL_USE_PROMPT } from "./runtime/prompts.js";
|
|
6
|
+
import { DEFAULT_MAX_TURNS } from "./util/constants.js";
|
|
7
|
+
import { TODO_WRITE_GUIDANCE } from "./tools/todo-write.js";
|
|
8
|
+
import { ASK_USER_GUIDANCE } from "./tools/ask-user.js";
|
|
9
|
+
import { SUB_AGENT_GUIDANCE } from "./tools/sub-agent.js";
|
|
10
|
+
export const SYSTEM_PROMPT_BOUNDARY = '\n\n---\n<!-- SYSTEM_PROMPT_BOUNDARY -->\n\n';
|
|
11
|
+
function contextLimitFor(maxInputTokens) {
|
|
12
|
+
return Math.floor(maxInputTokens * 0.75);
|
|
13
|
+
}
|
|
14
|
+
function buildSystemPrompt(base, skills, builtInTools, maxTurns) {
|
|
15
|
+
const parts = [base];
|
|
16
|
+
const toolUseLines = [TOOL_USE_PROMPT, `- Turn budget: ${maxTurns} tool-calling turns per run.`];
|
|
17
|
+
if (typeof builtInTools === "object") {
|
|
18
|
+
if (builtInTools.todoWrite)
|
|
19
|
+
toolUseLines.push(TODO_WRITE_GUIDANCE);
|
|
20
|
+
if (builtInTools.askUser)
|
|
21
|
+
toolUseLines.push(ASK_USER_GUIDANCE);
|
|
22
|
+
if (builtInTools.subAgent)
|
|
23
|
+
toolUseLines.push(SUB_AGENT_GUIDANCE);
|
|
24
|
+
}
|
|
25
|
+
parts.push(toolUseLines.join("\n"));
|
|
26
|
+
if (skills?.length) {
|
|
27
|
+
const lines = skills.map((s) => `- \`${s.name}\`: ${s.description || "no description"}`);
|
|
28
|
+
parts.push(["Available skills (call via the Skill tool):", ...lines].join("\n"));
|
|
29
|
+
}
|
|
30
|
+
return parts.join(SYSTEM_PROMPT_BOUNDARY);
|
|
31
|
+
}
|
|
32
|
+
export async function createSession(opts) {
|
|
33
|
+
const llm = createLLM(opts.llm);
|
|
34
|
+
const tools = new ToolRegistry();
|
|
35
|
+
const mcp = new MCPServerManager(tools, opts.clientInfo ?? { name: "agent-core", version: "0.0.0" });
|
|
36
|
+
const session = new Session({
|
|
37
|
+
...opts,
|
|
38
|
+
systemPrompt: buildSystemPrompt(opts.systemPrompt, opts.skills, opts.builtInTools, opts.maxTurns ?? DEFAULT_MAX_TURNS),
|
|
39
|
+
llm,
|
|
40
|
+
tools,
|
|
41
|
+
mcp,
|
|
42
|
+
contextLimit: contextLimitFor(llm.maxInputTokens),
|
|
43
|
+
});
|
|
44
|
+
if (opts.tools) {
|
|
45
|
+
tools.registerAll(opts.tools);
|
|
46
|
+
}
|
|
47
|
+
if (opts.mcpServers) {
|
|
48
|
+
await session.connectMCP(opts.mcpServers);
|
|
49
|
+
}
|
|
50
|
+
return session;
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Session, SessionBusyError, type SessionOptions, type SessionView, type SessionState, type PromptResult } from "./runtime/session.js";
|
|
2
|
+
export type { RunStatus } from "./runtime/agent.js";
|
|
3
|
+
export type { SessionMessage } from "./runtime/session-messages.js";
|
|
4
|
+
export { INITIAL_RUN_METRICS, type SessionEvent, type TimelineEvent, type RunMetrics } from "./runtime/events.js";
|
|
5
|
+
export type { Tool, ToolContext, ToolSchema, Todo, TodoStatus, TextResult } from "./tools/types.js";
|
|
6
|
+
export { toolError } from "./tools/types.js";
|
|
7
|
+
export type { BuiltinToolsOptions } from "./tools/registry.js";
|
|
8
|
+
export type { Skill } from "./skills/loader.js";
|
|
9
|
+
export { tryLoadSkills } from "./skills/loader.js";
|
|
10
|
+
export type { MCPClientInfo, MCPServerConfig, MCPServerInfo } from "./mcp/types.js";
|
|
11
|
+
export type { LLMConfig, LLMThinkingEffort, LLMBackend } from "./llm/types.js";
|
|
12
|
+
export { createSession, SYSTEM_PROMPT_BOUNDARY } from "./create-session.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Session, SessionBusyError } from "./runtime/session.js";
|
|
2
|
+
export { INITIAL_RUN_METRICS } from "./runtime/events.js";
|
|
3
|
+
export { toolError } from "./tools/types.js";
|
|
4
|
+
export { tryLoadSkills } from "./skills/loader.js";
|
|
5
|
+
export { createSession, SYSTEM_PROMPT_BOUNDARY } from "./create-session.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { type LLMAssistantMessage, type LLMMessage } from "./messages.js";
|
|
3
|
+
import type { ChatOptions, LLMThinkingEffort, ResolvedLLMConfig } from "./types.js";
|
|
4
|
+
import { BaseAdapter } from "./base.js";
|
|
5
|
+
export declare const THINKING_BUDGET: Record<LLMThinkingEffort, number>;
|
|
6
|
+
export declare class AnthropicAdapter extends BaseAdapter {
|
|
7
|
+
private client;
|
|
8
|
+
constructor(config: ResolvedLLMConfig);
|
|
9
|
+
stream(opts: ChatOptions): Promise<LLMAssistantMessage>;
|
|
10
|
+
}
|
|
11
|
+
export declare function toAnthropicMessages(messages: LLMMessage[], includeThinking: boolean): {
|
|
12
|
+
system: string | undefined;
|
|
13
|
+
messages: Anthropic.MessageParam[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { EmptyAssistantMessageError, parseToolArgs, toText, } from "./messages.js";
|
|
3
|
+
import { BaseAdapter } from "./base.js";
|
|
4
|
+
import { netFetch } from "../util/net.js";
|
|
5
|
+
const CONTINUE_CUE = "Continue the work, using the prior conversation as context.";
|
|
6
|
+
export const THINKING_BUDGET = {
|
|
7
|
+
high: 16000,
|
|
8
|
+
max: 32000,
|
|
9
|
+
};
|
|
10
|
+
export class AnthropicAdapter extends BaseAdapter {
|
|
11
|
+
client;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super(config);
|
|
14
|
+
this.client = new Anthropic({
|
|
15
|
+
apiKey: config.apiKey,
|
|
16
|
+
baseURL: config.baseUrl || undefined,
|
|
17
|
+
maxRetries: 0,
|
|
18
|
+
fetch: netFetch,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async stream(opts) {
|
|
22
|
+
const useThinking = opts.thinking !== false;
|
|
23
|
+
const budget = THINKING_BUDGET[this.thinkingEffort];
|
|
24
|
+
const { system, messages } = toAnthropicMessages(opts.messages, useThinking);
|
|
25
|
+
const tools = opts.tools.map(toAnthropicTool);
|
|
26
|
+
const cacheControl = { type: "ephemeral" };
|
|
27
|
+
const systemCached = system
|
|
28
|
+
? [{ type: "text", text: system, cache_control: cacheControl }]
|
|
29
|
+
: undefined;
|
|
30
|
+
const toolsCached = tools.length > 0 && !system
|
|
31
|
+
? [...tools.slice(0, -1), { ...tools[tools.length - 1], cache_control: cacheControl }]
|
|
32
|
+
: undefined;
|
|
33
|
+
const params = {
|
|
34
|
+
model: this.model,
|
|
35
|
+
max_tokens: this.maxOutputTokens,
|
|
36
|
+
messages,
|
|
37
|
+
...(systemCached && { system: systemCached }),
|
|
38
|
+
...(useThinking && {
|
|
39
|
+
thinking: { type: "enabled", budget_tokens: Math.min(budget, this.maxOutputTokens - 1) },
|
|
40
|
+
output_config: { effort: this.thinkingEffort },
|
|
41
|
+
}),
|
|
42
|
+
...(tools.length > 0 && { tools: toolsCached ?? tools }),
|
|
43
|
+
};
|
|
44
|
+
const stream = this.client.messages.stream(params, { signal: opts.signal });
|
|
45
|
+
if (opts.onUsage)
|
|
46
|
+
stream.on("streamEvent", (e) => { if (e.type === "message_start")
|
|
47
|
+
opts.onUsage(e.message.usage.input_tokens, 0); });
|
|
48
|
+
if (opts.onDelta)
|
|
49
|
+
stream.on("text", (delta) => opts.onDelta(delta));
|
|
50
|
+
if (opts.onThinking)
|
|
51
|
+
stream.on("thinking", (delta) => opts.onThinking(delta));
|
|
52
|
+
if (opts.onToolCall)
|
|
53
|
+
stream.on("contentBlock", (block) => { if (block.type === "tool_use")
|
|
54
|
+
opts.onToolCall(); });
|
|
55
|
+
const final = await stream.finalMessage();
|
|
56
|
+
opts.onUsage?.(final.usage.input_tokens, final.usage.output_tokens);
|
|
57
|
+
const thinking = [];
|
|
58
|
+
let text = "";
|
|
59
|
+
const toolCalls = [];
|
|
60
|
+
for (const block of final.content) {
|
|
61
|
+
if (block.type === "thinking") {
|
|
62
|
+
thinking.push({ type: "thinking", thinking: block.thinking, signature: block.signature });
|
|
63
|
+
}
|
|
64
|
+
else if (block.type === "redacted_thinking") {
|
|
65
|
+
thinking.push({ type: "redacted_thinking", data: block.data });
|
|
66
|
+
}
|
|
67
|
+
else if (block.type === "text") {
|
|
68
|
+
text += block.text;
|
|
69
|
+
}
|
|
70
|
+
else if (block.type === "tool_use") {
|
|
71
|
+
toolCalls.push({
|
|
72
|
+
id: block.id,
|
|
73
|
+
type: "function",
|
|
74
|
+
function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const message = {
|
|
79
|
+
role: "assistant",
|
|
80
|
+
content: text || null,
|
|
81
|
+
};
|
|
82
|
+
if (thinking.length)
|
|
83
|
+
message.thinking = thinking;
|
|
84
|
+
if (toolCalls.length)
|
|
85
|
+
message.tool_calls = toolCalls;
|
|
86
|
+
if (!text && !thinking.length && !toolCalls.length) {
|
|
87
|
+
throw new EmptyAssistantMessageError();
|
|
88
|
+
}
|
|
89
|
+
return message;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function toAnthropicTool(schema) {
|
|
93
|
+
return {
|
|
94
|
+
name: schema.function.name,
|
|
95
|
+
description: schema.function.description,
|
|
96
|
+
input_schema: schema.function.parameters,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function toAnthropicMessages(messages, includeThinking) {
|
|
100
|
+
let system;
|
|
101
|
+
const rest = [];
|
|
102
|
+
for (const m of messages) {
|
|
103
|
+
if (m.role === "system") {
|
|
104
|
+
const text = toText(m.content);
|
|
105
|
+
system = system ? `${system}\n\n${text}` : text;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
rest.push(m);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
while (rest.length > 0 &&
|
|
112
|
+
rest[0].role === "assistant" &&
|
|
113
|
+
!rest[0].tool_calls?.length) {
|
|
114
|
+
const text = toText(rest[0].content);
|
|
115
|
+
if (text)
|
|
116
|
+
system = system ? `${system}\n\n${text}` : text;
|
|
117
|
+
rest.shift();
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const m of rest) {
|
|
121
|
+
const param = toMessageParam(m, includeThinking);
|
|
122
|
+
const last = out[out.length - 1];
|
|
123
|
+
if (last && last.role === param.role) {
|
|
124
|
+
const merged = mergeContent(last.content, param.content);
|
|
125
|
+
if (merged === null)
|
|
126
|
+
out.push(param);
|
|
127
|
+
else
|
|
128
|
+
last.content = merged;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
out.push(param);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (out.length === 0 || out[0].role === "assistant") {
|
|
135
|
+
out.unshift({ role: "user", content: CONTINUE_CUE });
|
|
136
|
+
}
|
|
137
|
+
return { system, messages: out };
|
|
138
|
+
}
|
|
139
|
+
function toMessageParam(m, includeThinking) {
|
|
140
|
+
if (m.role === "user") {
|
|
141
|
+
return { role: "user", content: toText(m.content) };
|
|
142
|
+
}
|
|
143
|
+
if (m.role === "tool") {
|
|
144
|
+
return {
|
|
145
|
+
role: "user",
|
|
146
|
+
content: [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const blocks = [];
|
|
150
|
+
const a = m;
|
|
151
|
+
if (includeThinking && a.thinking) {
|
|
152
|
+
for (const t of a.thinking)
|
|
153
|
+
blocks.push(t);
|
|
154
|
+
}
|
|
155
|
+
const text = toText(a.content);
|
|
156
|
+
if (text)
|
|
157
|
+
blocks.push({ type: "text", text });
|
|
158
|
+
if (a.tool_calls) {
|
|
159
|
+
for (const tc of a.tool_calls) {
|
|
160
|
+
const parsed = parseToolArgs(tc.function.arguments);
|
|
161
|
+
blocks.push({
|
|
162
|
+
type: "tool_use",
|
|
163
|
+
id: tc.id,
|
|
164
|
+
name: tc.function.name,
|
|
165
|
+
input: parsed.ok ? parsed.args : {},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { role: "assistant", content: blocks.length ? blocks : (text || "-") };
|
|
170
|
+
}
|
|
171
|
+
function hasToolResult(content) {
|
|
172
|
+
return Array.isArray(content) && content.some((block) => block.type === "tool_result");
|
|
173
|
+
}
|
|
174
|
+
function hasText(content) {
|
|
175
|
+
return typeof content === "string" ? content.length > 0 : content.some((b) => b.type === "text");
|
|
176
|
+
}
|
|
177
|
+
function mergeContent(a, b) {
|
|
178
|
+
const aText = hasText(a);
|
|
179
|
+
const bText = hasText(b);
|
|
180
|
+
if ((aText || bText) && (hasToolResult(a) || hasToolResult(b)))
|
|
181
|
+
return null;
|
|
182
|
+
if (typeof a === "string" && typeof b === "string")
|
|
183
|
+
return a ? `${a}\n${b}` : b;
|
|
184
|
+
const blocks = [];
|
|
185
|
+
if (typeof a === "string") {
|
|
186
|
+
if (a)
|
|
187
|
+
blocks.push({ type: "text", text: a });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
blocks.push(...a);
|
|
191
|
+
}
|
|
192
|
+
if (typeof b === "string") {
|
|
193
|
+
if (b)
|
|
194
|
+
blocks.push({ type: "text", text: b });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
blocks.push(...b);
|
|
198
|
+
}
|
|
199
|
+
return blocks;
|
|
200
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LLMAssistantMessage } from "./messages.js";
|
|
2
|
+
import type { Adapter, ChatOptions, LLMThinkingEffort, ResolvedLLMConfig } from "./types.js";
|
|
3
|
+
export declare abstract class BaseAdapter implements Adapter {
|
|
4
|
+
readonly model: string;
|
|
5
|
+
readonly thinkingEffort: LLMThinkingEffort;
|
|
6
|
+
readonly maxInputTokens: number;
|
|
7
|
+
readonly maxOutputTokens: number;
|
|
8
|
+
protected constructor(config: ResolvedLLMConfig);
|
|
9
|
+
abstract stream(opts: ChatOptions): Promise<LLMAssistantMessage>;
|
|
10
|
+
}
|
package/dist/llm/base.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class BaseAdapter {
|
|
2
|
+
model;
|
|
3
|
+
thinkingEffort;
|
|
4
|
+
maxInputTokens;
|
|
5
|
+
maxOutputTokens;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.model = config.model;
|
|
8
|
+
this.thinkingEffort = config.thinkingEffort;
|
|
9
|
+
this.maxInputTokens = config.maxInputTokens;
|
|
10
|
+
this.maxOutputTokens = config.maxOutputTokens;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Adapter, LLMClient, LLMConfig } from "./types.js";
|
|
2
|
+
export declare function isRetryableError(e: unknown, signal?: AbortSignal): boolean;
|
|
3
|
+
export declare function withRetryChat(adapter: Adapter): LLMClient["chat"];
|
|
4
|
+
export declare function createLLM(config: LLMConfig): LLMClient;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EmptyAssistantMessageError } from "./messages.js";
|
|
2
|
+
import { CompletionsAdapter } from "./completions.js";
|
|
3
|
+
import { AnthropicAdapter } from "./anthropic.js";
|
|
4
|
+
import { ResponsesAdapter } from "./responses.js";
|
|
5
|
+
import { isAbortError, withRetry, backoffDelay } from "../util/async.js";
|
|
6
|
+
import { DEFAULT_BACKEND, DEFAULT_MAX_INPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_THINKING_EFFORT, LLM_MAX_RETRIES } from "../util/constants.js";
|
|
7
|
+
export function isRetryableError(e, signal) {
|
|
8
|
+
if (signal?.aborted || isAbortError(e))
|
|
9
|
+
return false;
|
|
10
|
+
if (e instanceof EmptyAssistantMessageError)
|
|
11
|
+
return true;
|
|
12
|
+
const name = e.name;
|
|
13
|
+
if (name === "APIConnectionError" || name === "APIConnectionTimeoutError" || name === "APITimeoutError")
|
|
14
|
+
return true;
|
|
15
|
+
const status = e.status;
|
|
16
|
+
if (status != null)
|
|
17
|
+
return status === 429 || status >= 500;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
export function withRetryChat(adapter) {
|
|
21
|
+
return (opts) => {
|
|
22
|
+
let sawToolCall = false;
|
|
23
|
+
return withRetry(() => {
|
|
24
|
+
sawToolCall = false;
|
|
25
|
+
return adapter.stream({ ...opts, onToolCall: () => { sawToolCall = true; } });
|
|
26
|
+
}, {
|
|
27
|
+
retries: LLM_MAX_RETRIES,
|
|
28
|
+
retryable: (e) => !sawToolCall && isRetryableError(e, opts.signal),
|
|
29
|
+
backoff: backoffDelay,
|
|
30
|
+
onRetry: opts.onRetry,
|
|
31
|
+
signal: opts.signal,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function createLLM(config) {
|
|
36
|
+
const cfg = {
|
|
37
|
+
...config,
|
|
38
|
+
thinkingEffort: config.thinkingEffort ?? DEFAULT_THINKING_EFFORT,
|
|
39
|
+
backend: config.backend ?? DEFAULT_BACKEND,
|
|
40
|
+
maxInputTokens: config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS,
|
|
41
|
+
maxOutputTokens: config.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
42
|
+
};
|
|
43
|
+
let adapter;
|
|
44
|
+
switch (cfg.backend) {
|
|
45
|
+
case "responses":
|
|
46
|
+
adapter = new ResponsesAdapter(cfg);
|
|
47
|
+
break;
|
|
48
|
+
case "anthropic":
|
|
49
|
+
adapter = new AnthropicAdapter(cfg);
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
adapter = new CompletionsAdapter(cfg);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
model: adapter.model,
|
|
56
|
+
thinkingEffort: adapter.thinkingEffort,
|
|
57
|
+
maxInputTokens: adapter.maxInputTokens,
|
|
58
|
+
maxOutputTokens: adapter.maxOutputTokens,
|
|
59
|
+
chat: withRetryChat(adapter),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LLMAssistantMessage } from "./messages.js";
|
|
2
|
+
import type { ChatOptions, ResolvedLLMConfig } from "./types.js";
|
|
3
|
+
import { BaseAdapter } from "./base.js";
|
|
4
|
+
export declare class CompletionsAdapter extends BaseAdapter {
|
|
5
|
+
private client;
|
|
6
|
+
constructor(config: ResolvedLLMConfig);
|
|
7
|
+
stream(opts: ChatOptions): Promise<LLMAssistantMessage>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { EmptyAssistantMessageError } from "./messages.js";
|
|
3
|
+
import { BaseAdapter } from "./base.js";
|
|
4
|
+
import { netFetch } from "../util/net.js";
|
|
5
|
+
export class CompletionsAdapter extends BaseAdapter {
|
|
6
|
+
client;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super(config);
|
|
9
|
+
this.client = new OpenAI({
|
|
10
|
+
apiKey: config.apiKey,
|
|
11
|
+
baseURL: config.baseUrl || undefined,
|
|
12
|
+
maxRetries: 0,
|
|
13
|
+
fetch: netFetch,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async stream(opts) {
|
|
17
|
+
const { messages, tools, onDelta, onThinking, onUsage, onToolCall, thinking, signal } = opts;
|
|
18
|
+
let content = "";
|
|
19
|
+
let refusal = "";
|
|
20
|
+
const calls = new Map();
|
|
21
|
+
const useThinking = thinking !== false;
|
|
22
|
+
const params = {
|
|
23
|
+
model: this.model,
|
|
24
|
+
max_tokens: this.maxOutputTokens,
|
|
25
|
+
messages,
|
|
26
|
+
stream: true,
|
|
27
|
+
stream_options: { include_usage: true },
|
|
28
|
+
...(tools.length > 0 && { tools }),
|
|
29
|
+
...(useThinking && { reasoning_effort: this.thinkingEffort })
|
|
30
|
+
};
|
|
31
|
+
const stream = await this.client.chat.completions.create(params, { signal });
|
|
32
|
+
for await (const chunk of stream) {
|
|
33
|
+
if (chunk.usage) {
|
|
34
|
+
onUsage?.(chunk.usage.prompt_tokens ?? 0, chunk.usage.completion_tokens ?? 0);
|
|
35
|
+
}
|
|
36
|
+
const delta = chunk.choices[0]?.delta;
|
|
37
|
+
if (!delta)
|
|
38
|
+
continue;
|
|
39
|
+
if (delta.content) {
|
|
40
|
+
content += delta.content;
|
|
41
|
+
onDelta?.(delta.content);
|
|
42
|
+
}
|
|
43
|
+
const thinkingDelta = delta;
|
|
44
|
+
const thinkingText = thinkingDelta.reasoning_content ?? thinkingDelta.reasoning;
|
|
45
|
+
if (thinkingText) {
|
|
46
|
+
onThinking?.(thinkingText);
|
|
47
|
+
}
|
|
48
|
+
const refusalDelta = delta;
|
|
49
|
+
if (refusalDelta.refusal) {
|
|
50
|
+
refusal += refusalDelta.refusal;
|
|
51
|
+
onDelta?.(refusalDelta.refusal);
|
|
52
|
+
}
|
|
53
|
+
if (delta.tool_calls) {
|
|
54
|
+
onToolCall?.();
|
|
55
|
+
for (const tc of delta.tool_calls) {
|
|
56
|
+
let acc = calls.get(tc.index);
|
|
57
|
+
if (!acc) {
|
|
58
|
+
acc = { id: tc.id ?? "", name: "", arguments: "" };
|
|
59
|
+
calls.set(tc.index, acc);
|
|
60
|
+
}
|
|
61
|
+
if (tc.id)
|
|
62
|
+
acc.id = tc.id;
|
|
63
|
+
if (tc.function?.name)
|
|
64
|
+
acc.name = tc.function.name;
|
|
65
|
+
if (tc.function?.arguments)
|
|
66
|
+
acc.arguments += tc.function.arguments;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const message = {
|
|
71
|
+
role: "assistant",
|
|
72
|
+
content: content || refusal || null,
|
|
73
|
+
};
|
|
74
|
+
if (calls.size) {
|
|
75
|
+
message.tool_calls = [...calls.entries()]
|
|
76
|
+
.sort((a, b) => a[0] - b[0])
|
|
77
|
+
.map(([, acc]) => ({
|
|
78
|
+
id: acc.id,
|
|
79
|
+
type: "function",
|
|
80
|
+
function: { name: acc.name, arguments: acc.arguments },
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
if (!content && !refusal && !calls.size) {
|
|
84
|
+
throw new EmptyAssistantMessageError();
|
|
85
|
+
}
|
|
86
|
+
return message;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface TextContentPart {
|
|
2
|
+
type: "text";
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolCall {
|
|
6
|
+
id: string;
|
|
7
|
+
type: "function";
|
|
8
|
+
function: {
|
|
9
|
+
name: string;
|
|
10
|
+
arguments: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface ThinkingBlock {
|
|
14
|
+
type: "thinking";
|
|
15
|
+
thinking: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RedactedThinkingBlock {
|
|
19
|
+
type: "redacted_thinking";
|
|
20
|
+
data: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LLMAssistantMessage {
|
|
23
|
+
role: "assistant";
|
|
24
|
+
content: string | null | TextContentPart[];
|
|
25
|
+
tool_calls?: ToolCall[];
|
|
26
|
+
thinking?: Array<ThinkingBlock | RedactedThinkingBlock>;
|
|
27
|
+
}
|
|
28
|
+
export type LLMMessage = {
|
|
29
|
+
role: "system";
|
|
30
|
+
content: string | TextContentPart[];
|
|
31
|
+
} | {
|
|
32
|
+
role: "user";
|
|
33
|
+
content: string | TextContentPart[];
|
|
34
|
+
name?: string;
|
|
35
|
+
} | LLMAssistantMessage | {
|
|
36
|
+
role: "tool";
|
|
37
|
+
tool_call_id: string;
|
|
38
|
+
content: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function toText(content: string | TextContentPart[] | null | undefined): string;
|
|
41
|
+
export declare class EmptyAssistantMessageError extends Error {
|
|
42
|
+
constructor();
|
|
43
|
+
}
|
|
44
|
+
export declare function parseToolArgs(args: string | undefined): {
|
|
45
|
+
ok: true;
|
|
46
|
+
args: Record<string, unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { toErrorMessage } from "../util/text.js";
|
|
2
|
+
export function toText(content) {
|
|
3
|
+
if (!content)
|
|
4
|
+
return "";
|
|
5
|
+
if (typeof content === "string")
|
|
6
|
+
return content;
|
|
7
|
+
return content.map((p) => p.text).join("");
|
|
8
|
+
}
|
|
9
|
+
export class EmptyAssistantMessageError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("empty assistant message: no content, refusal, thinking, or tool calls");
|
|
12
|
+
this.name = "EmptyAssistantMessageError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function parseToolArgs(args) {
|
|
16
|
+
if (!args)
|
|
17
|
+
return { ok: true, args: {} };
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(args);
|
|
20
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
21
|
+
return { ok: false, error: "arguments must be a JSON object" };
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, args: parsed };
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return { ok: false, error: toErrorMessage(e) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { type LLMAssistantMessage, type LLMMessage } from "./messages.js";
|
|
3
|
+
import type { ChatOptions, ResolvedLLMConfig } from "./types.js";
|
|
4
|
+
import { BaseAdapter } from "./base.js";
|
|
5
|
+
import type { ToolSchema } from "../tools/types.js";
|
|
6
|
+
type ResponsesInputItem = OpenAI.Responses.ResponseInputItem;
|
|
7
|
+
export declare class ResponsesAdapter extends BaseAdapter {
|
|
8
|
+
private client;
|
|
9
|
+
constructor(config: ResolvedLLMConfig);
|
|
10
|
+
stream(opts: ChatOptions): Promise<LLMAssistantMessage>;
|
|
11
|
+
}
|
|
12
|
+
export declare function toResponsesTool(schema: ToolSchema): OpenAI.Responses.FunctionTool;
|
|
13
|
+
export declare function toResponsesInput(messages: LLMMessage[]): ResponsesInputItem[];
|
|
14
|
+
export {};
|