@chatbotkit/agent 1.28.0 → 1.29.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
@@ -9,6 +9,20 @@
9
9
 
10
10
  Build autonomous AI agents that can use custom tools and execute complex tasks with the full power of the ChatBotKit platform.
11
11
 
12
+ ## Why ChatBotKit?
13
+
14
+ **Build lighter, future-proof AI agents.** When you build with ChatBotKit, the heavy lifting happens on our servers—not in your application. This architectural advantage delivers:
15
+
16
+ - 🪶 **Lightweight Agents**: Your agents stay lean because complex AI processing, model orchestration, and tool execution happen server-side. Less code in your app means faster load times and simpler maintenance.
17
+
18
+ - 🛡️ **Robust & Streamlined**: Server-side processing provides a more reliable experience with built-in error handling, automatic retries, and consistent behavior across all platforms.
19
+
20
+ - 🔄 **Backward & Forward Compatible**: As AI technology evolves—new models, new capabilities, new paradigms—your agents automatically benefit. No code changes required on your end.
21
+
22
+ - 🔮 **Future-Proof**: Agents you build today will remain capable tomorrow. When we add support for new AI models or capabilities, your existing agents gain those powers without any updates to your codebase.
23
+
24
+ This means you can focus on building great user experiences while ChatBotKit handles the complexity of the ever-changing AI landscape.
25
+
12
26
  ## Installation
13
27
 
