@arki-moe/agent-ts 5.1.0 → 5.3.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 CHANGED
@@ -33,9 +33,10 @@ const agent = new Agent("openai", {
33
33
  });
34
34
  agent.registerTool(getTimeTool);
35
35
 
36
- // run: Executes tool chain automatically, returns new messages, context is maintained automatically
37
- const msgs = await agent.run("What time is it?");
38
- console.log(msgs);
36
+ // run: Executes tool chain automatically, returns new messages and usage, context is maintained automatically
37
+ const result = await agent.run("What time is it?");
38
+ console.log(result.messages);
39
+ console.log(result.usage, result.usageSum);
39
40
 
40
41
  // context is a public property that can be read directly
41
42
  console.log(agent.context);
@@ -47,6 +48,7 @@ console.log(agent.context);
47
48
  |---------|----------|----------|
48
49
  | `openai` | `apiKey` (or `OPENAI_API_KEY` env), `model` | `system`, `baseUrl` |
49
50
  | `openrouter` | `apiKey` (or `OPENROUTER_API_KEY` env), `model` | `system`, `baseUrl`, `httpReferer`, `title` |
51
+ | `selfhost_chat_completions` | `apiKey` (or `SELFHOST_API_KEY` env), `model` | `system`, `baseUrl` |
50
52
 
51
53
  When `apiKey` is not provided in config, adapters read from the corresponding environment variable. An error is thrown only when both are missing.
52
54
 
@@ -55,7 +57,7 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
55
57
  - `Agent(adapterName, config)` - Create Agent
56
58
  - `agent.context` - Public property, complete conversation history
57
59
  - `agent.registerTool(tool)` - Register tool
58
- - `agent.run(message)` - Execute tool chain automatically, returns all new `Message[]`
60
+ - `agent.run(message)` - Execute tool chain automatically, returns `{ messages, usage, usageSum }`
59
61
 
60
62
  ### Config
61
63
 
@@ -67,6 +69,7 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
67
69
  | `endCondition` | `(context, last) => boolean` | Stop condition for `run`. Defaults to `last.role === Role.Ai` |
68
70
  | `onStream` | `(textDelta: string) => void \| Promise<void>` | Stream hook for AI text only. When provided, adapters use SSE streaming and still return the final `Message[]`. |
69
71
  | `isAbort` | `() => boolean \| Promise<boolean>` | Abort hook polled during `run`; return `true` to stop early and return partial results. |
72
+ | `toolChoice` | `"auto" \| "required" \| "none" \| object` | Tool choice passed to adapters. Defaults to `auto` when tools are provided. |
70
73
  | `onToolCall` | `(message, args, agent) => boolean \| void \| Promise<boolean \| void>` | Called before each tool execution; return `false` to skip tool execution and `onToolResult` |
71
74
  | `onToolResult` | `(message, agent) => void \| Promise<void>` | Called after each tool execution (`message.role === Role.ToolResult`) |
72
75
 
@@ -1,2 +1,2 @@
1
- import { type Message, type Tool } from "../types";
2
- export declare function openaiAdapter(config: Record<string, unknown>, context: Message[], tools: Tool[]): Promise<Message[]>;
1
+ import { type AdapterResult, type Message, type Tool } from "../types";
2
+ export declare function openaiAdapter(config: Record<string, unknown>, context: Message[], tools: Tool[]): Promise<AdapterResult>;
@@ -27,10 +27,23 @@ function toOpenAIMessages(context) {
27
27
  return out;
28
28
  }, []);
29
29
  }
30
+ function toUsage(usage) {
31
+ if (!usage)
32
+ return undefined;
33
+ const promptTokens = usage.prompt_tokens;
34
+ const completionTokens = usage.completion_tokens;
35
+ const totalTokens = usage.total_tokens;
36
+ if (typeof promptTokens !== "number"
37
+ || typeof completionTokens !== "number"
38
+ || typeof totalTokens !== "number")
39
+ return undefined;
40
+ return { promptTokens, completionTokens, totalTokens };
41
+ }
30
42
  async function openaiAdapter(config, context, tools) {
31
43
  const baseUrl = config.baseUrl ?? "https://api.openai.com";
32
44
  const apiKey = config.apiKey || process.env.OPENAI_API_KEY || "";
33
45
  const model = config.model ?? "gpt-5-nano";
46
+ const toolChoice = config.toolChoice;
34
47
  const onStream = config.onStream;
35
48
  const { check, isAborted } = (0, abort_1.createAbortChecker)(config.isAbort);
36
49
  if (!apiKey)
@@ -44,11 +57,12 @@ async function openaiAdapter(config, context, tools) {
44
57
  model,
45
58
  messages,
46
59
  tools: tools.length ? tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters ?? {} } })) : undefined,
