@cuylabs/agent-core 0.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 ADDED
@@ -0,0 +1,275 @@
1
+ # @cuylabs/agent-core
2
+
3
+ Embeddable AI agent infrastructure using Vercel AI SDK. Core building blocks for streaming AI agents with session management, resilience, and model capabilities.
4
+
5
+ ## Features
6
+
7
+ - **Agent Framework** - Create AI agents with tool support and streaming responses
8
+ - **Session Management** - Persistent conversation state with branching support
9
+ - **LLM Streaming** - Real-time streaming with proper backpressure handling
10
+ - **Error Resilience** - Automatic retry with exponential backoff
11
+ - **Context Management** - Token counting and automatic context pruning
12
+ - **Tool Framework** - Type-safe tool definitions with Zod schemas
13
+ - **Approval System** - Configurable tool approval workflows
14
+ - **Checkpoint System** - Undo/restore capabilities for file operations
15
+ - **Model Capabilities** - Runtime model capability detection
16
+ - **MCP Support** - Extend agents with Model Context Protocol servers
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @cuylabs/agent-core
22
+ # or
23
+ pnpm add @cuylabs/agent-core
24
+ ```
25
+
26
+ You'll also need at least one AI SDK provider:
27
+
28
+ ```bash
29
+ npm install @ai-sdk/openai
30
+ # or @ai-sdk/anthropic, @ai-sdk/google
31
+ ```
32
+
33
+ For OpenAI-compatible endpoints (local or hosted), add:
34
+
35
+ ```bash
36
+ npm install @ai-sdk/openai-compatible
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```typescript
42
+ import { createAgent, Tool } from "@cuylabs/agent-core";
43
+ import { openai } from "@ai-sdk/openai";
44
+ import { z } from "zod";
45
+
46
+ // Define tools
47
+ const tools: Tool[] = [
48
+ {
49
+ name: "greet",
50
+ description: "Greet a user by name",
51
+ parameters: z.object({
52
+ name: z.string().describe("Name to greet"),
53
+ }),
54
+ execute: async ({ name }) => `Hello, ${name}!`,
55
+ },
56
+ ];
57
+
58
+ // Create agent
59
+ const agent = createAgent({
60
+ model: openai("gpt-4o"),
61
+ cwd: process.cwd(),
62
+ tools,
63
+ });
64
+
65
+ // Stream responses
66
+ for await (const event of agent.chat("session-1", "Hello!")) {
67
+ switch (event.type) {
68
+ case "text-delta":
69
+ process.stdout.write(event.text);
70
+ break;
71
+ case "tool-call":
72
+ console.log(`Calling tool: ${event.toolName}`);
73
+ break;
74
+ case "tool-result":
75
+ console.log(`Tool result: ${event.result}`);
76
+ break;
77
+ case "complete":
78
+ console.log("\nDone!");
79
+ break;
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## API Reference
85
+
86
+ ### Local Models (Ollama Example)
87
+
88
+ ```typescript
89
+ import { createAgent, createModelResolver } from "@cuylabs/agent-core";
90
+
91
+ const resolveModel = createModelResolver({
92
+ engines: {
93
+ ollama: {
94
+ adapter: "openai-compatible",
95
+ settings: {
96
+ baseUrl: "http://localhost:11434/v1",
97
+ },
98
+ },
99
+ },
100
+ });
101
+
102
+ const agent = createAgent({
103
+ model: resolveModel("ollama/llama3.1"),
104
+ cwd: process.cwd(),
105
+ });
106
+ ```
107
+
108
+ ### Agent
109
+
110
+ ```typescript
111
+ import { createAgent, Agent } from "@cuylabs/agent-core";
112
+ import { openai } from "@ai-sdk/openai";
113
+
114
+ const agent = createAgent({
115
+ model: openai("gpt-4o"),
116
+ cwd: "/path/to/project",
117
+ tools: myTools,
118
+ systemPrompt: "You are a helpful assistant.",
119
+ temperature: 0.7,
120
+ maxSteps: 10,
121
+ });
122
+
123
+ // Streaming chat
124
+ for await (const event of agent.chat(sessionId, message)) {
125
+ // Handle events
126
+ }
127
+ ```
128
+
129
+ ### Session Management
130
+
131
+ ```typescript
132
+ import {
133
+ SessionManager,
134
+ MemoryStorage,
135
+ FileStorage
136
+ } from "@cuylabs/agent-core";
137
+
138
+ // In-memory storage (default)
139
+ const memoryStorage = new MemoryStorage();
140
+
141
+ // File-based persistent storage
142
+ const fileStorage = new FileStorage({
143
+ dataDir: "/path/to/data",
144
+ });
145
+
146
+ const sessions = new SessionManager(fileStorage);
147
+ await sessions.createSession({ id: "my-session" });
148
+ ```
149
+
150
+ ### Tool Framework
151
+
152
+ ```typescript
153
+ import { defineTool, Tool, ToolRegistry } from "@cuylabs/agent-core";
154
+ import { z } from "zod";
155
+
156
+ const myTool = defineTool({
157
+ name: "calculate",
158
+ description: "Perform a calculation",
159
+ parameters: z.object({
160
+ expression: z.string(),
161
+ }),
162
+ execute: async ({ expression }, ctx) => {
163
+ // ctx provides sessionId, cwd, abort signal, etc.
164
+ return eval(expression); // Don't actually do this!
165
+ },
166
+ });
167
+
168
+ // Register tools
169
+ const registry = new ToolRegistry();
170
+ registry.register(myTool);
171
+ ```
172
+
173
+ ### Error Handling & Retry
174
+
175
+ ```typescript
176
+ import {
177
+ withRetry,
178
+ LLMError,
179
+ isRetryable,
180
+ getRetryDelay
181
+ } from "@cuylabs/agent-core";
182
+
183
+ const result = await withRetry(
184
+ async () => {
185
+ // Your operation
186
+ },
187
+ {
188
+ maxRetries: 3,
189
+ initialDelay: 1000,
190
+ maxDelay: 30000,
191
+ }
192
+ );
193
+ ```
194
+
195
+ ### Context Management
196
+
197
+ ```typescript
198
+ import {
199
+ ContextManager,
200
+ estimateTokens,
201
+ pruneContext
202
+ } from "@cuylabs/agent-core";
203
+
204
+ const ctx = new ContextManager({
205
+ maxInputTokens: 100000,
206
+ pruneThreshold: 0.9,
207
+ });
208
+
209
+ const tokens = estimateTokens("Your text here");
210
+ ```
211
+
212
+ ### Model Capabilities
213
+
214
+ ```typescript
215
+ import {
216
+ ModelCapabilityResolver,
217
+ getDefaultResolver
218
+ } from "@cuylabs/agent-core";
219
+
220
+ const resolver = getDefaultResolver();
221
+ const caps = await resolver.resolve("gpt-4o");
222
+
223
+ console.log(caps.supportsImages);
224
+ console.log(caps.supportsToolUse);
225
+ console.log(caps.maxOutputTokens);
226
+ ```
227
+
228
+ ### MCP (Model Context Protocol)
229
+
230
+ ```typescript
231
+ import {
232
+ createAgent,
233
+ createMCPManager,
234
+ stdioServer
235
+ } from "@cuylabs/agent-core";
236
+ import { anthropic } from "@ai-sdk/anthropic";
237
+
238
+ // Create MCP manager with servers
239
+ const mcp = createMCPManager({
240
+ filesystem: stdioServer("npx", [
241
+ "-y", "@modelcontextprotocol/server-filesystem", "/tmp"
242
+ ]),
243
+ });
244
+
245
+ // Create agent with MCP tools
246
+ const agent = createAgent({
247
+ model: anthropic("claude-sonnet-4-20250514"),
248
+ mcp,
249
+ });
250
+
251
+ // MCP tools are automatically available
252
+ const result = await agent.send("session", "List files in /tmp");
253
+
254
+ // Clean up
255
+ await agent.close();
256
+ ```
257
+
258
+ ## Event Types
259
+
260
+ The `chat()` method yields these event types:
261
+
262
+ | Event Type | Description |
263
+ |------------|-------------|
264
+ | `text-delta` | Streaming text chunk |
265
+ | `tool-call` | Tool invocation started |
266
+ | `tool-result` | Tool execution completed |
267
+ | `reasoning` | Model reasoning (if supported) |
268
+ | `message` | Complete assistant message |
269
+ | `complete` | Stream finished with usage stats |
270
+ | `error` | Error occurred |
271
+
272
+ ## Concurrency Note
273
+
274
+ When using `runConcurrent()` to run multiple tasks in parallel, be aware that all tasks share the same `SessionManager` instance. For independent conversations, create separate `Agent` instances or use distinct session IDs.
275
+
@@ -0,0 +1,288 @@
1
+ // src/tool/tool.ts
2
+ import { z } from "zod";
3
+
4
+ // src/tool/truncation.ts
5
+ import * as fs from "fs/promises";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ import * as crypto from "crypto";
9
+ var MAX_LINES = 2e3;
10
+ var MAX_BYTES = 1e5;
11
+ var TRUNCATE_DIR = path.join(os.tmpdir(), "cuylabs-agent-outputs");
12
+ var TRUNCATE_GLOB = path.join(TRUNCATE_DIR, "*");
13
+ function truncateOutput(output, options = {}) {
14
+ const maxLines = options.maxLines ?? MAX_LINES;
15
+ const maxBytes = options.maxBytes ?? MAX_BYTES;
16
+ const lines = output.split("\n");
17
+ const bytes = Buffer.byteLength(output, "utf-8");
18
+ if (lines.length <= maxLines && bytes <= maxBytes) {
19
+ return {
20
+ content: output,
21
+ truncated: false
22
+ };
23
+ }
24
+ const hash = crypto.createHash("sha256").update(output).digest("hex").slice(0, 16);
25
+ const outputPath = path.join(TRUNCATE_DIR, `output-${hash}.txt`);
26
+ fs.mkdir(TRUNCATE_DIR, { recursive: true }).then(() => fs.writeFile(outputPath, output, "utf-8")).catch(() => {
27
+ });
28
+ let truncated;
29
+ if (lines.length > maxLines) {
30
+ const keepLines = Math.floor(maxLines / 2);
31
+ const firstPart = lines.slice(0, keepLines);
32
+ const lastPart = lines.slice(-keepLines);
33
+ const omitted = lines.length - keepLines * 2;
34
+ truncated = [
35
+ ...firstPart,
36
+ `
37
+ ... (${omitted} lines omitted, full output saved to ${outputPath}) ...
38
+ `,
39
+ ...lastPart
40
+ ].join("\n");
41
+ } else {
42
+ const keepBytes = Math.floor(maxBytes / 2);
43
+ const start = output.slice(0, keepBytes);
44
+ const end = output.slice(-keepBytes);
45
+ const omittedBytes = bytes - keepBytes * 2;
46
+ truncated = `${start}
47
+ ... (${omittedBytes} bytes omitted, full output saved to ${outputPath}) ...
48
+ ${end}`;
49
+ }
50
+ return {
51
+ content: truncated,
52
+ truncated: true,
53
+ outputPath
54
+ };
55
+ }
56
+ function formatSize(bytes) {
57
+ if (bytes < 1024) return `${bytes} B`;
58
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
60
+ }
61
+
62
+ // src/tool/tool.ts
63
+ var Tool;
64
+ ((Tool2) => {
65
+ function define(id, init) {
66
+ return {
67
+ id,
68
+ init: async (initCtx) => {
69
+ const toolInfo = typeof init === "function" ? await init(initCtx) : init;
70
+ const originalExecute = toolInfo.execute;
71
+ toolInfo.execute = async (params, ctx) => {
72
+ try {
73
+ toolInfo.parameters.parse(params);
74
+ } catch (error) {
75
+ if (error instanceof z.ZodError && toolInfo.formatValidationError) {
76
+ throw new Error(toolInfo.formatValidationError(error), { cause: error });
77
+ }
78
+ throw new Error(
79
+ `The ${id} tool was called with invalid arguments: ${error}.
80
+ Please rewrite the input so it satisfies the expected schema.`,
81
+ { cause: error }
82
+ );
83
+ }
84
+ const result = await originalExecute(params, ctx);
85
+ if (result.metadata.truncated !== void 0) {
86
+ return result;
87
+ }
88
+ const truncated = truncateOutput(result.output);
89
+ return {
90
+ ...result,
91
+ output: truncated.content,
92
+ metadata: {
93
+ ...result.metadata,
94
+ truncated: truncated.truncated,
95
+ ...truncated.truncated && { outputPath: truncated.outputPath }
96
+ }
97
+ };
98
+ };
99
+ return toolInfo;
100
+ }
101
+ };
102
+ }
103
+ Tool2.define = define;
104
+ function defineSimple(id, config) {
105
+ return define(id, config);
106
+ }
107
+ Tool2.defineSimple = defineSimple;
108
+ })(Tool || (Tool = {}));
109
+ function defineTool(definition) {
110
+ return Tool.define(definition.id, {
111
+ description: definition.description,
112
+ parameters: definition.parameters,
113
+ execute: async (params, ctx) => {
114
+ const result = await definition.execute(params, ctx);
115
+ return {
116
+ title: result.title,
117
+ output: result.output,
118
+ metadata: result.metadata ?? {}
119
+ };
120
+ }
121
+ });
122
+ }
123
+
124
+ // src/tool/registry.ts
125
+ var ToolRegistry = class {
126
+ tools = /* @__PURE__ */ new Map();
127
+ groups = /* @__PURE__ */ new Map();
128
+ // --------------------------------------------------------------------------
129
+ // Tool registration
130
+ // --------------------------------------------------------------------------
131
+ /**
132
+ * Register a tool. Throws if a tool with the same ID is already registered.
133
+ * Use `set()` for upsert semantics.
134
+ */
135
+ register(tool) {
136
+ if (this.tools.has(tool.id)) {
137
+ throw new Error(`Tool '${tool.id}' is already registered`);
138
+ }
139
+ this.tools.set(tool.id, tool);
140
+ }
141
+ /** Register multiple tools (throws on duplicates). */
142
+ registerAll(tools) {
143
+ for (const tool of tools) {
144
+ this.register(tool);
145
+ }
146
+ }
147
+ /** Register or replace a tool (upsert). */
148
+ set(tool) {
149
+ this.tools.set(tool.id, tool);
150
+ }
151
+ /** Unregister a tool by ID. Returns `true` if it existed. */
152
+ unregister(id) {
153
+ return this.tools.delete(id);
154
+ }
155
+ /** Get a tool by ID. */
156
+ get(id) {
157
+ return this.tools.get(id);
158
+ }
159
+ /** Check if a tool is registered. */
160
+ has(id) {
161
+ return this.tools.has(id);
162
+ }
163
+ /** Get all tool IDs. */
164
+ ids() {
165
+ return Array.from(this.tools.keys());
166
+ }
167
+ /** Get all tools. */
168
+ all() {
169
+ return Array.from(this.tools.values());
170
+ }
171
+ /** Clear all tools and groups. */
172
+ clear() {
173
+ this.tools.clear();
174
+ this.groups.clear();
175
+ }
176
+ /** Number of registered tools. */
177
+ get size() {
178
+ return this.tools.size;
179
+ }
180
+ // --------------------------------------------------------------------------
181
+ // Group management
182
+ // --------------------------------------------------------------------------
183
+ /**
184
+ * Register a named group of tool IDs.
185
+ * The group name can be used in `resolve()` specs.
186
+ * Tool IDs don't need to be registered yet — resolution is lazy.
187
+ */
188
+ registerGroup(name, toolIds) {
189
+ this.groups.set(name, toolIds);
190
+ }
191
+ /** Get tools in a group (only returns registered tools). */
192
+ getGroup(name) {
193
+ const ids = this.groups.get(name);
194
+ if (!ids) return void 0;
195
+ return ids.map((id) => this.tools.get(id)).filter((t) => t !== void 0);
196
+ }
197
+ /** Check if a group is registered. */
198
+ hasGroup(name) {
199
+ return this.groups.has(name);
200
+ }
201
+ /** List all group names. */
202
+ listGroups() {
203
+ return Array.from(this.groups.keys());
204
+ }
205
+ // --------------------------------------------------------------------------
206
+ // Resolution
207
+ // --------------------------------------------------------------------------
208
+ /**
209
+ * Resolve a `ToolSpec` to an array of tools.
210
+ *
211
+ * Supports group names, individual tool IDs, comma-separated strings,
212
+ * exclusions with `-` prefix, booleans, and pass-through arrays.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * registry.resolve("read-only"); // group name
217
+ * registry.resolve("read,grep,glob"); // comma-separated IDs
218
+ * registry.resolve("all,-bash"); // all except bash
219
+ * registry.resolve(["read", "grep"]); // array of IDs
220
+ * registry.resolve(true); // all registered tools
221
+ * registry.resolve(false); // empty array
222
+ * ```
223
+ */
224
+ resolve(spec) {
225
+ if (spec === true) return this.all();
226
+ if (spec === false) return [];
227
+ if (Array.isArray(spec) && spec.length > 0 && typeof spec[0] !== "string") {
228
+ return spec;
229
+ }
230
+ const tokens = Array.isArray(spec) ? spec : spec.split(",").map((s) => s.trim()).filter(Boolean);
231
+ const includes = [];
232
+ const excludes = [];
233
+ for (const token of tokens) {
234
+ if (token.startsWith("-")) {
235
+ excludes.push(token.slice(1));
236
+ } else {
237
+ includes.push(token);
238
+ }
239
+ }
240
+ let result = /* @__PURE__ */ new Map();
241
+ if (includes.length === 0 && excludes.length > 0) {
242
+ for (const [id, tool] of this.tools) {
243
+ result.set(id, tool);
244
+ }
245
+ } else {
246
+ for (const token of includes) {
247
+ if (token === "all") {
248
+ for (const [id, tool] of this.tools) {
249
+ result.set(id, tool);
250
+ }
251
+ } else if (this.groups.has(token)) {
252
+ const ids = this.groups.get(token);
253
+ for (const id of ids) {
254
+ const tool = this.tools.get(id);
255
+ if (tool) result.set(id, tool);
256
+ }
257
+ } else if (this.tools.has(token)) {
258
+ result.set(token, this.tools.get(token));
259
+ }
260
+ }
261
+ }
262
+ for (const id of excludes) {
263
+ if (this.groups.has(id)) {
264
+ const ids = this.groups.get(id);
265
+ for (const gid of ids) {
266
+ result.delete(gid);
267
+ }
268
+ } else {
269
+ result.delete(id);
270
+ }
271
+ }
272
+ return Array.from(result.values());
273
+ }
274
+ };
275
+ var defaultRegistry = new ToolRegistry();
276
+
277
+ export {
278
+ MAX_LINES,
279
+ MAX_BYTES,
280
+ TRUNCATE_DIR,
281
+ TRUNCATE_GLOB,
282
+ truncateOutput,
283
+ formatSize,
284
+ Tool,
285
+ defineTool,
286
+ ToolRegistry,
287
+ defaultRegistry
288
+ };