@codeworksh/aikit 0.1.0 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanchit Kudari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,347 +1,205 @@
1
1
  # @codeworksh/aikit
2
2
 
3
- Bun-native AI toolkit for building AI agents and coding-agent workflows.
3
+ Toolkit for building AI agents and streaming LLM workflows.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bun add @codeworksh/aikit
8
+ npm install @codeworksh/aikit
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ Node `>=24.14.1` is required.
12
+
13
+ ## What You Get
14
+
15
+ - `llm(...)` to resolve a model from the built-in registry
16
+ - `stream(...)` and `stream.complete(...)` for provider-normalized LLM streaming
17
+ - `Agent.create(...)` for a stateful agent instance
18
+ - `agent.loop(...)` and `agent.loopContinue(...)` for lower-level turn orchestration
19
+ - `Message` helpers for typed messages and generated `messageId` values
20
+
21
+ ## Root API
12
22
 
13
23
  ```ts
14
- import { Message, llm, stream } from "@codeworksh/aikit";
24
+ import { Agent, Message, agent, llm, stream } from "@codeworksh/aikit";
25
+ ```
15
26
 
16
- const model = await llm("anthropic", "claude-sonnet-4-5");
17
- if (!model) throw new Error("Model not found");
27
+ ## Quick Start
18
28
 
19
- const context: Message.Context = {
20
- systemPrompt: "You are a concise assistant.",
21
- messages: [
22
- {
23
- role: "user",
24
- time: { created: Date.now() },
25
- parts: [{ type: "text", text: "Say hello in one line." }],
26
- },
27
- ],
28
- };
29
+ ```ts
30
+ import { Agent } from "@codeworksh/aikit";
29
31
 
30
- const s = stream(model, context, {
31
- apiKey: process.env.ANTHROPIC_API_KEY,
32
+ const agent = await Agent.create({
33
+ provider: "anthropic",
34
+ model: "claude-haiku-4-5-20251001",
35
+ getApiKey: async () => process.env.ANTHROPIC_API_KEY,
32
36
  });
33
37
 
34
- for await (const event of s) {
35
- if (event.type === "text.delta") {
36
- process.stdout.write(event.delta);
37
- }
38
- }
38
+ agent.setSystemPrompt("Be concise.");
39
39
 
40
- const message = await s.result();
41
- console.log(message.parts);
42
- ```
40
+ await agent.prompt([{ type: "text", text: "Reply with exactly: hello" }]);
43
41
 
44
- ## Core Concepts
42
+ const lastMessage = agent.state.messages.at(-1);
43
+ console.log(lastMessage?.messageId);
44
+ console.log(lastMessage?.role);
45
+ ```
45
46
 
46
- ### LLM
47
+ ## Stream Events
47
48
 
48
- `llm(...)` resolves a model definition from the built-in registry. `stream(...)` and `stream.complete(...)` use that model to produce `AssistantMessage` output made of `message.parts`.
49
+ Use `stream(...)` when you want token and part-level events.
49
50
 
50
51
  ```ts
51
- import { llm, stream } from "@codeworksh/aikit";
52
+ import { Message, llm, stream } from "@codeworksh/aikit";
52
53
 
53
54
  const model = await llm("anthropic", "claude-haiku-4-5-20251001");
54
- if (!model) throw new Error("Missing model");
55
+ if (!model) throw new Error("Model not found");
55
56
 