47
- tool_choice: tools.length ? "auto" : undefined,
60
+ tool_choice: tools.length ? (toolChoice ?? "auto") : undefined,
48
61
  stream: onStream ? true : undefined,
62
+ stream_options: onStream ? { include_usage: true } : undefined,
49
63
  };
50
64
  if (await check())
51
- return [];
65
+ return { messages: [] };
52
66
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
53
67
  method: "POST",
54
68
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -70,6 +84,7 @@ async function openaiAdapter(config, context, tools) {
70
84
  }
71
85
  if (onStream) {
72
86
  let content = "";
87
+ let usage;
73
88
  const toolCalls = new Map();
74
89
  const upsertToolCall = (tc) => {
75
90
  const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
@@ -98,6 +113,11 @@ async function openaiAdapter(config, context, tools) {
98
113
  catch {
99
114
  throw new Error(`OpenAI API returned invalid JSON: ${dataLine.slice(0, 200)}`);
100
115
  }
116
+ if (parsed?.usage) {
117
+ const nextUsage = toUsage(parsed.usage);
118
+ if (nextUsage)
119
+ usage = nextUsage;
120
+ }
101
121
  const delta = parsed?.choices?.[0]?.delta;
102
122
  if (!delta)
103
123
  return;
@@ -113,22 +133,25 @@ async function openaiAdapter(config, context, tools) {
113
133
  await check();
114
134
  }, check);
115
135
  if (toolCalls.size > 0) {
116
- return [...toolCalls.entries()]
117
- .sort((a, b) => a[0] - b[0])
118
- .map(([index, tc]) => {
119
- if (!tc.name)
120
- throw new Error(`OpenAI streaming tool call missing function name at index ${index}`);
121
- return {
122
- role: types_1.Role.ToolCall,
123
- toolName: tc.name,
124
- callId: tc.id ?? `call_${index}`,
125
- argsText: tc.args.length ? tc.args : "{}",
126
- };
127
- });
136
+ return {
137
+ messages: [...toolCalls.entries()]
138
+ .sort((a, b) => a[0] - b[0])
139
+ .map(([index, tc]) => {
140
+ if (!tc.name)
141
+ throw new Error(`OpenAI streaming tool call missing function name at index ${index}`);
142
+ return {
143
+ role: types_1.Role.ToolCall,
144
+ toolName: tc.name,
145
+ callId: tc.id ?? `call_${index}`,
146
+ argsText: tc.args.length ? tc.args : "{}",
147
+ };
148
+ }),
149
+ usage,
150
+ };
128
151
  }
129
152
  if (!content)
130
- return [];
131
- return [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }];
153
+ return { messages: [], usage };
154
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
132
155
  }
133
156
  const text = await res.text();
134
157
  let data;
@@ -140,16 +163,20 @@ async function openaiAdapter(config, context, tools) {
140
163
  }
141
164
  if (data.error)
142
165
  throw new Error(`OpenAI API error: ${data.error.message}`);
166
+ const usage = toUsage(data.usage);
143
167
  const msg = data.choices?.[0]?.message;
144
168
  if (!msg)
145
169
  throw new Error("OpenAI API returned empty response");
146
170
  if (msg.tool_calls?.length) {
147
- return msg.tool_calls.map((tc) => ({
148
- role: types_1.Role.ToolCall,
149
- toolName: tc.function.name,
150
- callId: tc.id,
151
- argsText: tc.function.arguments ?? "{}",
152
- }));
171
+ return {
172
+ messages: msg.tool_calls.map((tc) => ({
173
+ role: types_1.Role.ToolCall,
174
+ toolName: tc.function.name,
175
+ callId: tc.id,
176
+ argsText: tc.function.arguments ?? "{}",
177
+ })),
178
+ usage,
179
+ };
153
180
  }
