@arki-moe/agent-ts 4.0.0 → 5.1.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 +8 -2
- package/dist/abort.d.ts +5 -0
- package/dist/abort.js +22 -0
- package/dist/adapter/openai.js +9 -2
- package/dist/adapter/openrouter.js +9 -2
- package/dist/adapter/sse.d.ts +1 -1
- package/dist/adapter/sse.js +9 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +14 -4
- package/dist/types.d.ts +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ const getTimeTool: Tool = {
|
|
|
13
13
|
name: "get_time",
|
|
14
14
|
description: "Get the current time in ISO format",
|
|
15
15
|
parameters: { type: "object", properties: {} },
|
|
16
|
+
data: { category: "time" },
|
|
16
17
|
execute: (_args, _agent) => new Date().toISOString(),
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -54,7 +55,7 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
|
|
|
54
55
|
- `Agent(adapterName, config)` - Create Agent
|
|
55
56
|
- `agent.context` - Public property, complete conversation history
|
|
56
57
|
- `agent.registerTool(tool)` - Register tool
|
|
57
|
-
- `agent.run(message
|
|
58
|
+
- `agent.run(message)` - Execute tool chain automatically, returns all new `Message[]`
|
|
58
59
|
|
|
59
60
|
### Config
|
|
60
61
|
|
|
@@ -65,13 +66,18 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
|
|
|
65
66
|
| `system` | `string` | Optional system prompt |
|
|
66
67
|
| `endCondition` | `(context, last) => boolean` | Stop condition for `run`. Defaults to `last.role === Role.Ai` |
|
|
67
68
|
| `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
|
+
| `isAbort` | `() => boolean \| Promise<boolean>` | Abort hook polled during `run`; return `true` to stop early and return partial results. |
|
|
68
70
|
| `onToolCall` | `(message, args, agent) => boolean \| void \| Promise<boolean \| void>` | Called before each tool execution; return `false` to skip tool execution and `onToolResult` |
|
|
69
71
|
| `onToolResult` | `(message, agent) => void \| Promise<void>` | Called after each tool execution (`message.role === Role.ToolResult`) |
|
|
70
72
|
|
|
71
|
-
`agent.run` always appends new messages to `agent.context`.
|
|
73
|
+
`agent.run` always appends new messages to `agent.context`. Multiple tool calls in a single model response are executed in parallel.
|
|
72
74
|
|
|
73
75
|
`onToolCall` receives parsed JSON args and can mutate them before execution. Returning `false` skips the tool call and does not emit a `ToolResult` message.
|
|
74
76
|
|
|
77
|
+
`tool.data` is a local JSON metadata bag for your own use and is not sent to adapters.
|
|
78
|
+
|
|
79
|
+
`isAbort` is polled on the hot path (including streaming). When it returns `true`, `agent.run` stops and returns whatever messages it has so far. Streaming abort returns a partial AI message with `isPartial: true`.
|
|
80
|
+
|
|
75
81
|
## Scripts
|
|
76
82
|
|
|
77
83
|
| Command | Description |
|
package/dist/abort.d.ts
ADDED
package/dist/abort.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAbortChecker = createAbortChecker;
|
|
4
|
+
function createAbortChecker(isAbort) {
|
|
5
|
+
let aborted = false;
|
|
6
|
+
const check = async () => {
|
|
7
|
+
if (!isAbort)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
const result = await Promise.resolve(isAbort());
|
|
11
|
+
if (result)
|
|
12
|
+
aborted = true;
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
aborted = true;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const isAborted = () => aborted;
|
|
21
|
+
return { check, isAborted };
|
|
22
|
+
}
|
package/dist/adapter/openai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.openaiAdapter = openaiAdapter;
|
|
4
|
+
const abort_1 = require("../abort");
|
|
4
5
|
const types_1 = require("../types");
|
|
5
6
|
const sse_1 = require("./sse");
|
|
6
7
|
const ROLE_TO_OPENAI = {
|
|
@@ -31,6 +32,7 @@ async function openaiAdapter(config, context, tools) {
|
|
|
31
32
|
const apiKey = config.apiKey || process.env.OPENAI_API_KEY || "";
|
|
32
33
|
const model = config.model ?? "gpt-5-nano";
|
|
33
34
|
const onStream = config.onStream;
|
|
35
|
+
const { check, isAborted } = (0, abort_1.createAbortChecker)(config.isAbort);
|
|
34
36
|
if (!apiKey)
|
|
35
37
|
throw new Error("OpenAI adapter requires apiKey in config or OPENAI_API_KEY env");
|
|
36
38
|
const contextMessages = toOpenAIMessages(context);
|
|
@@ -45,6 +47,8 @@ async function openaiAdapter(config, context, tools) {
|
|
|
45
47
|
tool_choice: tools.length ? "auto" : undefined,
|
|
46
48
|
stream: onStream ? true : undefined,
|
|
47
49
|
};
|
|
50
|
+
if (await check())
|
|
51
|
+
return [];
|
|
48
52
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
|
|
49
53
|
method: "POST",
|
|
50
54
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -106,7 +110,8 @@ async function openaiAdapter(config, context, tools) {
|
|
|
106
110
|
content += delta.content;
|
|
107
111
|
await Promise.resolve(onStream(delta.content));
|
|
108
112
|
}
|
|
109
|
-
|
|
113
|
+
await check();
|
|
114
|
+
}, check);
|
|
110
115
|
if (toolCalls.size > 0) {
|
|
111
116
|
return [...toolCalls.entries()]
|
|
112
117
|
.sort((a, b) => a[0] - b[0])
|
|
@@ -121,7 +126,9 @@ async function openaiAdapter(config, context, tools) {
|
|
|
121
126
|
};
|
|
122
127
|
});
|
|
123
128
|
}
|
|
124
|
-
|
|
129
|
+
if (!content)
|
|
130
|
+
return [];
|
|
131
|
+
return [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }];
|
|
125
132
|
}
|
|
126
133
|
const text = await res.text();
|
|
127
134
|
let data;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.openrouterAdapter = openrouterAdapter;
|
|
4
|
+
const abort_1 = require("../abort");
|
|
4
5
|
const types_1 = require("../types");
|
|
5
6
|
const sse_1 = require("./sse");
|
|
6
7
|
const ROLE_TO_OPENROUTER = {
|
|
@@ -33,6 +34,7 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
33
34
|
const httpReferer = config.httpReferer;
|
|
34
35
|
const title = config.title;
|
|
35
36
|
const onStream = config.onStream;
|
|
37
|
+
const { check, isAborted } = (0, abort_1.createAbortChecker)(config.isAbort);
|
|
36
38
|
if (!apiKey)
|
|
37
39
|
throw new Error("OpenRouter adapter requires apiKey in config or OPENROUTER_API_KEY env");
|
|
38
40
|
const contextMessages = toOpenRouterMessages(context);
|
|
@@ -55,6 +57,8 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
55
57
|
headers["HTTP-Referer"] = httpReferer;
|
|
56
58
|
if (title)
|
|
57
59
|
headers["X-Title"] = title;
|
|
60
|
+
if (await check())
|
|
61
|
+
return [];
|
|
58
62
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
59
63
|
method: "POST",
|
|
60
64
|
headers,
|
|
@@ -116,7 +120,8 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
116
120
|
content += delta.content;
|
|
117
121
|
await Promise.resolve(onStream(delta.content));
|
|
118
122
|
}
|
|
119
|
-
|
|
123
|
+
await check();
|
|
124
|
+
}, check);
|
|
120
125
|
if (toolCalls.size > 0) {
|
|
121
126
|
return [...toolCalls.entries()]
|
|
122
127
|
.sort((a, b) => a[0] - b[0])
|
|
@@ -131,7 +136,9 @@ async function openrouterAdapter(config, context, tools) {
|
|
|
131
136
|
};
|
|
132
137
|
});
|
|
133
138
|
}
|
|
134
|
-
|
|
139
|
+
if (!content)
|
|
140
|
+
return [];
|
|
141
|
+
return [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }];
|
|
135
142
|
}
|
|
136
143
|
const text = await res.text();
|
|
137
144
|
let data;
|
package/dist/adapter/sse.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function readSse(res: Response, onData: (data: string) => void | Promise<void>): Promise<void>;
|
|
1
|
+
export declare function readSse(res: Response, onData: (data: string) => void | Promise<void>, isAbort?: () => boolean | Promise<boolean>): Promise<void>;
|
package/dist/adapter/sse.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readSse = readSse;
|
|
4
|
-
async function readSse(res, onData) {
|
|
4
|
+
async function readSse(res, onData, isAbort) {
|
|
5
5
|
const body = res.body;
|
|
6
6
|
if (!body)
|
|
7
7
|
throw new Error("Response body is empty");
|
|
@@ -10,12 +10,16 @@ async function readSse(res, onData) {
|
|
|
10
10
|
let buffer = "";
|
|
11
11
|
let dataLines = [];
|
|
12
12
|
for (;;) {
|
|
13
|
+
if (isAbort && (await isAbort()))
|
|
14
|
+
return;
|
|
13
15
|
const { value, done } = await reader.read();
|
|
14
16
|
if (done)
|
|
15
17
|
break;
|
|
16
18
|
buffer += decoder.decode(value, { stream: true });
|
|
17
19
|
let newlineIndex = buffer.indexOf("\n");
|
|
18
20
|
while (newlineIndex !== -1) {
|
|
21
|
+
if (isAbort && (await isAbort()))
|
|
22
|
+
return;
|
|
19
23
|
let line = buffer.slice(0, newlineIndex);
|
|
20
24
|
buffer = buffer.slice(newlineIndex + 1);
|
|
21
25
|
if (line.endsWith("\r"))
|
|
@@ -34,6 +38,8 @@ async function readSse(res, onData) {
|
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
if (buffer) {
|
|
41
|
+
if (isAbort && (await isAbort()))
|
|
42
|
+
return;
|
|
37
43
|
let line = buffer;
|
|
38
44
|
if (line.endsWith("\r"))
|
|
39
45
|
line = line.slice(0, -1);
|
|
@@ -41,6 +47,8 @@ async function readSse(res, onData) {
|
|
|
41
47
|
dataLines.push(line.slice(5).trimStart());
|
|
42
48
|
}
|
|
43
49
|
if (dataLines.length > 0) {
|
|
50
|
+
if (isAbort && (await isAbort()))
|
|
51
|
+
return;
|
|
44
52
|
const data = dataLines.join("\n");
|
|
45
53
|
await onData(data);
|
|
46
54
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AgentConfig, AgentLike, Context, Message,
|
|
1
|
+
import type { AgentConfig, AgentLike, Context, Message, Tool } from "./types";
|
|
2
2
|
export { openaiAdapter } from "./adapter/openai";
|
|
3
3
|
export { openrouterAdapter } from "./adapter/openrouter";
|
|
4
|
-
export type { Adapter, AgentConfig, AgentLike, Context, Message,
|
|
4
|
+
export type { Adapter, AgentConfig, AgentLike, Context, Message, Tool } from "./types";
|
|
5
5
|
export { Role } from "./types";
|
|
6
6
|
export declare class Agent implements AgentLike {
|
|
7
7
|
context: Context;
|
|
@@ -14,5 +14,5 @@ export declare class Agent implements AgentLike {
|
|
|
14
14
|
private onToolResult?;
|
|
15
15
|
constructor(adapterName: string, config: AgentConfig);
|
|
16
16
|
registerTool(tool: Tool): void;
|
|
17
|
-
run(message: string
|
|
17
|
+
run(message: string): Promise<Message[]>;
|
|
18
18
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Agent = exports.Role = exports.openrouterAdapter = exports.openaiAdapter = void 0;
|
|
4
4
|
const openai_1 = require("./adapter/openai");
|
|
5
5
|
const openrouter_1 = require("./adapter/openrouter");
|
|
6
|
+
const abort_1 = require("./abort");
|
|
6
7
|
const types_1 = require("./types");
|
|
7
8
|
var openai_2 = require("./adapter/openai");
|
|
8
9
|
Object.defineProperty(exports, "openaiAdapter", { enumerable: true, get: function () { return openai_2.openaiAdapter; } });
|
|
@@ -28,9 +29,11 @@ class Agent {
|
|
|
28
29
|
registerTool(tool) {
|
|
29
30
|
this.tools.push(tool);
|
|
30
31
|
}
|
|
31
|
-
async run(message
|
|
32
|
-
const once = options.once ?? false;
|
|
32
|
+
async run(message) {
|
|
33
33
|
const all = [];
|
|
34
|
+
const { check } = (0, abort_1.createAbortChecker)(this.config.isAbort);
|
|
35
|
+
if (await check())
|
|
36
|
+
return all;
|
|
34
37
|
const sessionContext = [...this.context];
|
|
35
38
|
const persistToContext = (msgs) => {
|
|
36
39
|
this.context.push(...msgs);
|
|
@@ -40,9 +43,10 @@ class Agent {
|
|
|
40
43
|
};
|
|
41
44
|
const userMessage = { role: types_1.Role.User, content: message };
|
|
42
45
|
pushToSession([userMessage]);
|
|
43
|
-
|
|
44
|
-
persistToContext([userMessage]);
|
|
46
|
+
persistToContext([userMessage]);
|
|
45
47
|
const runAdapter = async () => {
|
|
48
|
+
if (await check())
|
|
49
|
+
return [];
|
|
46
50
|
const msgs = await this.adapter(this.config, sessionContext, this.tools);
|
|
47
51
|
pushToSession(msgs);
|
|
48
52
|
persistToContext(msgs);
|
|
@@ -50,7 +54,11 @@ class Agent {
|
|
|
50
54
|
return msgs;
|
|
51
55
|
};
|
|
52
56
|
let msgs = await runAdapter();
|
|
57
|
+
if (await check())
|
|
58
|
+
return all;
|
|
53
59
|
for (;;) {
|
|
60
|
+
if (await check())
|
|
61
|
+
return all;
|
|
54
62
|
const last = msgs[msgs.length - 1];
|
|
55
63
|
if (this.endCondition(sessionContext, last))
|
|
56
64
|
return all;
|
|
@@ -58,6 +66,8 @@ class Agent {
|
|
|
58
66
|
if (toolCalls.length === 0)
|
|
59
67
|
return all;
|
|
60
68
|
const results = await Promise.all(toolCalls.map(async (m) => {
|
|
69
|
+
if (await check())
|
|
70
|
+
return null;
|
|
61
71
|
const tool = this.tools.find((t) => t.name === m.toolName);
|
|
62
72
|
if (!tool)
|
|
63
73
|
throw new Error(`Tool "${m.toolName}" is not registered`);
|
package/dist/types.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type Message = {
|
|
|
14
14
|
} | {
|
|
15
15
|
role: Role.Ai;
|
|
16
16
|
content: string;
|
|
17
|
+
isPartial?: boolean;
|
|
17
18
|
} | {
|
|
18
19
|
role: Role.ToolCall;
|
|
19
20
|
toolName: string;
|
|
@@ -29,17 +30,19 @@ export type Context = Message[];
|
|
|
29
30
|
export interface AgentLike {
|
|
30
31
|
context: Context;
|
|
31
32
|
registerTool: (tool: Tool) => void;
|
|
32
|
-
run: (message: string
|
|
33
|
+
run: (message: string) => Promise<Message[]>;
|
|
33
34
|
}
|
|
34
35
|
export type Tool = {
|
|
35
36
|
name: string;
|
|
36
37
|
description: string;
|
|
37
38
|
parameters: unknown;
|
|
39
|
+
data?: Record<string, unknown>;
|
|
38
40
|
execute: (args: unknown, agent: AgentLike) => Promise<unknown> | unknown;
|
|
39
41
|
};
|
|
40
42
|
export type AgentConfig = {
|
|
41
43
|
endCondition?: (context: Message[], last: Message) => boolean;
|
|
42
44
|
onStream?: (textDelta: string) => void | Promise<void>;
|
|
45
|
+
isAbort?: () => boolean | Promise<boolean>;
|
|
43
46
|
onToolCall?: (message: Extract<Message, {
|
|
44
47
|
role: Role.ToolCall;
|
|
45
48
|
}>, args: unknown, agent: AgentLike) => boolean | void | Promise<boolean | void>;
|
|
@@ -48,7 +51,4 @@ export type AgentConfig = {
|
|
|
48
51
|
}>, agent: AgentLike) => void | Promise<void>;
|
|
49
52
|
[key: string]: unknown;
|
|
50
53
|
};
|
|
51
|
-
export type RunOptions = {
|
|
52
|
-
once?: boolean;
|
|
53
|
-
};
|
|
54
54
|
export type Adapter = (config: Record<string, unknown>, context: Message[], tools: Tool[]) => Promise<Message[]>;
|