56
- const message = await stream.complete(
57
+ const s = stream(
57
58
  model,
58
59
  {
59
60
  messages: [
60
- {
61
+ Message.createUserMessage({
61
62
  role: "user",
62
63
  time: { created: Date.now() },
63
- parts: [{ type: "text", text: "Reply with exactly: ok" }],
64
- },
64
+ parts: [{ type: "text", text: "Count from 1 to 3" }],
65
+ }),
65
66
  ],
66
67
  },
67
68
  { apiKey: process.env.ANTHROPIC_API_KEY },
68
69
  );
69
- ```
70
-
71
- ### Agent
72
-
73
- `agent.loop(...)` runs a full assistant turn loop on top of model streaming. It handles tool calls, validates arguments, mutates tool-call parts in-place, and emits higher-level agent events.
74
-
75
- ```ts
76
- import { Type } from "@sinclair/typebox";
77
- import { Agent, Message, agent, llm } from "@codeworksh/aikit";
78
-
79
- const calculatorParams = Type.Object({
80
- expression: Type.String(),
81
- });
82
-
83
- const calculatorTool: Agent.AgentTool<typeof calculatorParams> = {
84
- name: "calculator",
85
- label: "Calculator",
86
- description: "Evaluate a math expression",
87
- parameters: calculatorParams,
88
- async execute(callID, params) {
89
- return {
90
- status: "completed",
91
- result: {
92
- content: [{ type: "text", text: `${params.expression} = 4` }],
93
- isError: false,
94
- },
95
- };
96
- },
97
- };
98
-
99
- const model = await llm("anthropic", "claude-haiku-4-5-20251001");
100
- if (!model) throw new Error("Missing model");
101
-
102
- const prompt: Message.UserMessage = {
103
- role: "user",
104
- time: { created: Date.now() },
105
- parts: [{ type: "text", text: "Use the calculator tool for 2 + 2." }],
106
- };
107
-
108
- const run = agent.loop(
109
- {
110
- model,
111
- convertToLlm: async (messages) => messages,
112
- apiKey: process.env.ANTHROPIC_API_KEY,
113
- beforeToolExecution: async ({ toolCall }) => {
114
- console.log("validated args", toolCall.args);
115
- },
116
- afterToolExecution: async ({ result }) => result,
117
- },
118
- {
119
- systemPrompt: "Use tools when helpful.",
120
- messages: [],
121
- tools: [calculatorTool],
122
- },
123
- [prompt],
124
- );
125
70
 
126
- for await (const event of run) {
127
- if (event.type === "message.part.update" && event.source === "tool") {
128
- console.log(event.part);
71
+ for await (const event of s) {
72
+ if (event.type === "text.delta") {
73
+ process.stdout.write(event.delta);
129
74
  }
130
75
  }
131
76
 
132
- const messages = await run.result();
133
- console.log(messages.at(-1));
77
+ const finalMessage = await s.result();
78
+ console.log(finalMessage.messageId);
134
79
  ```
135
80
 
136
- ### Agent Vs LLM
81
+ ## Agent Instance
137
82
 
138
- Use `stream(...)` when you want raw provider events and manual control over tool handling.
83
+ `Agent.create(...)` is the simplest way to work with a stateful agent.
139
84
 
140
- Use `agent.loop(...)` when you want a higher-level runtime that:
141
- - streams assistant message lifecycle events
142
- - validates tool arguments
143
- - executes tools sequentially or in parallel
144
- - supports `beforeToolExecution` and `afterToolExecution` callbacks
85
+ ```ts
86
+ import { Agent } from "@codeworksh/aikit";
145
87
 
146
- ## Event Flow
88
+ const agent = await Agent.create({
89
+ provider: "anthropic",
90
+ model: "claude-haiku-4-5-20251001",
91
+ getApiKey: async () => process.env.ANTHROPIC_API_KEY,
92
+ });
147
93
 
148
- LLM streams emit low-level provider-normalized events:
94
+ agent.setSystemPrompt("Be concise.");
149
95
 
150
- ```text
151
- start
152
- text.start / thinking.start / toolcall.start
153
- text.delta / thinking.delta / toolcall.delta
154
- text.end / thinking.end / toolcall.end
155
- done | error
156
- ```
96
+ agent.subscribe((event) => {
97
+ if (event.type === "message.end") {
98
+ console.log(event.message.role, event.message.messageId);
99
+ }
100
+ });
157
101
 
158
- ## With Tools
102
+ await agent.prompt([{ type: "text", text: "Say hello in one line." }]);
159
103
 
160
- Agent loops emit higher-level lifecycle events around messages, parts, turns, and tools:
161
-
162
- ```text
163
- agent.start
164
- turn.start
165
- message.start
166
- message.part.start
167
- message.part.update
168
- message.part.end
169
- message.update
170
- message.end
171
- tool.execution.start
172
- tool.execution.update
173
- tool.execution.end
174
- turn.end
175
- agent.end
104
+ console.log(agent.state.messages);
176
105
  ```
177
106
 
178
- Example:
107
+ ## With Tools
179
108
 
180
109
  ```ts
181
110
  import { Type } from "@sinclair/typebox";
182
- import { Agent, Message, agent, llm } from "@codeworksh/aikit";
183
-
184
- const searchParams = Type.Object({
185
- query: Type.String(),
186
- limit: Type.Optional(Type.Number()),
187
- });
188
-
189
- const searchTool: Agent.AgentTool<typeof searchParams, { progress: number }, { hits: number }> = {
190
- name: "search",
191
- label: "Search",
192
- description: "Search indexed documents",
193
- parameters: searchParams,
194
- async execute(callID, params, signal, onUpdate) {
195
- onUpdate?.({
196
- status: "running",
197
- partial: {
198
- content: [{ type: "text", text: `Searching for ${params.query}` }],
199
- details: { progress: 50 },
200
- },
201
- });
111
+ import { Agent } from "@codeworksh/aikit";
202
112
 
113
+ const calculatorTool = Agent.defineTool({
114
+ name: "calculator",
115
+ label: "Calculator",
116
+ description: "Evaluate arithmetic expressions",
117
+ parameters: Type.Object({
118
+ expression: Type.String(),
119
+ }),
120
+ async execute(_callID, params) {
203
121
  return {
204
122
  status: "completed",
205
123
  result: {
206
- content: [{ type: "text", text: `Found results for ${params.query}` }],
207
- details: { hits: 3 },
124
+ content: [{ type: "text", text: `result for ${params.expression}` }],
208
125
  isError: false,
209
126
  },
210
127
  };
211
128
  },
212
- };
129
+ });
213
130
 
214
- const model = await llm("anthropic", "claude-haiku-4-5-20251001");
215
- if (!model) throw new Error("Missing model");
131
+ const instance = await Agent.create({
132
+ provider: "anthropic",
133
+ model: "claude-haiku-4-5-20251001",
134
+ getApiKey: async () => process.env.ANTHROPIC_API_KEY,
135
+ initialState: {
136
+ tools: [calculatorTool],
137
+ },
138
+ });
216
139
 
217
- const prompt: Message.UserMessage = {
218
- role: "user",
219
- time: { created: Date.now() },
220
- parts: [{ type: "text", text: "Search for agent loop documentation." }],
221
- };
140
+ await instance.prompt([{ type: "text", text: "Use the calculator tool for 25 * 18." }]);
141
+ ```
142
+
143
+ ## Lower-Level Loop
144
+
145
+ Use `agent.loop(...)` when you want loop execution without creating a stateful instance.
146
+
147
+ ```ts
148
+ import { Message, agent, llm } from "@codeworksh/aikit";
149
+
150
+ const model = await llm("anthropic", "claude-haiku-4-5-20251001");
151
+ if (!model) throw new Error("Model not found");
222
152
 
223
153
  const run = agent.loop(
224
154
  {
225
155
  model,
226
156
  apiKey: process.env.ANTHROPIC_API_KEY,
227
157
  convertToLlm: async (messages) => messages,
228
- toolExecution: "parallel",
229
158
  },
230
159
  {
231
- systemPrompt: "Use tools when useful.",
160
+ systemPrompt: "Be concise.",
232
161
  messages: [],
233
- tools: [searchTool],
162
+ tools: [],
234
163
  },
235
- [prompt],
164
+ [
165
+ Message.createUserMessage({
166
+ role: "user",
167
+ time: { created: Date.now() },
168
+ parts: [{ type: "text", text: "Reply with exactly: ok" }],
169
+ }),
170
+ ],
236
171
  );
237
172
 
238
- for await (const event of run) {
239
- if (event.type === "tool.execution.start") {
240
- console.log("tool started", event.name, event.args ?? event.rawArgs);
241
- }
242
-
243
- if (event.type === "tool.execution.update") {
244
- console.log("tool update", event.partial?.details);
245
- }
246
-
247
- if (event.type === "tool.execution.end") {
248
- console.log("tool finished", event.result.details);
249
- }
250
- }
173
+ const messages = await run.result();
174
+ console.log(messages.at(-1)?.role);
251
175
  ```
252
176
 
253
- ### Tool Execution Modes
177
+ ## Message IDs
254
178
 
255
- Tool execution mode is configurable with `toolExecution`:
179
+ Every user and assistant message has a `messageId`.
256
180
 
257
- - `parallel`: prepares tool calls in assistant order, then executes allowed calls concurrently. This is the default and is usually the right choice for independent tools.
258
- - `sequential`: prepares and executes each tool call one by one. Use this when tools depend on shared state, rate limits, or side effects that should not overlap.
181
+ Use the helpers when constructing messages yourself:
259
182
 
260
183
  ```ts
261
- const run = agent.loop(
262
- {
263
- model,
264
- convertToLlm: async (messages) => messages,
265
- toolExecution: "sequential",
266
- },
267
- context,
268
- [prompt],
269
- );
270
- ```
271
-
272
- ### beforeToolExecution
273
-
274
- `beforeToolExecution` runs after tool arguments have been validated and before the tool executes.
184
+ import { Message } from "@codeworksh/aikit";
275
185
 
276
- Use it to inspect validated params, enforce policy, or block execution.
186
+ const userMessage = Message.createUserMessage({
187
+ role: "user",
188
+ time: { created: Date.now() },
189
+ parts: [{ type: "text", text: "hello" }],
190
+ });
277
191
 
278
- ```ts
279
- const run = agent.loop(
280
- {
281
- model,
282
- convertToLlm: async (messages) => messages,
283
- beforeToolExecution: async ({ toolCall }) => {
284
- if (toolCall.name === "search" && toolCall.args?.query === "secret") {
285
- return {
286
- block: true,
287
- reason: "This query is not allowed",
288
- };
289
- }
290
- },
291
- },
292
- context,
293
- [prompt],
294
- );
192
+ console.log(userMessage.messageId);
295
193
  ```
296
194
 
297
- Returning `{ block: true }` prevents the tool from running and turns that assistant tool-call part into an error result.
298
-
299
- ### afterToolExecution
300
-
301
- `afterToolExecution` runs after the tool finishes and before the terminal tool result is emitted.
302
-
303
- Use it to normalize output, attach metadata, or override the final tool result.
304
-
305
- ```ts
306
- const run = agent.loop(
307
- {
308
- model,
309
- convertToLlm: async (messages) => messages,
310
- afterToolExecution: async ({ result }) => {
311
- if (result.status === "completed") {
312
- return {
313
- status: "completed",
314
- result: {
315
- ...result.result,
316
- details: {
317
- ...result.result.details,
318
- source: "post-processed",
319
- },
320
- },
321
- };
322
- }
323
- },
324
- },
325
- context,
326
- [prompt],
327
- );
328
- ```
195
+ This is useful for persistence layers like SQLite, event reconciliation, and updating stored messages by identity instead of array position.
329
196
 
330
- ## Public API
197
+ ## Lower-Level APIs
331
198
 
332
- The root package exports the facades and core namespaces used to build on top of `aikit`:
199
+ - `stream(...)`: raw provider-normalized stream
200
+ - `stream.complete(...)`: final assistant message without manual iteration
201
+ - `Agent.create(...)`: stateful agent instance
202
+ - `agent.loop(...)`: lower-level agent loop
203
+ - `agent.loopContinue(...)`: continue from an existing context
333
204
 
334
- ```ts
335
- import {
336
- agent,
337
- llm,
338
- stream,
339
- Agent,
340
- Event,
341
- Message,
342
- Model,
343
- Stream,
344
- validateToolArguments,
345
- validateToolCall,
346
- } from "@codeworksh/aikit";
347
- ```
205
+ API docs can be added later. For now, the tests under `packages/aikit/test/` are the best reference for concrete usage.