154
- return [{ role: types_1.Role.Ai, content: msg.content ?? "" }];
181
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
155
182
  }
@@ -1,2 +1,2 @@
1
- import { type Message, type Tool } from "../types";
2
- export declare function openrouterAdapter(config: Record<string, unknown>, context: Message[], tools: Tool[]): Promise<Message[]>;
1
+ import { type AdapterResult, type Message, type Tool } from "../types";
2
+ export declare function openrouterAdapter(config: Record<string, unknown>, context: Message[], tools: Tool[]): Promise<AdapterResult>;
@@ -27,10 +27,23 @@ function toOpenRouterMessages(context) {
27
27
  return out;
28
28
  }, []);
29
29
  }
30
+ function toUsage(usage) {
31
+ if (!usage)
32
+ return undefined;
33
+ const promptTokens = usage.prompt_tokens;
34
+ const completionTokens = usage.completion_tokens;
35
+ const totalTokens = usage.total_tokens;
36
+ if (typeof promptTokens !== "number"
37
+ || typeof completionTokens !== "number"
38
+ || typeof totalTokens !== "number")
39
+ return undefined;
40
+ return { promptTokens, completionTokens, totalTokens };
41
+ }
30
42
  async function openrouterAdapter(config, context, tools) {
31
43
  const baseUrl = config.baseUrl ?? "https://openrouter.ai/api/v1";
32
44
  const apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || "";
33
45
  const model = config.model ?? "gpt-5-nano";
46
+ const toolChoice = config.toolChoice;
34
47
  const httpReferer = config.httpReferer;
35
48
  const title = config.title;
36
49
  const onStream = config.onStream;
@@ -46,8 +59,9 @@ async function openrouterAdapter(config, context, tools) {
46
59
  model,
47
60
  messages,
48
61
  tools: tools.length ? tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters ?? {} } })) : undefined,
49
- tool_choice: tools.length ? "auto" : undefined,
62
+ tool_choice: tools.length ? (toolChoice ?? "auto") : undefined,
50
63
  stream: onStream ? true : undefined,
64
+ stream_options: onStream ? { include_usage: true } : undefined,
51
65
  };