14
28
  ```bash
@@ -178,6 +192,59 @@ The `execute` mode provides system tools for task management:
178
192
  - **`progress`** - Track completion status and blockers
179
193
  - **`exit`** - Signal task completion with status code
180
194
 
195
+ ### Skills Loading
196
+
197
+ Load skills from local directories and pass them as a feature to the agent. Skills are defined using `SKILL.md` files with front matter containing name and description.
198
+
199
+ ```javascript
200
+ import { execute, loadSkills, createSkillsFeature } from '@chatbotkit/agent'
201
+ import { ChatBotKit } from '@chatbotkit/sdk'
202
+
203
+ const client = new ChatBotKit({ secret: process.env.CHATBOTKIT_API_TOKEN })
204
+
205
+ // Load skills from directories
206
+ const skillsResult = await loadSkills(['./skills'], { watch: true })
207
+
208
+ // Create the skills feature for the API
209
+ const skillsFeature = createSkillsFeature(skillsResult.skills)
210
+
211
+ const stream = execute({
212
+ client,
213
+ model: 'gpt-4o',
214
+ messages: [{ type: 'user', text: 'Help me with my task' }],
215
+ extensions: {
216
+ features: [skillsFeature],
217
+ },
218
+ })
219
+
220
+ for await (const event of stream) {
221
+ // Handle events
222
+ }
223
+
224
+ // Clean up when done
225
+ skillsResult.close()
226
+ ```
227
+
228
+ #### SKILL.md Format
229
+
230
+ Create a `SKILL.md` file in each skill directory:
231
+
232
+ ```markdown
233
+ ---
234
+ name: My Skill
235
+ description: A brief description of what this skill does
236
+ ---
237
+
238
+ # My Skill
239
+
240
+ Additional documentation for the skill...
241
+ ```
242
+
243
+ #### Skills API
244
+
245
+ - **`loadSkills(directories, options)`** - Load skills from directories containing SKILL.md files
246
+ - **`createSkillsFeature(skills)`** - Create a feature configuration for the API
247
+
181
248
  ## Documentation
182
249
 
183
250
  For comprehensive information about the ChatBotKit Agent SDK, including detailed documentation on its functionalities, helper methods, and configuration options, please visit our [type documentation page](https://chatbotkit.github.io/node-sdk/modules/_chatbotkit_agent.html).
@@ -5,7 +5,7 @@ exports.execute = execute;
5
5
  const zod_1 = require("zod");
6
6
  const zod_to_json_schema_1 = require("zod-to-json-schema");
7
7
  async function* complete(options) {
8
- const { client, tools, ...request } = options;
8
+ const { client, tools, abortSignal, ...request } = options;
9
9
  const channelToTool = new Map();
10
10
  const functions = tools
11
11
  ? Object.entries(tools).map(([name, tool]) => {
@@ -47,10 +47,13 @@ async function* complete(options) {
47
47
  .complete(null, {
48
48
  ...request,
49
49
  functions,
50
+ limits: {
51
+ iterations: 1,
52
+ },
50
53
  })
51
- .stream();
54
+ .stream({ abortSignal });
52
55
  const toolEventQueue = [];
53
- const runningTools = new Set();
56
+ const runningToolPromises = [];
54
57
  const executeToolAsync = async (channel, name, tool, args) => {
55
58
  try {
56
59
  let parsedArgs = args;
@@ -88,11 +91,11 @@ async function* complete(options) {
88
91
  message: { error: errorMessage },
89
92
  });
90
93
  }
91
- finally {
92
- runningTools.delete(channel);
93
- }
94
94
  };
95
95
  for await (const event of stream) {
96
+ if (abortSignal?.aborted) {
97
+ break;
98
+ }
96
99
  while (toolEventQueue.length > 0) {
97
100
  const toolEvent = toolEventQueue.shift();
98
101
  if (toolEvent) {
@@ -116,9 +119,7 @@ async function* complete(options) {
116
119
  },
117
120
  };
118
121
  const toolPromise = executeToolAsync(channel, name, tool, args);
119
- runningTools.add(channel);
120
- toolPromise.catch(() => {
121
- });
122
+ runningToolPromises.push(toolPromise);
122
123
  }
123
124
  }
124
125
  yield event;
@@ -129,8 +130,8 @@ async function* complete(options) {
129
130
  yield toolEvent;
130
131
  }
131
132
  }
132
- while (runningTools.size > 0) {
133
- await new Promise((resolve) => setTimeout(resolve, 10));
133
+ if (runningToolPromises.length > 0) {
134
+ await Promise.allSettled(runningToolPromises);
134
135
  while (toolEventQueue.length > 0) {
135
136
  const toolEvent = toolEventQueue.shift();
136
137
  if (toolEvent) {
@@ -140,9 +141,10 @@ async function* complete(options) {
140
141
  }
141
142
  }
142
143
  async function* execute(options) {
143
- const { client, tools = {}, maxIterations = 50, ...request } = options;
144
- const messages = [...(request.messages || [])];
144
+ const { client, tools = {}, maxIterations = 100, abortSignal, ...request } = options;
145
+ const messages = request.messages || [];
145
146
  let exitResult = null;
147
+ let internalAbort = null;
146
148
  const systemTools = {
147
149
  plan: {
148
150
  description: 'Create or update a plan for approaching the task. Break down the task into clear, actionable steps. Use this at the start and whenever you need to revise your approach.',
@@ -209,8 +211,29 @@ async function* execute(options) {
209
211
  };
210
212
  },
211
213
  },
214
+ abort: {
215
+ description: 'Immediately abort the current task. Use this when the user explicitly asks you to stop, cancel, or abort what you are doing. Set hard to true to kill running processes immediately.',
216
+ input: zod_1.z.object({
217
+ reason: zod_1.z.string().optional().describe('Brief reason for aborting'),
218
+ hard: zod_1.z
219
+ .boolean()
220
+ .optional()
221
+ .describe('If true, immediately kill running processes. If false (default), finish the current operation gracefully.'),
222
+ }),
223
+ handler: async (input) => {
224
+ const reason = input.reason || 'aborted by user request';
225
+ exitResult = { code: 1, message: reason };
226
+ if (input.hard && internalAbort) {
227
+ internalAbort.abort(reason);
228
+ }
229
+ return {
230
+ success: true,
231
+ message: `Task aborted: ${reason}`,
232
+ };
233
+ },
234
+ },
212
235
  };
213
- const allTools = { ...systemTools, ...tools };
236
+ const allTools = { ...tools, ...systemTools };
214
237
  const systemInstruction = `
