modeldispatcher-browser-agent 0.6.7 → 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/README.md +60 -1
- package/dist/agent.d.ts +12 -3
- package/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +3 -1
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts +5 -1
- package/dist/providers/gemini.js +94 -0
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/ollama.d.ts +3 -1
- package/dist/providers/ollama.js +53 -0
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openaiCompatible.d.ts +5 -1
- package/dist/providers/openaiCompatible.js +82 -0
- package/dist/providers/openaiCompatible.js.map +1 -1
- package/dist/toolMessages.d.ts +4 -0
- package/dist/toolMessages.js +10 -0
- package/dist/toolMessages.js.map +1 -0
- package/dist/tools.d.ts +54 -0
- package/dist/tools.js +140 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/package.json +2 -2
- package/src/agent.ts +2 -2
- package/src/index.ts +7 -0
- package/src/providers/anthropic.ts +90 -1
- package/src/providers/gemini.ts +125 -1
- package/src/providers/ollama.ts +73 -1
- package/src/providers/openaiCompatible.ts +103 -1
- package/src/toolMessages.ts +11 -0
- package/src/tools.ts +215 -0
- package/src/types.ts +52 -0
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { consumeLines, describeHttpError, fetchWithRetry } from "../http.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ChatMessage,
|
|
11
|
+
ProviderCallConfig,
|
|
12
|
+
RequestOptions,
|
|
13
|
+
ToolChatMessage,
|
|
14
|
+
ToolSpec,
|
|
15
|
+
ToolTurn,
|
|
16
|
+
} from "../types.js";
|
|
10
17
|
|
|
11
18
|
const DEFAULT_MAX_TOKENS = 1024;
|
|
12
19
|
|
|
@@ -87,3 +94,98 @@ export async function streamOpenAICompatible(
|
|
|
87
94
|
});
|
|
88
95
|
return full;
|
|
89
96
|
}
|
|
97
|
+
|
|
98
|
+
function toToolApiMessages(messages: readonly ToolChatMessage[], systemInstruction?: string) {
|
|
99
|
+
const hasSystemTurn = messages.some((m) => m.role === "system");
|
|
100
|
+
const system = !hasSystemTurn && systemInstruction ? [{ role: "system", content: systemInstruction }] : [];
|
|
101
|
+
return [
|
|
102
|
+
...system,
|
|
103
|
+
...messages.flatMap((m): Record<string, unknown>[] => {
|
|
104
|
+
if (m.role === "tool") {
|
|
105
|
+
return m.results.map((r) => ({ role: "tool", tool_call_id: r.callId, content: r.content }));
|
|
106
|
+
}
|
|
107
|
+
if ("toolCalls" in m) {
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
role: "assistant",
|
|
111
|
+
content: m.content || null,
|
|
112
|
+
tool_calls: m.toolCalls.map((c) => ({
|
|
113
|
+
id: c.id,
|
|
114
|
+
type: "function",
|
|
115
|
+
function: { name: c.name, arguments: JSON.stringify(c.arguments) },
|
|
116
|
+
})),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
return [{ role: m.role, content: m.content }];
|
|
121
|
+
}),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface OpenAIToolCall {
|
|
126
|
+
id?: string;
|
|
127
|
+
function?: { name?: string; arguments?: string };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Parse the model's tool-call arguments: a JSON string that may be empty. */
|
|
131
|
+
export function parseArguments(raw: string | undefined, toolName: string): Record<string, unknown> {
|
|
132
|
+
if (!raw || !raw.trim()) return {};
|
|
133
|
+
let parsed: unknown;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(raw);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`Tool call ${toolName} sent arguments that are not valid JSON`);
|
|
138
|
+
}
|
|
139
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
140
|
+
throw new Error(`Tool call ${toolName} sent arguments that are not a JSON object`);
|
|
141
|
+
}
|
|
142
|
+
return parsed as Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One non-streaming turn with tools available (OpenAI or Groq). */
|
|
146
|
+
export async function completeWithToolsOpenAICompatible(
|
|
147
|
+
provider: "openai" | "groq",
|
|
148
|
+
cfg: ProviderCallConfig,
|
|
149
|
+
messages: readonly ToolChatMessage[],
|
|
150
|
+
tools: readonly ToolSpec[],
|
|
151
|
+
options: RequestOptions = {},
|
|
152
|
+
): Promise<ToolTurn> {
|
|
153
|
+
const res = await fetchWithRetry(
|
|
154
|
+
BASE_URLS[provider],
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
model: cfg.model,
|
|
160
|
+
max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
161
|
+
messages: toToolApiMessages(messages, options.systemInstruction),
|
|
162
|
+
...(tools.length
|
|
163
|
+
? {
|
|
164
|
+
tools: tools.map((t) => ({
|
|
165
|
+
type: "function",
|
|
166
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
167
|
+
})),
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
options.signal,
|
|
173
|
+
);
|
|
174
|
+
if (!res.ok) throw new Error(await describeHttpError(res));
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
const choice = data.choices?.[0];
|
|
177
|
+
if (!choice?.message) throw new Error(`${provider} returned no answer`);
|
|
178
|
+
if (choice.finish_reason === "content_filter") throw new Error(`${provider} filtered the answer`);
|
|
179
|
+
const message = choice.message;
|
|
180
|
+
const calls: OpenAIToolCall[] = message.tool_calls ?? [];
|
|
181
|
+
if (choice.finish_reason === "length" && calls.length) {
|
|
182
|
+
throw new Error(`${provider} ran out of tokens in the middle of a tool call`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
text: message.content ?? "",
|
|
186
|
+
toolCalls: calls.map((c) => {
|
|
187
|
+
const name = c.function?.name ?? "";
|
|
188
|
+
return { id: c.id ?? "", name, arguments: parseArguments(c.function?.arguments, name) };
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Helpers for the neutral tool-conversation history ({@link ToolChatMessage}). */
|
|
2
|
+
|
|
3
|
+
import type { ToolChatMessage } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/** The content of the history's system turn, if it has one. */
|
|
6
|
+
export function systemTurnOf(messages: readonly ToolChatMessage[]): string | undefined {
|
|
7
|
+
for (const m of messages) {
|
|
8
|
+
if (m.role === "system") return m.content;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool calling in the browser: the model asks for tools, the app runs them
|
|
3
|
+
* on the device, and the results go back until the model answers. The same
|
|
4
|
+
* bounded loop as the Python core's `AgentLoop`, client-side.
|
|
5
|
+
*
|
|
6
|
+
* - runWithTools() — a chat history and a set of tools in, the final answer
|
|
7
|
+
* and the tools used out
|
|
8
|
+
*
|
|
9
|
+
* Each turn is dispatched through the same fallback list as `complete()`, so
|
|
10
|
+
* a failing provider hands the run to the next one mid-conversation: the
|
|
11
|
+
* history is kept in a neutral form ({@link ToolChatMessage}) and translated
|
|
12
|
+
* per provider on every turn. Non-streaming: a turn may end in tool calls,
|
|
13
|
+
* so there is no single text stream to show.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
AllProvidersExhaustedError,
|
|
18
|
+
candidatesOf,
|
|
19
|
+
NoProviderConfiguredError,
|
|
20
|
+
type FailedAttempt,
|
|
21
|
+
} from "./agent.js";
|
|
22
|
+
import { completeWithToolsAnthropic } from "./providers/anthropic.js";
|
|
23
|
+
import { completeWithToolsGemini } from "./providers/gemini.js";
|
|
24
|
+
import { completeWithToolsOllama } from "./providers/ollama.js";
|
|
25
|
+
import { completeWithToolsOpenAICompatible } from "./providers/openaiCompatible.js";
|
|
26
|
+
import type {
|
|
27
|
+
AgentConfig,
|
|
28
|
+
ProviderCallConfig,
|
|
29
|
+
ProviderId,
|
|
30
|
+
RequestOptions,
|
|
31
|
+
ToolCall,
|
|
32
|
+
ToolChatMessage,
|
|
33
|
+
ToolResult,
|
|
34
|
+
ToolSpec,
|
|
35
|
+
ToolTurn,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
/** A tool the app runs itself: its declaration and its implementation. */
|
|
39
|
+
export interface Tool {
|
|
40
|
+
readonly spec: ToolSpec;
|
|
41
|
+
/** Runs on the device with the model's decoded arguments; its return value
|
|
42
|
+
* (or the message of what it throws) is what the model reads. */
|
|
43
|
+
readonly run: (args: Readonly<Record<string, unknown>>) => string | Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RunWithToolsOptions extends Omit<RequestOptions, "jsonMode"> {
|
|
47
|
+
readonly tools: readonly Tool[];
|
|
48
|
+
/** Model turns allowed before giving up (each may request several tools).
|
|
49
|
+
* Defaults to {@link DEFAULT_MAX_STEPS}. */
|
|
50
|
+
readonly maxSteps?: number;
|
|
51
|
+
/** Called after each tool runs, e.g. to show "looked up AAPL" while waiting. */
|
|
52
|
+
readonly onToolResult?: (call: ToolCall, result: ToolResult) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ToolRunStopReason = "completed" | "max_steps";
|
|
56
|
+
|
|
57
|
+
export interface ToolRunResult {
|
|
58
|
+
/** The model's final answer; may be empty when `stopReason` is "max_steps". */
|
|
59
|
+
readonly text: string;
|
|
60
|
+
readonly stopReason: ToolRunStopReason;
|
|
61
|
+
/** Names of the tools run, in order, repeats included. */
|
|
62
|
+
readonly toolsUsed: readonly string[];
|
|
63
|
+
/** The provider and model that produced the final turn. */
|
|
64
|
+
readonly provider: ProviderId;
|
|
65
|
+
readonly model: string;
|
|
66
|
+
/** The whole conversation including tool turns, to continue it later. */
|
|
67
|
+
readonly messages: readonly ToolChatMessage[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const DEFAULT_MAX_STEPS = 6;
|
|
71
|
+
|
|
72
|
+
function completeTurn(
|
|
73
|
+
candidate: ProviderCallConfig,
|
|
74
|
+
messages: readonly ToolChatMessage[],
|
|
75
|
+
tools: readonly ToolSpec[],
|
|
76
|
+
options: RequestOptions,
|
|
77
|
+
): Promise<ToolTurn> {
|
|
78
|
+
switch (candidate.provider) {
|
|
79
|
+
case "gemini":
|
|
80
|
+
return completeWithToolsGemini(candidate, messages, tools, options);
|
|
81
|
+
case "anthropic":
|
|
82
|
+
return completeWithToolsAnthropic(candidate, messages, tools, options);
|
|
83
|
+
case "ollama":
|
|
84
|
+
return completeWithToolsOllama(candidate, messages, tools, options);
|
|
85
|
+
case "openai":
|
|
86
|
+
case "groq":
|
|
87
|
+
return completeWithToolsOpenAICompatible(candidate.provider, candidate, messages, tools, options);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface DispatchedTurn {
|
|
92
|
+
readonly turn: ToolTurn;
|
|
93
|
+
readonly candidate: ProviderCallConfig;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function dispatchTurn(
|
|
97
|
+
candidates: readonly ProviderCallConfig[],
|
|
98
|
+
messages: readonly ToolChatMessage[],
|
|
99
|
+
tools: readonly ToolSpec[],
|
|
100
|
+
options: RequestOptions,
|
|
101
|
+
): Promise<DispatchedTurn> {
|
|
102
|
+
const attempts: FailedAttempt[] = [];
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
try {
|
|
105
|
+
return { turn: await completeTurn(candidate, messages, tools, options), candidate };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (options.signal?.aborted) throw err;
|
|
108
|
+
attempts.push({ provider: candidate.provider, error: messageOf(err) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new AllProvidersExhaustedError(attempts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Give calls without a provider id (Gemini, Ollama) one unique in the whole
|
|
116
|
+
* conversation, counting the calls already in it: a continued conversation
|
|
117
|
+
* must not repeat an id (Anthropic rejects duplicates).
|
|
118
|
+
*/
|
|
119
|
+
function numberCalls(calls: readonly ToolCall[], history: readonly ToolChatMessage[]): ToolCall[] {
|
|
120
|
+
let next = history.reduce((n, m) => n + ("toolCalls" in m ? m.toolCalls.length : 0), 0);
|
|
121
|
+
return calls.map((call) => (call.id ? call : { ...call, id: `call_${next++}` }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function messageOf(err: unknown): string {
|
|
125
|
+
return err instanceof Error ? err.message : String(err);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The system prompt for every turn, decided once so a fallback doesn't change
|
|
130
|
+
* it: the explicit option, else every system turn in the history, joined.
|
|
131
|
+
*/
|
|
132
|
+
function systemOf(messages: readonly ToolChatMessage[], explicit: string | undefined): string | undefined {
|
|
133
|
+
if (explicit !== undefined) return explicit;
|
|
134
|
+
const turns = messages.flatMap((m) => (m.role === "system" ? [m.content] : []));
|
|
135
|
+
return turns.length ? turns.join("\n\n") : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What goes to the provider: no system turns (sent as the instruction) and no
|
|
139
|
+
* empty plain turns, which Anthropic and Gemini reject. */
|
|
140
|
+
function sendable(history: readonly ToolChatMessage[]): ToolChatMessage[] {
|
|
141
|
+
return history.filter((m) => m.role !== "system" && (m.role === "tool" || "toolCalls" in m || m.content !== ""));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function runTool(tools: ReadonlyMap<string, Tool>, call: ToolCall): Promise<ToolResult> {
|
|
145
|
+
const tool = tools.get(call.name);
|
|
146
|
+
if (!tool) return { callId: call.id, name: call.name, content: `unknown tool: ${call.name}`, isError: true };
|
|
147
|
+
try {
|
|
148
|
+
return { callId: call.id, name: call.name, content: await tool.run(call.arguments), isError: false };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Handed back to the model, which can retry or explain, as the Python core does.
|
|
151
|
+
return { callId: call.id, name: call.name, content: `tool error: ${messageOf(err)}`, isError: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run a conversation in which the model may call `options.tools`, until it
|
|
157
|
+
* answers without asking for one or `maxSteps` turns pass. Tools run one at
|
|
158
|
+
* a time, in the order asked, so a write lands before a later read.
|
|
159
|
+
*
|
|
160
|
+
* Raises:
|
|
161
|
+
* NoProviderConfiguredError: `cfg.providers` has no usable credential.
|
|
162
|
+
* AllProvidersExhaustedError: every candidate failed on some turn.
|
|
163
|
+
*/
|
|
164
|
+
export async function runWithTools(
|
|
165
|
+
cfg: AgentConfig,
|
|
166
|
+
messages: readonly ToolChatMessage[],
|
|
167
|
+
options: RunWithToolsOptions,
|
|
168
|
+
): Promise<ToolRunResult> {
|
|
169
|
+
const candidates = candidatesOf(cfg);
|
|
170
|
+
if (candidates.length === 0) throw new NoProviderConfiguredError();
|
|
171
|
+
const { tools, maxSteps = DEFAULT_MAX_STEPS, onToolResult, systemInstruction, ...rest } = options;
|
|
172
|
+
const system = systemOf(messages, systemInstruction);
|
|
173
|
+
const requestOptions: RequestOptions = system === undefined ? rest : { ...rest, systemInstruction: system };
|
|
174
|
+
const specs = tools.map((t) => t.spec);
|
|
175
|
+
const byName = new Map(tools.map((t) => [t.spec.name, t]));
|
|
176
|
+
|
|
177
|
+
const history: ToolChatMessage[] = [...messages];
|
|
178
|
+
const toolsUsed: string[] = [];
|
|
179
|
+
let last: DispatchedTurn | undefined;
|
|
180
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
181
|
+
last = await dispatchTurn(candidates, sendable(history), specs, requestOptions);
|
|
182
|
+
const calls = numberCalls(last.turn.toolCalls, history);
|
|
183
|
+
if (calls.length === 0) {
|
|
184
|
+
if (last.turn.text) history.push({ role: "assistant", content: last.turn.text });
|
|
185
|
+
return result(last, "completed", toolsUsed, history);
|
|
186
|
+
}
|
|
187
|
+
history.push({ role: "assistant", content: last.turn.text, toolCalls: calls });
|
|
188
|
+
const results: ToolResult[] = [];
|
|
189
|
+
for (const call of calls) {
|
|
190
|
+
const outcome = await runTool(byName, call);
|
|
191
|
+
toolsUsed.push(call.name);
|
|
192
|
+
results.push(outcome);
|
|
193
|
+
onToolResult?.(call, outcome);
|
|
194
|
+
}
|
|
195
|
+
history.push({ role: "tool", results });
|
|
196
|
+
}
|
|
197
|
+
if (!last) throw new RangeError(`maxSteps must be at least 1, got ${maxSteps}`);
|
|
198
|
+
return result(last, "max_steps", toolsUsed, history);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function result(
|
|
202
|
+
last: DispatchedTurn,
|
|
203
|
+
stopReason: ToolRunStopReason,
|
|
204
|
+
toolsUsed: readonly string[],
|
|
205
|
+
history: readonly ToolChatMessage[],
|
|
206
|
+
): ToolRunResult {
|
|
207
|
+
return {
|
|
208
|
+
text: last.turn.text,
|
|
209
|
+
stopReason,
|
|
210
|
+
toolsUsed,
|
|
211
|
+
provider: last.candidate.provider,
|
|
212
|
+
model: last.candidate.model,
|
|
213
|
+
messages: history,
|
|
214
|
+
};
|
|
215
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -67,6 +67,58 @@ export interface ChatMessage {
|
|
|
67
67
|
readonly content: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A tool the model may call, declared by name, purpose, and a JSON Schema for
|
|
72
|
+
* its arguments — the same shape as the Python core's `ToolSpec`. Keep
|
|
73
|
+
* `parameters` to the schema subset every provider accepts (`type`,
|
|
74
|
+
* `properties`, `required`, `description`, `enum`, `items`); Gemini rejects
|
|
75
|
+
* the rest, so `additionalProperties` and `$schema` are dropped for it.
|
|
76
|
+
*/
|
|
77
|
+
export interface ToolSpec {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly description: string;
|
|
80
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A model's request to run one tool, decoded from a provider response. */
|
|
84
|
+
export interface ToolCall {
|
|
85
|
+
/** The provider's call id, or one generated for providers that send none
|
|
86
|
+
* (Gemini, Ollama). Pairs the call with its {@link ToolResult}. */
|
|
87
|
+
readonly id: string;
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly arguments: Readonly<Record<string, unknown>>;
|
|
90
|
+
/** Gemini's `thoughtSignature`, echoed back on the next turn as its
|
|
91
|
+
* thinking models require. Absent for every other provider. */
|
|
92
|
+
readonly signature?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What running a {@link ToolCall} produced, fed back to the model. */
|
|
96
|
+
export interface ToolResult {
|
|
97
|
+
readonly callId: string;
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly content: string;
|
|
100
|
+
/** True when the tool threw or was unknown: the model sees the error text
|
|
101
|
+
* and can recover, instead of the whole run failing. */
|
|
102
|
+
readonly isError: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One turn of a tool-using conversation: a plain {@link ChatMessage}, the
|
|
107
|
+
* assistant asking for tools, or the results of every tool it asked for in
|
|
108
|
+
* that turn (kept together because Anthropic and Gemini want them in a
|
|
109
|
+
* single turn).
|
|
110
|
+
*/
|
|
111
|
+
export type ToolChatMessage =
|
|
112
|
+
| ChatMessage
|
|
113
|
+
| { readonly role: "assistant"; readonly content: string; readonly toolCalls: readonly ToolCall[] }
|
|
114
|
+
| { readonly role: "tool"; readonly results: readonly ToolResult[] };
|
|
115
|
+
|
|
116
|
+
/** One model turn in a tool loop: text, tool calls, or both. */
|
|
117
|
+
export interface ToolTurn {
|
|
118
|
+
readonly text: string;
|
|
119
|
+
readonly toolCalls: readonly ToolCall[];
|
|
120
|
+
}
|
|
121
|
+
|
|
70
122
|
/** Options shared by both complete() and streamComplete()/streamChat(). */
|
|
71
123
|
export interface RequestOptions {
|
|
72
124
|
/** System prompt / instruction, applied via each provider's native mechanism. */
|