52
66
  const headers = {
53
67
  "Content-Type": "application/json",
@@ -58,7 +72,7 @@ async function openrouterAdapter(config, context, tools) {
58
72
  if (title)
59
73
  headers["X-Title"] = title;
60
74
  if (await check())
61
- return [];
75
+ return { messages: [] };
62
76
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
63
77
  method: "POST",
64
78
  headers,
@@ -80,6 +94,7 @@ async function openrouterAdapter(config, context, tools) {
80
94
  }
81
95
  if (onStream) {
82
96
  let content = "";
97
+ let usage;
83
98
  const toolCalls = new Map();
84
99
  const upsertToolCall = (tc) => {
85
100
  const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
@@ -108,6 +123,11 @@ async function openrouterAdapter(config, context, tools) {
108
123
  catch {
109
124
  throw new Error(`OpenRouter API returned invalid JSON: ${dataLine.slice(0, 200)}`);
110
125
  }
126
+ if (parsed?.usage) {
127
+ const nextUsage = toUsage(parsed.usage);
128
+ if (nextUsage)
129
+ usage = nextUsage;
130
+ }
111
131
  const delta = parsed?.choices?.[0]?.delta;
112
132
  if (!delta)
113
133
  return;
@@ -123,22 +143,25 @@ async function openrouterAdapter(config, context, tools) {
123
143
  await check();
124
144
  }, check);
125
145
  if (toolCalls.size > 0) {
126
- return [...toolCalls.entries()]
127
- .sort((a, b) => a[0] - b[0])
128
- .map(([index, tc]) => {
129
- if (!tc.name)
130
- throw new Error(`OpenRouter streaming tool call missing function name at index ${index}`);
131
- return {
132
- role: types_1.Role.ToolCall,
133
- toolName: tc.name,
134
- callId: tc.id ?? `call_${index}`,
135
- argsText: tc.args.length ? tc.args : "{}",
136
- };
137
- });
146
+ return {
147
+ messages: [...toolCalls.entries()]
148
+ .sort((a, b) => a[0] - b[0])
149
+ .map(([index, tc]) => {
150
+ if (!tc.name)
151
+ throw new Error(`OpenRouter streaming tool call missing function name at index ${index}`);
152
+ return {
153
+ role: types_1.Role.ToolCall,
154
+ toolName: tc.name,
155
+ callId: tc.id ?? `call_${index}`,
156
+ argsText: tc.args.length ? tc.args : "{}",
157
+ };
158
+ }),
159
+ usage,
160
+ };
138
161
  }
139
162
  if (!content)
140
- return [];
141
- return [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }];
163
+ return { messages: [], usage };
164
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
142
165
  }
143
166
  const text = await res.text();
144
167
  let data;
@@ -150,16 +173,20 @@ async function openrouterAdapter(config, context, tools) {
150
173
  }
151
174
  if (data.error)
152
175
  throw new Error(`OpenRouter API error: ${data.error.message}`);
176
+ const usage = toUsage(data.usage);
153
177
  const msg = data.choices?.[0]?.message;
154
178
  if (!msg)
155
179
  throw new Error("OpenRouter API returned empty response");
156
180
  if (msg.tool_calls?.length) {
157
- return msg.tool_calls.map((tc) => ({
158
- role: types_1.Role.ToolCall,
159
- toolName: tc.function.name,
160
- callId: tc.id,
161
- argsText: tc.function.arguments ?? "{}",
162
- }));
181
+ return {
182
+ messages: msg.tool_calls.map((tc) => ({
183
+ role: types_1.Role.ToolCall,
184
+ toolName: tc.function.name,
185
+ callId: tc.id,
186
+ argsText: tc.function.arguments ?? "{}",
187
+ })),
188
+ usage,
189
+ };
163
190
  }
164
- return [{ role: types_1.Role.Ai, content: msg.content ?? "" }];
191
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
165
192
  }
@@ -0,0 +1,2 @@
1
+ import { type AdapterResult, type Message, type Tool } from "../types";
2
+ export declare function selfhostChatCompletionsAdapter(config: Record<string, unknown>, context: Message[], tools: Tool[]): Promise<AdapterResult>;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selfhostChatCompletionsAdapter = selfhostChatCompletionsAdapter;
4
+ const abort_1 = require("../abort");
5
+ const types_1 = require("../types");
6
+ const sse_1 = require("./sse");
7
+ const ROLE_TO_OPENAI = {
8
+ [types_1.Role.System]: "system",
9
+ [types_1.Role.User]: "user",
10
+ [types_1.Role.Ai]: "assistant",
11
+ };
12
+ function toOpenAIMessages(context) {
13
+ return context.reduce((out, m) => {
14
+ if (m.role === types_1.Role.System || m.role === types_1.Role.User || m.role === types_1.Role.Ai)
15
+ return [...out, { role: ROLE_TO_OPENAI[m.role], content: m.content }];
16
+ if (m.role === types_1.Role.ToolResult)
17
+ return [...out, { role: "tool", content: m.content, tool_call_id: m.callId }];
18
+ if (m.role === types_1.Role.ToolCall) {
19
+ const tc = { id: m.callId, type: "function", function: { name: m.toolName, arguments: m.argsText ?? "{}" } };
20
+ const last = out[out.length - 1];
21
+ if (last?.tool_calls) {
22
+ last.tool_calls.push(tc);
23
+ return out;
24
+ }
25
+ return [...out, { role: "assistant", tool_calls: [tc] }];
26
+ }
27
+ return out;
28
+ }, []);
29
+ }
30
+ function toUsage(usage) {
31
+ if (!usage)
32
+ return undefined;
33
+ const promptTokens = usage.prompt_tokens;
34
+ const completionTokens = usage.completion_tokens;
35
+ const totalTokens = usage.total_tokens;
36
+ if (typeof promptTokens !== "number"
37
+ || typeof completionTokens !== "number"
38
+ || typeof totalTokens !== "number")
39
+ return undefined;
40
+ return { promptTokens, completionTokens, totalTokens };
41
+ }
42
+ async function selfhostChatCompletionsAdapter(config, context, tools) {
43
+ const baseUrl = config.baseUrl ?? "http://localhost:1234";
44
+ const apiKey = config.apiKey || process.env.SELFHOST_API_KEY || "";
45
+ const model = config.model ?? "gpt-5-nano";
46
+ const toolChoice = config.toolChoice;
47
+ const onStream = config.onStream;
48
+ const { check, isAborted } = (0, abort_1.createAbortChecker)(config.isAbort);
49
+ if (!apiKey)
50
+ throw new Error("Self-host chat completions adapter requires apiKey in config or SELFHOST_API_KEY env");
51
+ const contextMessages = toOpenAIMessages(context);
52
+ const systemContent = config.system;
53
+ const messages = systemContent
54
+ ? [{ role: "system", content: systemContent }, ...contextMessages]
55
+ : contextMessages;
56
+ const body = {
57
+ model,
58
+ messages,
59
+ tools: tools.length ? tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters ?? {} } })) : undefined,
60
+ tool_choice: tools.length ? (toolChoice ?? "auto") : undefined,
61
+ stream: onStream ? true : undefined,
62
+ stream_options: onStream ? { include_usage: true } : undefined,
63
+ };
64
+ if (await check())
65
+ return { messages: [] };
66
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
69
+ body: JSON.stringify(body),
70
+ });
71
+ if (!res.ok) {
72
+ const text = await res.text();
73
+ let errMsg = `Self-host chat completions API HTTP ${res.status}`;
74
+ try {
75
+ const parsed = JSON.parse(text);
76
+ if (parsed?.error?.message)
77
+ errMsg = parsed.error.message;
78
+ }
79
+ catch {
80
+ if (text)
81
+ errMsg += `: ${text.slice(0, 200)}`;
82
+ }
83
+ throw new Error(errMsg);
84
+ }
85
+ if (onStream) {
86
+ let content = "";
87
+ let usage;
88
+ const toolCalls = new Map();
89
+ const upsertToolCall = (tc) => {
90
+ const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
91
+ let entry = toolCalls.get(index);
92
+ if (!entry) {
93
+ entry = { args: "" };
94
+ toolCalls.set(index, entry);
95
+ }
96
+ if (typeof tc?.id === "string")
97
+ entry.id = tc.id;
98
+ const fn = tc?.function;
99
+ if (fn) {
100
+ if (typeof fn.name === "string")
101
+ entry.name = fn.name;
102
+ if (typeof fn.arguments === "string")
103
+ entry.args += fn.arguments;
104
+ }
105
+ };
106
+ await (0, sse_1.readSse)(res, async (dataLine) => {
107
+ if (dataLine === "[DONE]")
108
+ return;
109
+ let parsed;
110
+ try {
111
+ parsed = JSON.parse(dataLine);
112
+ }
113
+ catch {
114
+ throw new Error(`Self-host chat completions API returned invalid JSON: ${dataLine.slice(0, 200)}`);
115
+ }
116
+ if (parsed?.usage) {
117
+ const nextUsage = toUsage(parsed.usage);
118
+ if (nextUsage)
119
+ usage = nextUsage;
120
+ }
121
+ const delta = parsed?.choices?.[0]?.delta;
122
+ if (!delta)
123
+ return;
124
+ if (Array.isArray(delta.tool_calls)) {
125
+ for (const tc of delta.tool_calls) {
126
+ upsertToolCall(tc);
127
+ }
128
+ }
129
+ if (typeof delta.content === "string") {
130
+ content += delta.content;
131
+ await Promise.resolve(onStream(delta.content));
132
+ }
133
+ await check();
134
+ }, check);
135
+ if (toolCalls.size > 0) {
136
+ return {
137
+ messages: [...toolCalls.entries()]
138
+ .sort((a, b) => a[0] - b[0])
139
+ .map(([index, tc]) => {
140
+ if (!tc.name)
141
+ throw new Error(`Self-host streaming tool call missing function name at index ${index}`);
142
+ return {
143
+ role: types_1.Role.ToolCall,
144
+ toolName: tc.name,
145
+ callId: tc.id ?? `call_${index}`,
146
+ argsText: tc.args.length ? tc.args : "{}",
147
+ };
148
+ }),
149
+ usage,
150
+ };
151
+ }
152
+ if (!content)
153
+ return { messages: [], usage };
154
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
155
+ }
156
+ const text = await res.text();
157
+ let data;
158
+ try {
159
+ data = JSON.parse(text);
160
+ }
161
+ catch {
162
+ throw new Error(`Self-host chat completions API returned invalid JSON: ${text.slice(0, 200)}`);
163
+ }
164
+ if (data.error)
165
+ throw new Error(`Self-host chat completions API error: ${data.error.message}`);
166
+ const usage = toUsage(data.usage);
167
+ const msg = data.choices?.[0]?.message;
168
+ if (!msg)
169
+ throw new Error("Self-host chat completions API returned empty response");
170
+ if (msg.tool_calls?.length) {
171
+ return {
172
+ messages: msg.tool_calls.map((tc) => ({
173
+ role: types_1.Role.ToolCall,
174
+ toolName: tc.function.name,
175
+ callId: tc.id,
176
+ argsText: tc.function.arguments ?? "{}",
177
+ })),
178
+ usage,
179
+ };
180
+ }
181
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
182
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { AgentConfig, AgentLike, Context, Message, Tool } from "./types";
1
+ import type { AgentConfig, AgentLike, Context, Message, Tool, Usage } from "./types";
2
2
  export { openaiAdapter } from "./adapter/openai";
