@arki-moe/agent-ts 2.1.1 → 2.2.2
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 +4 -0
- package/dist/adapter/openai.js +64 -1
- package/dist/adapter/openrouter.js +64 -1
- package/dist/adapter/sse.d.ts +1 -0
- package/dist/adapter/sse.js +47 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,9 @@ const agent = new Agent("openai", {
|
|
|
20
20
|
apiKey: "sk-...",
|
|
21
21
|
model: "gpt-5-nano",
|
|
22
22
|
system: "You are a helpful assistant. Reply concisely.",
|
|
23
|
+
onStream: (textDelta) => {
|
|
24
|
+
process.stdout.write(textDelta);
|
|
25
|
+
},
|
|
23
26
|
onToolCall: (message, args) => {
|
|
24
27
|
console.log("tool call:", message);
|
|
25
28
|
console.log("tool args:", args);
|
|
@@ -61,6 +64,7 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
|
|
|
61
64
|
| `model` | `string` | Model name |
|
|
62
65
|
| `system` | `string` | Optional system prompt |
|
|
63
66
|
| `endCondition` | `(context, last) => boolean` | Stop condition for `run`. Defaults to `last.role === Role.Ai` |
|
|
67
|
+
| `onStream` | `(textDelta: string) => void \| Promise<void>` | Stream hook for AI text only. When provided, adapters use SSE streaming and still return the final `Message[]`. |
|
|
64
68
|
| `onToolCall` | `(message, args) => boolean \| void \| Promise<boolean \| void>` | Called before each tool execution; return `false` to skip tool execution and `onToolResult` |
|
|
65
69
|
| `onToolResult` | `(message) => void \| Promise<void>` | Called after each tool execution (`message.role === Role.ToolResult`) |
|
|
66
70
|
|
package/dist/adapter/openai.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.openaiAdapter = openaiAdapter;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
|
+
const sse_1 = require("./sse");
|
|
5
6
|
const ROLE_TO_OPENAI = {
|
|
6
7
|
[types_1.Role.System]: "system",
|
|
7
8
|
[types_1.Role.User]: "user",
|
|
@@ -29,6 +30,7 @@ async function openaiAdapter(config, context, tools) {
|
|
|
29
30
|
const baseUrl = config.baseUrl ?? "https://api.openai.com";
|
|
30
31
|
const apiKey = config.apiKey || process.env.OPENAI_API_KEY || "";
|
|
31
32
|
const model = config.model ?? "gpt-5-nano";
|
|
33
|
+
const onStream = config.onStream;
|
|
32
34
|
if (!apiKey)
|
|
33
35
|
throw new Error("OpenAI adapter requires apiKey in config or OPENAI_API_KEY env");
|
|
34
36
|
const contextMessages = toOpenAIMessages(context);
|
|
@@ -41,14 +43,15 @@ async function openaiAdapter(config, context, tools) {
|
|
|
41
43
|
messages,
|
|
42
44
|
tools: tools.length ? tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters ?? {} } })) : undefined,
|
|
43
45
|
tool_choice: tools.length ? "auto" : undefined,
|
|
46
|
+
stream: onStream ? true : undefined,
|
|
44
47
|
};
|
|
45
48
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
|
|
46
49
|
method: "POST",
|
|
47
50
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
48
51
|
body: JSON.stringify(body),
|
|
49
52
|
});
|
|
50
|
-
const text = await res.text();
|
|
51
53
|
if (!res.ok) {
|
|
54
|
+
const text = await res.text();
|
|
52
55
|
let errMsg = `OpenAI API HTTP ${res.status}`;
|
|
53
56
|
try {
|
|
54
57
|
const parsed = JSON.parse(text);
|
|
@@ -61,6 +64,66 @@ async function openaiAdapter(config, context, tools) {
|
|
|
61
64
|
}
|
|
62
65
|
throw new Error(errMsg);
|
|
63
66
|
}
|
|
67
|
+
if (onStream) {
|
|
68
|
+
let content = "";
|
|
69
|
+
const toolCalls = new Map();
|
|
70
|
+
const upsertToolCall = (tc) => {
|
|
71
|
+
const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
|
|
72
|
+
let entry = toolCalls.get(index);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
entry = { args: "" };
|
|
75
|
+
toolCalls.set(index, entry);
|
|
76
|
+
}
|
|
77
|
+
if (typeof tc?.id === "string")
|
|
78
|
+
entry.id = tc.id;
|
|
79
|
+
const fn = tc?.function;
|
|
80
|
+
if (fn) {
|
|
81
|
+
if (typeof fn.name === "string")
|
|
82
|
+
entry.name = fn.name;
|
|
83
|
+
if (typeof fn.arguments === "string")
|
|
84
|
+
entry.args += fn.arguments;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
await (0, sse_1.readSse)(res, async (dataLine) => {
|
|
88
|
+
if (dataLine === "[DONE]")
|
|
89
|
+
return;
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(dataLine);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error(`OpenAI API returned invalid JSON: ${dataLine.slice(0, 200)}`);
|
|
96
|
+
}
|
|
97
|
+
const delta = parsed?.choices?.[0]?.delta;
|
|
98
|
+
if (!delta)
|
|
99
|
+
return;
|
|
100
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
101
|
+
for (const tc of delta.tool_calls) {
|
|
102
|
+
upsertToolCall(tc);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (typeof delta.content === "string") {
|
|
106
|
+
content += delta.content;
|
|
107
|
+
await Promise.resolve(onStream(delta.content));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (toolCalls.size > 0) {
|
|
111
|
+
return [...toolCalls.entries()]
|
|
112
|
+
.sort((a, b) => a[0] - b[0])
|
|
113
|
+
.map(([index, tc]) => {
|
|
114
|
+
if (!tc.name)
|
|
115
|
+
throw new Error(`OpenAI streaming tool call missing function name at index ${index}`);
|
|
116
|
+
return {
|
|
117
|
+
role: types_1.Role.ToolCall,
|
|
118
|
+
toolName: tc.name,
|
|
119
|
+
callId: tc.id ?? `call_${index}`,
|
|
120
|
+
argsText: tc.args.length ? tc.args : "{}",
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return [{ role: types_1.Role.Ai, content }];
|
|
125
|
+
}
|
|
126
|
+
const text = await res.text();
|
|
64
127
|
let data;
|
|
65
128
|
try {
|
|
66
129
|
data = JSON.parse(text);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.openrouterAdapter = openrouterAdapter;
|
|
4
4
|
const types_1 = require("../types");
|
|
5
|
+
const sse_1 = require("./sse");
|
|
5
6
|
const ROLE_TO_OPENROUTER = {
|
|
6
7
|
[types_1.Role.System]: "system",
|
|
7
8
|
[types_1.Role.User]: "user",
|
|
@@ -31,6 +32,7 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
31
32
|
const model = config.model ?? "gpt-5-nano";
|
|
32
33
|
const httpReferer = config.httpReferer;
|
|
33
34
|
const title = config.title;
|
|
35
|
+
const onStream = config.onStream;
|
|
34
36
|
if (!apiKey)
|
|
35
37
|
throw new Error("OpenRouter adapter requires apiKey in config or OPENROUTER_API_KEY env");
|
|
36
38
|
const contextMessages = toOpenRouterMessages(context);
|
|
@@ -43,6 +45,7 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
43
45
|
messages,
|
|
44
46
|
tools: tools.length ? tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters ?? {} } })) : undefined,
|
|
45
47
|
tool_choice: tools.length ? "auto" : undefined,
|
|
48
|
+
stream: onStream ? true : undefined,
|
|
46
49
|
};
|
|
47
50
|
const headers = {
|
|
48
51
|
"Content-Type": "application/json",
|
|
@@ -57,8 +60,8 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
57
60
|
headers,
|
|
58
61
|
body: JSON.stringify(body),
|
|
59
62
|
});
|
|
60
|
-
const text = await res.text();
|
|
61
63
|
if (!res.ok) {
|
|
64
|
+
const text = await res.text();
|
|
62
65
|
let errMsg = `OpenRouter API HTTP ${res.status}`;
|
|
63
66
|
try {
|
|
64
67
|
const parsed = JSON.parse(text);
|
|
@@ -71,6 +74,66 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
71
74
|
}
|
|
72
75
|
throw new Error(errMsg);
|
|
73
76
|
}
|
|
77
|
+
if (onStream) {
|
|
78
|
+
let content = "";
|
|
79
|
+
const toolCalls = new Map();
|
|
80
|
+
const upsertToolCall = (tc) => {
|
|
81
|
+
const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
|
|
82
|
+
let entry = toolCalls.get(index);
|
|
83
|
+
if (!entry) {
|
|
84
|
+
entry = { args: "" };
|
|
85
|
+
toolCalls.set(index, entry);
|
|
86
|
+
}
|
|
87
|
+
if (typeof tc?.id === "string")
|
|
88
|
+
entry.id = tc.id;
|
|
89
|
+
const fn = tc?.function;
|
|
90
|
+
if (fn) {
|
|
91
|
+
if (typeof fn.name === "string")
|
|
92
|
+
entry.name = fn.name;
|
|
93
|
+
if (typeof fn.arguments === "string")
|
|
94
|
+
entry.args += fn.arguments;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await (0, sse_1.readSse)(res, async (dataLine) => {
|
|
98
|
+
if (dataLine === "[DONE]")
|
|
99
|
+
return;
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(dataLine);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`OpenRouter API returned invalid JSON: ${dataLine.slice(0, 200)}`);
|
|
106
|
+
}
|
|
107
|
+
const delta = parsed?.choices?.[0]?.delta;
|
|
108
|
+
if (!delta)
|
|
109
|
+
return;
|
|
110
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
111
|
+
for (const tc of delta.tool_calls) {
|
|
112
|
+
upsertToolCall(tc);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (typeof delta.content === "string") {
|
|
116
|
+
content += delta.content;
|
|
117
|
+
await Promise.resolve(onStream(delta.content));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (toolCalls.size > 0) {
|
|
121
|
+
return [...toolCalls.entries()]
|
|
122
|
+
.sort((a, b) => a[0] - b[0])
|
|
123
|
+
.map(([index, tc]) => {
|
|
124
|
+
if (!tc.name)
|
|
125
|
+
throw new Error(`OpenRouter streaming tool call missing function name at index ${index}`);
|
|
126
|
+
return {
|
|
127
|
+
role: types_1.Role.ToolCall,
|
|
128
|
+
toolName: tc.name,
|
|
129
|
+
callId: tc.id ?? `call_${index}`,
|
|
130
|
+
argsText: tc.args.length ? tc.args : "{}",
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return [{ role: types_1.Role.Ai, content }];
|
|
135
|
+
}
|
|
136
|
+
const text = await res.text();
|
|
74
137
|
let data;
|
|
75
138
|
try {
|
|
76
139
|
data = JSON.parse(text);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readSse(res: Response, onData: (data: string) => void | Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readSse = readSse;
|
|
4
|
+
async function readSse(res, onData) {
|
|
5
|
+
const body = res.body;
|
|
6
|
+
if (!body)
|
|
7
|
+
throw new Error("Response body is empty");
|
|
8
|
+
const reader = body.getReader();
|
|
9
|
+
const decoder = new TextDecoder("utf-8");
|
|
10
|
+
let buffer = "";
|
|
11
|
+
let dataLines = [];
|
|
12
|
+
for (;;) {
|
|
13
|
+
const { value, done } = await reader.read();
|
|
14
|
+
if (done)
|
|
15
|
+
break;
|
|
16
|
+
buffer += decoder.decode(value, { stream: true });
|
|
17
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
18
|
+
while (newlineIndex !== -1) {
|
|
19
|
+
let line = buffer.slice(0, newlineIndex);
|
|
20
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
21
|
+
if (line.endsWith("\r"))
|
|
22
|
+
line = line.slice(0, -1);
|
|
23
|
+
if (line === "") {
|
|
24
|
+
if (dataLines.length > 0) {
|
|
25
|
+
const data = dataLines.join("\n");
|
|
26
|
+
dataLines = [];
|
|
27
|
+
await onData(data);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (line.startsWith("data:")) {
|
|
31
|
+
dataLines.push(line.slice(5).trimStart());
|
|
32
|
+
}
|
|
33
|
+
newlineIndex = buffer.indexOf("\n");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (buffer) {
|
|
37
|
+
let line = buffer;
|
|
38
|
+
if (line.endsWith("\r"))
|
|
39
|
+
line = line.slice(0, -1);
|
|
40
|
+
if (line.startsWith("data:"))
|
|
41
|
+
dataLines.push(line.slice(5).trimStart());
|
|
42
|
+
}
|
|
43
|
+
if (dataLines.length > 0) {
|
|
44
|
+
const data = dataLines.join("\n");
|
|
45
|
+
await onData(data);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type Tool = {
|
|
|
34
34
|
};
|
|
35
35
|
export type AgentConfig = {
|
|
36
36
|
endCondition?: (context: Message[], last: Message) => boolean;
|
|
37
|
+
onStream?: (textDelta: string) => void | Promise<void>;
|
|
37
38
|
onToolCall?: (message: Extract<Message, {
|
|
38
39
|
role: Role.ToolCall;
|
|
39
40
|
}>, args: unknown) => boolean | void | Promise<boolean | void>;
|