@arki-moe/agent-ts 5.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 CHANGED
@@ -66,6 +66,7 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
66
66
  | `system` | `string` | Optional system prompt |
67
67
  | `endCondition` | `(context, last) => boolean` | Stop condition for `run`. Defaults to `last.role === Role.Ai` |
68
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. |
69
70
  | `onToolCall` | `(message, args, agent) => boolean \| void \| Promise<boolean \| void>` | Called before each tool execution; return `false` to skip tool execution and `onToolResult` |
70
71
  | `onToolResult` | `(message, agent) => void \| Promise<void>` | Called after each tool execution (`message.role === Role.ToolResult`) |
71
72
 
@@ -75,6 +76,8 @@ When `apiKey` is not provided in config, adapters read from the corresponding en
75
76
 
76
77
  `tool.data` is a local JSON metadata bag for your own use and is not sent to adapters.
77
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
+
78
81
  ## Scripts
79
82
 
80
83
  | Command | Description |
@@ -0,0 +1,5 @@
1
+ export type AbortChecker = {
2
+ check: () => Promise<boolean>;
3
+ isAborted: () => boolean;
4
+ };
5
+ export declare function createAbortChecker(isAbort?: () => boolean | Promise<boolean>): AbortChecker;
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
+ }
@@ -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
- return [{ role: types_1.Role.Ai, content }];
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
- return [{ role: types_1.Role.Ai, content }];
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;
@@ -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>;
@@ -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.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; } });
@@ -30,6 +31,9 @@ class Agent {
30
31
  }
31
32
  async run(message) {
32
33
  const all = [];
34
+ const { check } = (0, abort_1.createAbortChecker)(this.config.isAbort);
35
+ if (await check())
36
+ return all;
33
37
  const sessionContext = [...this.context];
34
38
  const persistToContext = (msgs) => {
35
39
  this.context.push(...msgs);
@@ -41,6 +45,8 @@ class Agent {
41
45
  pushToSession([userMessage]);
42
46
  persistToContext([userMessage]);
43
47
  const runAdapter = async () => {
48
+ if (await check())
49
+ return [];
44
50
  const msgs = await this.adapter(this.config, sessionContext, this.tools);
45
51
  pushToSession(msgs);
46
52
  persistToContext(msgs);
@@ -48,7 +54,11 @@ class Agent {
48
54
  return msgs;
49
55
  };
50
56
  let msgs = await runAdapter();
57
+ if (await check())
58
+ return all;
51
59
  for (;;) {
60
+ if (await check())
61
+ return all;
52
62
  const last = msgs[msgs.length - 1];
53
63
  if (this.endCondition(sessionContext, last))
54
64
  return all;
@@ -56,6 +66,8 @@ class Agent {
56
66
  if (toolCalls.length === 0)
57
67
  return all;
58
68
  const results = await Promise.all(toolCalls.map(async (m) => {
69
+ if (await check())
70
+ return null;
59
71
  const tool = this.tools.find((t) => t.name === m.toolName);
60
72
  if (!tool)
61
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;
@@ -41,6 +42,7 @@ export type Tool = {
41
42
  export type AgentConfig = {
42
43
  endCondition?: (context: Message[], last: Message) => boolean;
43
44
  onStream?: (textDelta: string) => void | Promise<void>;
45
+ isAbort?: () => boolean | Promise<boolean>;
44
46
  onToolCall?: (message: Extract<Message, {
45
47
  role: Role.ToolCall;
46
48
  }>, args: unknown, agent: AgentLike) => boolean | void | Promise<boolean | void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arki-moe/agent-ts",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Minimal Agent library, zero dependencies",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",