215
238
  ${options.extensions?.backstory || ''}
216
239
 
@@ -222,31 +245,58 @@ The goal is to complete the assigned task efficiently and effectively. Follow th
222
245
  2. **Track Progress**: Regularly use the 'progress' function to update status and identify issues
223
246
  3. **Use Tools**: Leverage available tools to accomplish each step of your plan
224
247
  4. **Exit When Done**: Call the 'exit' function with code 0 when successful, or non-zero code if unable to complete
225
- 5. **Be Autonomous**: Work through the task systematically without waiting for additional input
248
+ 5. **Abort**: If the user asks you to stop, cancel, or abort, call the 'abort' function immediately. Use hard=true if processes are running that need to be killed right away.
249
+ 6. **Be Autonomous**: Work through the task systematically without waiting for additional input
250
+ 7. **Be Responsive**: If the user sends a new message while you are working, acknowledge it briefly and adjust your approach if needed. Always prioritize user input over your current plan.
226
251
  `.trim();
227
252
  let iteration = 0;
228
253
  while (iteration < maxIterations && exitResult === null) {
254
+ if (abortSignal?.aborted) {
255
+ exitResult = {
256
+ code: 1,
257
+ message: 'Task execution aborted',
258
+ };
259
+ break;
260
+ }
229
261
  iteration++;
230
262
  yield { type: 'iteration', data: { iteration } };
263
+ let lastEndReason = null;
264
+ internalAbort = new AbortController();
265
+ if (abortSignal?.aborted) {
266
+ internalAbort.abort(abortSignal.reason);
267
+ }
268
+ else if (abortSignal) {
269
+ const capturedAbort = internalAbort;
270
+ abortSignal.addEventListener('abort', () => capturedAbort.abort(abortSignal.reason), { once: true });
271
+ }
272
+ const iterSignal = internalAbort.signal;
231
273
  for await (const event of complete({
232
274
  ...request,
233
275
  client,
234
276
  messages,
235
277
  tools: allTools,
278
+ abortSignal: iterSignal,
236
279
  extensions: {
237
280
  ...options.extensions,
238
281
  backstory: systemInstruction,
239
282
  },
240
283
  })) {
284
+ if (event.type === 'message') {
285
+ messages.push(event.data);
286
+ }
287
+ if (event.type === 'result') {
288
+ if (event.data.end.reason) {
289
+ lastEndReason = event.data.end.reason;
290
+ }
291
+ }
241
292
  yield event;
242
293
  }
243
294
  if (exitResult) {
244
295
  break;
245
296
  }
246
- messages.push({
247
- type: 'user',
248
- text: 'Continue with the next step of your plan. If all steps are complete, call exit with the appropriate status code.',
249
- });
297
+ if (lastEndReason === 'stop') {
298
+ break;
299
+ }
250
300
  }
251
301
  if (exitResult === null) {
252
302
  exitResult = {
@@ -1,11 +1,13 @@
1
- export function complete(options: ConversationCompleteRequest & {
1
+ export function complete(options: Omit<ConversationCompleteRequest, "functions" | "limits"> & {
2
2
  client: ChatBotKit;
3
3
  tools?: Tools;
4
+ abortSignal?: AbortSignal;
4
5
  }): AsyncGenerator<ConversationCompleteStreamType | ToolCallStartEvent | ToolCallEndEvent | ToolCallErrorEvent, void, unknown>;
5
- export function execute(options: ConversationCompleteRequest & {
6
+ export function execute(options: Omit<ConversationCompleteRequest, "functions" | "limits"> & {
6
7
  client: ChatBotKit;
7
8
  tools?: Tools;
8
9
  maxIterations?: number;
10
+ abortSignal?: AbortSignal;
9
11
  }): AsyncGenerator<ConversationCompleteStreamType | ToolCallStartEvent | ToolCallEndEvent | ToolCallErrorEvent | IterationEvent | ExitEvent, void, unknown>;
10
12
  export type ZodObject = import("zod").ZodObject<any>;
11
13
  export type ChatBotKit = import("@chatbotkit/sdk").ChatBotKit;
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tools = exports.execute = exports.complete = void 0;
3
+ exports.createSkillsFeature = exports.loadSkills = exports.tools = exports.execute = exports.complete = void 0;
4
4
  var agent_js_1 = require("./agent.cjs");
5
5
  Object.defineProperty(exports, "complete", { enumerable: true, get: function () { return agent_js_1.complete; } });
6
6
  Object.defineProperty(exports, "execute", { enumerable: true, get: function () { return agent_js_1.execute; } });
7
7
  var tools_js_1 = require("./tools.cjs");
8
8
  Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_js_1.tools; } });
9
+ var skills_js_1 = require("./skills.cjs");
10
+ Object.defineProperty(exports, "loadSkills", { enumerable: true, get: function () { return skills_js_1.loadSkills; } });
11
+ Object.defineProperty(exports, "createSkillsFeature", { enumerable: true, get: function () { return skills_js_1.createSkillsFeature; } });
@@ -1,2 +1,3 @@
1
1
  export { tools } from "./tools.js";
2
2
  export { complete, execute } from "./agent.js";
3
+ export { loadSkills, createSkillsFeature } from "./skills.js";
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadSkills = loadSkills;
4
+ exports.createSkillsFeature = createSkillsFeature;
5
+ const tslib_1 = require("tslib");
6
+ const promises_1 = require("fs/promises");
7
+ const js_yaml_1 = tslib_1.__importDefault(require("js-yaml"));
8
+ const path_1 = require("path");
9
+ function parseFrontMatter(content) {
10
+ const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---/;
11
+ const match = content.match(frontMatterRegex);
12
+ if (!match) {
13
+ return {};
14
+ }
15
+ try {
16
+ const frontMatter = match[1];
17
+ const parsed = (js_yaml_1.default.load(frontMatter));
18
+ if (typeof parsed !== 'object' || parsed === null) {
19
+ return {};
20
+ }
21
+ return {
22
+ name: typeof parsed.name === 'string' ? parsed.name : undefined,
23
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
24
+ };
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ async function loadSkillFromDirectory(skillDir) {
31
+ const skillFilePath = (0, path_1.join)(skillDir, 'SKILL.md');
32
+ try {
33
+ const content = await (0, promises_1.readFile)(skillFilePath, 'utf-8');
34
+ const { name, description } = parseFrontMatter(content);
35
+ if (!name || !description) {
36
+ return null;
37
+ }
38
+ return {
39
+ name,
40
+ description,
41
+ path: (0, path_1.resolve)(skillDir),
42
+ };
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function loadSkills(directories, options = {}) {
49
+ const skills = ([]);
50
+ const watchControllers = ([]);
51
+ async function scanDirectory(baseDir) {
52
+ try {
53
+ const entries = await (0, promises_1.readdir)(baseDir);
54
+ for (const entry of entries) {
55
+ const entryPath = (0, path_1.join)(baseDir, entry);
56
+ const entryStat = await (0, promises_1.stat)(entryPath);
57
+ if (entryStat.isDirectory()) {
58
+ const skill = await loadSkillFromDirectory(entryPath);
59
+ if (skill) {
60
+ const existingIdx = skills.findIndex((s) => s.path === skill.path);
61
+ if (existingIdx !== -1) {
62
+ skills[existingIdx] = skill;
63
+ }
64
+ else {
65
+ skills.push(skill);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ }
73
+ }
74
+ for (const dir of directories) {
75
+ await scanDirectory(dir);
76
+ }
77
+ if (options.watch) {
78
+ for (const dir of directories) {
79
+ const controller = new AbortController();
80
+ watchControllers.push(controller);
81
+ (async () => {
82
+ try {
83
+ const watcher = (0, promises_1.watch)(dir, {
84
+ recursive: true,
85
+ signal: controller.signal,
86
+ });
87
+ for await (const event of watcher) {
88
+ if (event.filename?.endsWith('SKILL.md')) {
89
+ await scanDirectory(dir);
90
+ }
91
+ }
92
+ }
93
+ catch (err) {
94
+ if (err instanceof Error &&
95
+ err.name !== 'AbortError' &&
96
+ !err.message.includes('AbortError')) {
97
+ }
98
+ }
99
+ })();
100
+ }
101
+ }
102
+ return {
103
+ skills,
104
+ close: () => {
105
+ for (const controller of watchControllers) {
106
+ controller.abort();
107
+ }
108
+ },
109
+ };
110
+ }
111
+ function createSkillsFeature(skills) {
112
+ return {
113
+ name: ('skills'),
114
+ options: { skills },
115
+ };
116
+ }
@@ -0,0 +1,18 @@
1
+ export function loadSkills(directories: string[], options?: {
2
+ watch?: boolean;
3
+ }): Promise<SkillsResult>;
4
+ export function createSkillsFeature(skills: SkillDefinition[]): {
5
+ name: "skills";
6
+ options: {
7
+ skills: SkillDefinition[];
8
+ };
9
+ };
10
+ export type SkillDefinition = {
11
+ name: string;
12
+ description: string;
13
+ path: string;
14
+ };
15
+ export type SkillsResult = {
16
+ skills: SkillDefinition[];
17
+ close: () => void;
18
+ };
@@ -1,11 +1,13 @@
1
- export function complete(options: ConversationCompleteRequest & {
1
+ export function complete(options: Omit<ConversationCompleteRequest, "functions" | "limits"> & {
2
2
  client: ChatBotKit;
3
3
  tools?: Tools;
4
+ abortSignal?: AbortSignal;
4
5
  }): AsyncGenerator<ConversationCompleteStreamType | ToolCallStartEvent | ToolCallEndEvent | ToolCallErrorEvent, void, unknown>;
5
- export function execute(options: ConversationCompleteRequest & {
6
+ export function execute(options: Omit<ConversationCompleteRequest, "functions" | "limits"> & {
6
7
  client: ChatBotKit;
7
8
  tools?: Tools;
8
9
  maxIterations?: number;
10
+ abortSignal?: AbortSignal;
9
11
  }): AsyncGenerator<ConversationCompleteStreamType | ToolCallStartEvent | ToolCallEndEvent | ToolCallErrorEvent | IterationEvent | ExitEvent, void, unknown>;
10
12
  export type ZodObject = import("zod").ZodObject<any>;
11
13
  export type ChatBotKit = import("@chatbotkit/sdk").ChatBotKit;
package/dist/esm/agent.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
3
  export async function* complete(options) {
4
- const { client, tools, ...request } = options;
4
+ const { client, tools, abortSignal, ...request } = options;
5
5
  const channelToTool = new Map();
6
6
  const functions = tools
7
7
  ? Object.entries(tools).map(([name, tool]) => {
@@ -43,10 +43,13 @@ export async function* complete(options) {
43
43
  .complete(null, {
44
44
  ...request,
45
45
  functions,
46
+ limits: {
47
+ iterations: 1,
48
+ },
46
49
  })
47
- .stream();
50
+ .stream({ abortSignal });
48
51
  const toolEventQueue = [];
49
- const runningTools = new Set();
52
+ const runningToolPromises = [];
50
53
  const executeToolAsync = async (channel, name, tool, args) => {
51
54
  try {
52
55
  let parsedArgs = args;
@@ -84,11 +87,11 @@ export async function* complete(options) {
84
87
  message: { error: errorMessage },
85
88
  });
86
89
  }
87
- finally {
88
- runningTools.delete(channel);
89
- }
90
90
  };
91
91
  for await (const event of stream) {
92
+ if (abortSignal?.aborted) {
93
+ break;
94
+ }
92
95
  while (toolEventQueue.length > 0) {
93
96
  const toolEvent = toolEventQueue.shift();
94
97
  if (toolEvent) {
@@ -112,9 +115,7 @@ export async function* complete(options) {
112
115
  },
113
116
  };
114
117
  const toolPromise = executeToolAsync(channel, name, tool, args);
115
- runningTools.add(channel);
116
- toolPromise.catch(() => {
117
- });
118
+ runningToolPromises.push(toolPromise);
118
119
  }
119
120
  }
120
121
  yield event;
@@ -125,8 +126,8 @@ export async function* complete(options) {
125
126
  yield toolEvent;
126
127
  }
127
128
  }
128
- while (runningTools.size > 0) {
129
- await new Promise((resolve) => setTimeout(resolve, 10));
129
+ if (runningToolPromises.length > 0) {
130
+ await Promise.allSettled(runningToolPromises);
130
131
  while (toolEventQueue.length > 0) {
131
132
  const toolEvent = toolEventQueue.shift();
132
133
  if (toolEvent) {
@@ -136,9 +137,10 @@ export async function* complete(options) {
136
137
  }
137
138
  }
138
139
  export async function* execute(options) {
139
- const { client, tools = {}, maxIterations = 50, ...request } = options;
140
- const messages = [...(request.messages || [])];
140
+ const { client, tools = {}, maxIterations = 100, abortSignal, ...request } = options;
141
+ const messages = request.messages || [];
141
142
  let exitResult = null;
143
+ let internalAbort = null;
142
144
  const systemTools = {
143
145
  plan: {
144
146
  description: 'Create or update a plan for approaching the task. Break down the task into clear, actionable steps. Use this at the start and whenever you need to revise your approach.',
@@ -205,8 +207,29 @@ export async function* execute(options) {
205
207
  };
206
208
  },
207
209
  },
210
+ abort: {
211
+ description: 'Immediately abort the current task. Use this when the user explicitly asks you to stop, cancel, or abort what you are doing. Set hard to true to kill running processes immediately.',
212
+ input: z.object({
213
+ reason: z.string().optional().describe('Brief reason for aborting'),
214
+ hard: z
215
+ .boolean()
216
+ .optional()
217
+ .describe('If true, immediately kill running processes. If false (default), finish the current operation gracefully.'),
218
+ }),
219
+ handler: async (input) => {
220
+ const reason = input.reason || 'aborted by user request';
221
+ exitResult = { code: 1, message: reason };
222
+ if (input.hard && internalAbort) {
223
+ internalAbort.abort(reason);
224
+ }
225
+ return {
226
+ success: true,
227
+ message: `Task aborted: ${reason}`,
228
+ };
229
+ },
230
+ },
208
231
  };
209
- const allTools = { ...systemTools, ...tools };
232
+ const allTools = { ...tools, ...systemTools };
210
233
  const systemInstruction = `