3
3
  export { openrouterAdapter } from "./adapter/openrouter";
4
- export type { Adapter, AgentConfig, AgentLike, Context, Message, Tool } from "./types";
4
+ export { selfhostChatCompletionsAdapter } from "./adapter/selfhost_chat_completions";
5
+ export type { Adapter, AgentConfig, AgentLike, Context, Message, Tool, Usage, AdapterResult } from "./types";
5
6
  export { Role } from "./types";
6
7
  export declare class Agent implements AgentLike {
7
8
  context: Context;
@@ -14,5 +15,9 @@ export declare class Agent implements AgentLike {
14
15
  private onToolResult?;
15
16
  constructor(adapterName: string, config: AgentConfig);
16
17
  registerTool(tool: Tool): void;
17
- run(message: string): Promise<Message[]>;
18
+ run(message: string): Promise<{
19
+ messages: Message[];
20
+ usage?: Usage;
21
+ usageSum?: Usage;
22
+ }>;
18
23
  }
package/dist/index.js CHANGED
@@ -1,19 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Agent = exports.Role = exports.openrouterAdapter = exports.openaiAdapter = void 0;
3
+ exports.Agent = exports.Role = exports.selfhostChatCompletionsAdapter = exports.openrouterAdapter = exports.openaiAdapter = void 0;
4
4
  const openai_1 = require("./adapter/openai");
5
5
  const openrouter_1 = require("./adapter/openrouter");
6
+ const selfhost_chat_completions_1 = require("./adapter/selfhost_chat_completions");
6
7
  const abort_1 = require("./abort");
7
8
  const types_1 = require("./types");
8
9
  var openai_2 = require("./adapter/openai");
9
10
  Object.defineProperty(exports, "openaiAdapter", { enumerable: true, get: function () { return openai_2.openaiAdapter; } });
10
11
  var openrouter_2 = require("./adapter/openrouter");
11
12
  Object.defineProperty(exports, "openrouterAdapter", { enumerable: true, get: function () { return openrouter_2.openrouterAdapter; } });
13
+ var selfhost_chat_completions_2 = require("./adapter/selfhost_chat_completions");
14
+ Object.defineProperty(exports, "selfhostChatCompletionsAdapter", { enumerable: true, get: function () { return selfhost_chat_completions_2.selfhostChatCompletionsAdapter; } });
12
15
  var types_2 = require("./types");
13
16
  Object.defineProperty(exports, "Role", { enumerable: true, get: function () { return types_2.Role; } });
14
17
  const adapters = {
15
18
  openai: openai_1.openaiAdapter,
16
19
  openrouter: openrouter_1.openrouterAdapter,
20
+ selfhost_chat_completions: selfhost_chat_completions_1.selfhostChatCompletionsAdapter,
17
21
  };
18
22
  class Agent {
19
23
  constructor(adapterName, config) {
@@ -31,9 +35,11 @@ class Agent {
31
35
  }
32
36
  async run(message) {
33
37
  const all = [];
38
+ let usage;
39
+ let usageSum;
34
40
  const { check } = (0, abort_1.createAbortChecker)(this.config.isAbort);
35
41
  if (await check())
36
- return all;
42
+ return { messages: [] };
37
43
  const sessionContext = [...this.context];
38
44
  const persistToContext = (msgs) => {
39
45
  this.context.push(...msgs);
@@ -44,10 +50,26 @@ class Agent {
44
50
  const userMessage = { role: types_1.Role.User, content: message };
45
51
  pushToSession([userMessage]);
46
52
  persistToContext([userMessage]);
53
+ const addUsage = (next) => {
54
+ if (!next)
55
+ return;
56
+ usage = next;
57
+ if (!usageSum) {
58
+ usageSum = { ...next };
59
+ return;
60
+ }
61
+ usageSum = {
62
+ promptTokens: usageSum.promptTokens + next.promptTokens,
63
+ completionTokens: usageSum.completionTokens + next.completionTokens,
64
+ totalTokens: usageSum.totalTokens + next.totalTokens,
65
+ };
66
+ };
47
67
  const runAdapter = async () => {
48
68
  if (await check())
49
69
  return [];
50
- const msgs = await this.adapter(this.config, sessionContext, this.tools);
70
+ const result = await this.adapter(this.config, sessionContext, this.tools);
71
+ addUsage(result.usage);
72
+ const msgs = result.messages;
51
73
  pushToSession(msgs);
52
74
  persistToContext(msgs);
53
75
  all.push(...msgs);
@@ -55,16 +77,16 @@ class Agent {
55
77
  };
56
78
  let msgs = await runAdapter();
57
79
  if (await check())
58
- return all;
80
+ return { messages: all, usage, usageSum };
59
81
  for (;;) {
60
82
  if (await check())
61
- return all;
83
+ return { messages: all, usage, usageSum };
62
84
  const last = msgs[msgs.length - 1];
63
85
  if (this.endCondition(sessionContext, last))
64
- return all;
86
+ return { messages: all, usage, usageSum };
65
87
  const toolCalls = msgs.filter((m) => m.role === types_1.Role.ToolCall);
66
88
  if (toolCalls.length === 0)
67
- return all;
89
+ return { messages: all, usage, usageSum };
68
90
  const results = await Promise.all(toolCalls.map(async (m) => {
69
91
  if (await check())
70
92
  return null;
package/dist/types.d.ts CHANGED
@@ -27,10 +27,19 @@ export type Message = {
27
27
  isError?: boolean;
28
28
  };
29
29
  export type Context = Message[];
30
+ export type Usage = {
31
+ promptTokens: number;
32
+ completionTokens: number;
33
+ totalTokens: number;
34
+ };
30
35
  export interface AgentLike {
31
36
  context: Context;
32
37
  registerTool: (tool: Tool) => void;
33
- run: (message: string) => Promise<Message[]>;
38
+ run: (message: string) => Promise<{
39
+ messages: Message[];
40
+ usage?: Usage;
41
+ usageSum?: Usage;
42
+ }>;
34
43
  }
35
44
  export type Tool = {
36
45
  name: string;
@@ -43,6 +52,7 @@ export type AgentConfig = {
43
52
  endCondition?: (context: Message[], last: Message) => boolean;
44
53
  onStream?: (textDelta: string) => void | Promise<void>;
45
54
  isAbort?: () => boolean | Promise<boolean>;
55
+ toolChoice?: "auto" | "required" | "none" | Record<string, unknown>;
46
56
  onToolCall?: (message: Extract<Message, {
47
57
  role: Role.ToolCall;
48
58
  }>, args: unknown, agent: AgentLike) => boolean | void | Promise<boolean | void>;
@@ -51,4 +61,8 @@ export type AgentConfig = {
51
61
  }>, agent: AgentLike) => void | Promise<void>;
52
62
  [key: string]: unknown;
53
63
  };
54
- export type Adapter = (config: Record<string, unknown>, context: Message[], tools: Tool[]) => Promise<Message[]>;
64
+ export type AdapterResult = {
65
+ messages: Message[];
66
+ usage?: Usage;
67
+ };
68
+ export type Adapter = (config: Record<string, unknown>, context: Message[], tools: Tool[]) => Promise<AdapterResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arki-moe/agent-ts",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "Minimal Agent library, zero dependencies",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",