211
234
  ${options.extensions?.backstory || ''}
212
235
 
@@ -218,31 +241,58 @@ The goal is to complete the assigned task efficiently and effectively. Follow th
218
241
  2. **Track Progress**: Regularly use the 'progress' function to update status and identify issues
219
242
  3. **Use Tools**: Leverage available tools to accomplish each step of your plan
220
243
  4. **Exit When Done**: Call the 'exit' function with code 0 when successful, or non-zero code if unable to complete
221
- 5. **Be Autonomous**: Work through the task systematically without waiting for additional input
244
+ 5. **Abort**: If the user asks you to stop, cancel, or abort, call the 'abort' function immediately. Use hard=true if processes are running that need to be killed right away.
245
+ 6. **Be Autonomous**: Work through the task systematically without waiting for additional input
246
+ 7. **Be Responsive**: If the user sends a new message while you are working, acknowledge it briefly and adjust your approach if needed. Always prioritize user input over your current plan.
222
247
  `.trim();
223
248
  let iteration = 0;
224
249
  while (iteration < maxIterations && exitResult === null) {
250
+ if (abortSignal?.aborted) {
251
+ exitResult = {
252
+ code: 1,
253
+ message: 'Task execution aborted',
254
+ };
255
+ break;
256
+ }
225
257
  iteration++;
226
258
  yield { type: 'iteration', data: { iteration } };
259
+ let lastEndReason = null;
260
+ internalAbort = new AbortController();
261
+ if (abortSignal?.aborted) {
262
+ internalAbort.abort(abortSignal.reason);
263
+ }
264
+ else if (abortSignal) {
265
+ const capturedAbort = internalAbort;
266
+ abortSignal.addEventListener('abort', () => capturedAbort.abort(abortSignal.reason), { once: true });
267
+ }
268
+ const iterSignal = internalAbort.signal;
227
269
  for await (const event of complete({
228
270
  ...request,
229
271
  client,
230
272
  messages,
231
273
  tools: allTools,
274
+ abortSignal: iterSignal,
232
275
  extensions: {
233
276
  ...options.extensions,
234
277
  backstory: systemInstruction,
235
278
  },
236
279
  })) {
280
+ if (event.type === 'message') {
281
+ messages.push(event.data);
282
+ }
283
+ if (event.type === 'result') {
284
+ if (event.data.end.reason) {
285
+ lastEndReason = event.data.end.reason;
286
+ }
287
+ }
237
288
  yield event;
238
289
  }
239
290
  if (exitResult) {
240
291
  break;
241
292
  }
242
- messages.push({
243
- type: 'user',
244
- text: 'Continue with the next step of your plan. If all steps are complete, call exit with the appropriate status code.',
245
- });
293
+ if (lastEndReason === 'stop') {
294
+ break;
295
+ }
246
296
  }
247
297
  if (exitResult === null) {
248
298
  exitResult = {
@@ -1,2 +1,3 @@
1
1
  export { tools } from "./tools.js";
2
2
  export { complete, execute } from "./agent.js";
3
+ export { loadSkills, createSkillsFeature } from "./skills.js";
package/dist/esm/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { complete, execute } from './agent.js';
2
2
  export { tools } from './tools.js';
3
+ export { loadSkills, createSkillsFeature } from './skills.js';
@@ -0,0 +1,18 @@
1
+ export function loadSkills(directories: string[], options?: {
2
+ watch?: boolean;
3
+ }): Promise<SkillsResult>;
4
+ export function createSkillsFeature(skills: SkillDefinition[]): {
5
+ name: "skills";
6
+ options: {
7
+ skills: SkillDefinition[];
8
+ };
9
+ };
10
+ export type SkillDefinition = {
11
+ name: string;
12
+ description: string;
13
+ path: string;
14
+ };
15
+ export type SkillsResult = {
16
+ skills: SkillDefinition[];
17
+ close: () => void;
18
+ };
@@ -0,0 +1,111 @@
1
+ import { readFile, readdir, stat, watch } from 'fs/promises';
2
+ import yaml from 'js-yaml';
3
+ import { join, resolve } from 'path';
4
+ function parseFrontMatter(content) {
5
+ const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---/;
6
+ const match = content.match(frontMatterRegex);
7
+ if (!match) {
8
+ return {};
9
+ }
10
+ try {
11
+ const frontMatter = match[1];
12
+ const parsed = (yaml.load(frontMatter));
13
+ if (typeof parsed !== 'object' || parsed === null) {
14
+ return {};
15
+ }
16
+ return {
17
+ name: typeof parsed.name === 'string' ? parsed.name : undefined,
18
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
19
+ };
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ async function loadSkillFromDirectory(skillDir) {
26
+ const skillFilePath = join(skillDir, 'SKILL.md');
27
+ try {
28
+ const content = await readFile(skillFilePath, 'utf-8');
29
+ const { name, description } = parseFrontMatter(content);
30
+ if (!name || !description) {
31
+ return null;
32
+ }
33
+ return {
34
+ name,
35
+ description,
36
+ path: resolve(skillDir),
37
+ };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export async function loadSkills(directories, options = {}) {
44
+ const skills = ([]);
45
+ const watchControllers = ([]);
46
+ async function scanDirectory(baseDir) {
47
+ try {
48
+ const entries = await readdir(baseDir);
49
+ for (const entry of entries) {
50
+ const entryPath = join(baseDir, entry);
51
+ const entryStat = await stat(entryPath);
52
+ if (entryStat.isDirectory()) {
53
+ const skill = await loadSkillFromDirectory(entryPath);
54
+ if (skill) {
55
+ const existingIdx = skills.findIndex((s) => s.path === skill.path);
56
+ if (existingIdx !== -1) {
57
+ skills[existingIdx] = skill;
58
+ }
59
+ else {
60
+ skills.push(skill);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ }
68
+ }
69
+ for (const dir of directories) {
70
+ await scanDirectory(dir);
71
+ }
72
+ if (options.watch) {
73
+ for (const dir of directories) {
74
+ const controller = new AbortController();
75
+ watchControllers.push(controller);
76
+ (async () => {
77
+ try {
78
+ const watcher = watch(dir, {
79
+ recursive: true,
80
+ signal: controller.signal,
81
+ });
82
+ for await (const event of watcher) {
83
+ if (event.filename?.endsWith('SKILL.md')) {
84
+ await scanDirectory(dir);
85
+ }
86
+ }
87
+ }
88
+ catch (err) {
89
+ if (err instanceof Error &&
90
+ err.name !== 'AbortError' &&
91
+ !err.message.includes('AbortError')) {
92
+ }
93
+ }
94
+ })();
95
+ }
96
+ }
97
+ return {
98
+ skills,
99
+ close: () => {
100
+ for (const controller of watchControllers) {
101
+ controller.abort();
102
+ }
103
+ },
104
+ };
105
+ }
106
+ export function createSkillsFeature(skills) {
107
+ return {
108
+ name: ('skills'),
109
+ options: { skills },
110
+ };
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatbotkit/agent",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "ChatBotKit Agent implementation",
5
5
  "license": "ISC",
6
6
  "engines": {
@@ -72,6 +72,26 @@
72
72
  "default": "./dist/cjs/index.cjs"
73
73
  }
74
74
  },
75
+ "./skills": {
76
+ "import": {
77
+ "types": "./dist/esm/skills.d.ts",
78
+ "default": "./dist/esm/skills.js"
79
+ },
80
+ "require": {
81
+ "types": "./dist/cjs/skills.d.ts",
82
+ "default": "./dist/cjs/skills.cjs"
83
+ }
84
+ },
85
+ "./skills.js": {
86
+ "import": {
87
+ "types": "./dist/esm/skills.d.ts",
88
+ "default": "./dist/esm/skills.js"
89
+ },
90
+ "require": {
91
+ "types": "./dist/cjs/skills.d.ts",
92
+ "default": "./dist/cjs/skills.cjs"
93
+ }
94
+ },
75
95
  "./tools": {
76
96
  "import": {
77
97
  "types": "./dist/esm/tools.d.ts",
@@ -96,12 +116,14 @@
96
116
  },
97
117
  "types": "./dist/cjs/index.d.ts",
98
118
  "dependencies": {
119
+ "js-yaml": "^4.1.0",
99
120
  "tslib": "^2.6.2",
100
121
  "zod": "^3.25.76",
101
122
  "zod-to-json-schema": "^3.24.6",
102
- "@chatbotkit/sdk": "1.28.0"
123
+ "@chatbotkit/sdk": "1.29.0"
103
124
  },
104
125
  "devDependencies": {
126
+ "@types/js-yaml": "^4.0.9",
105
127
  "npm-run-all": "^4.1.5",
106
128
  "typedoc": "^0.28.14",
107
129
  "typedoc-plugin-markdown": "^4.9.0",