@codeworksh/aikit 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,347 @@
1
+ # @codeworksh/aikit
2
+
3
+ Bun-native AI toolkit for building AI agents and coding-agent workflows.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @codeworksh/aikit
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { Message, llm, stream } from "@codeworksh/aikit";
15
+
16
+ const model = await llm("anthropic", "claude-sonnet-4-5");
17
+ if (!model) throw new Error("Model not found");
18
+
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
+
30
+ const s = stream(model, context, {
31
+ apiKey: process.env.ANTHROPIC_API_KEY,
32
+ });
33
+
34
+ for await (const event of s) {
35
+ if (event.type === "text.delta") {
36
+ process.stdout.write(event.delta);
37
+ }
38
+ }
39
+
40
+ const message = await s.result();
41
+ console.log(message.parts);
42
+ ```
43
+
44
+ ## Core Concepts
45
+
46
+ ### LLM
47
+
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
+
50
+ ```ts
51
+ import { llm, stream } from "@codeworksh/aikit";
52
+
53
+ const model = await llm("anthropic", "claude-haiku-4-5-20251001");
54
+ if (!model) throw new Error("Missing model");
55
+
56
+ const message = await stream.complete(
57
+ model,
58
+ {
59
+ messages: [
60
+ {
61
+ role: "user",
62
+ time: { created: Date.now() },
63
+ parts: [{ type: "text", text: "Reply with exactly: ok" }],
64
+ },
65
+ ],
66
+ },
67
+ { apiKey: process.env.ANTHROPIC_API_KEY },
68
+ );
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
+
126
+ for await (const event of run) {
127
+ if (event.type === "message.part.update" && event.source === "tool") {
128
+ console.log(event.part);
129
+ }
130
+ }
131
+
132
+ const messages = await run.result();
133
+ console.log(messages.at(-1));
134
+ ```
135
+
136
+ ### Agent Vs LLM
137
+
138
+ Use `stream(...)` when you want raw provider events and manual control over tool handling.
139
+
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
145
+
146
+ ## Event Flow
147
+
148
+ LLM streams emit low-level provider-normalized events:
149
+
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
+ ```
157
+
158
+ ## With Tools
159
+
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
176
+ ```
177
+
178
+ Example:
179
+
180
+ ```ts
181
+ 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
+ });
202
+
203
+ return {
204
+ status: "completed",
205
+ result: {
206
+ content: [{ type: "text", text: `Found results for ${params.query}` }],
207
+ details: { hits: 3 },
208
+ isError: false,
209
+ },
210
+ };
211
+ },
212
+ };
213
+
214
+ const model = await llm("anthropic", "claude-haiku-4-5-20251001");
215
+ if (!model) throw new Error("Missing model");
216
+
217
+ const prompt: Message.UserMessage = {
218
+ role: "user",
219
+ time: { created: Date.now() },
220
+ parts: [{ type: "text", text: "Search for agent loop documentation." }],
221
+ };
222
+
223
+ const run = agent.loop(
224
+ {
225
+ model,
226
+ apiKey: process.env.ANTHROPIC_API_KEY,
227
+ convertToLlm: async (messages) => messages,
228
+ toolExecution: "parallel",
229
+ },
230
+ {
231
+ systemPrompt: "Use tools when useful.",
232
+ messages: [],
233
+ tools: [searchTool],
234
+ },
235
+ [prompt],
236
+ );
237
+
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
+ }
251
+ ```
252
+
253
+ ### Tool Execution Modes
254
+
255
+ Tool execution mode is configurable with `toolExecution`:
256
+
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.
259
+
260
+ ```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.
275
+
276
+ Use it to inspect validated params, enforce policy, or block execution.
277
+
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
+ );
295
+ ```
296
+
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
+ ```
329
+
330
+ ## Public API
331
+
332
+ The root package exports the facades and core namespaces used to build on top of `aikit`:
333
+
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
+ ```
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@codeworksh/aikit",
3
+ "version": "0.1.0",
4
+ "module": "src/index.ts",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "lint": "biome lint .",
11
+ "lint:fix": "biome lint --write ."
12
+ },
13
+ "dependencies": {
14
+ "@anthropic-ai/sdk": "^0.80.0",
15
+ "@codeworksh/utils": "1.0.0",
16
+ "@sinclair/typebox": "^0.34.48",
17
+ "ajv": "^8.18.0",
18
+ "ajv-formats": "^3.0.1",
19
+ "partial-json": "^0.1.7",
20
+ "remeda": "^2.33.6"
21
+ },
22
+ "devDependencies": {
23
+ "bun-types": "^1.3.11"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5"
27
+ },
28
+ "description": "AI toolkit for building AI agents and coding agents.",
29
+ "author": "Sanchit Kudari",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/codeworksh/codework.git",
34
+ "directory": "packages/aikit"
35
+ },
36
+ "homepage": "https://github.com/codeworksh/codework/tree/main/packages/aikit",
37
+ "bugs": {
38
+ "url": "https://github.com/codeworksh/codework/issues"
39
+ },
40
+ "files": [
41
+ "src",
42
+ "README.md"
43
+ ],
44
+ "engines": {
45
+ "bun": ">=1.3.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "keywords": [
51
+ "bun",
52
+ "typescript",
53
+ "ai",
54
+ "agent",
55
+ "toolkit",
56
+ "coding-agent",
57
+ "anthropic",
58
+ "streaming"
59
+ ]
60
+ }
@@ -0,0 +1,75 @@
1
+ import { type Static, type TSchema, Type } from "@sinclair/typebox";
2
+ import { Message } from "../message/message";
3
+ import type { Model } from "../model/model";
4
+ import type { Stream } from "../provider/stream";
5
+
6
+ export namespace Agent {
7
+ export const ToolTerminalResultSchema = Type.Union([Message.ToolCompletedSchema, Message.ToolErrorSchema]);
8
+ export const ToolResultSchema = Type.Union([
9
+ Message.ToolRunningSchema,
10
+ Message.ToolCompletedSchema,
11
+ Message.ToolErrorSchema,
12
+ ]);
13
+
14
+ export type ToolRunningResult<T> = Omit<Static<typeof Message.ToolRunningSchema>, "partial"> & {
15
+ partial?: Omit<Static<typeof Message.ToolRunningPartial>, "details"> & {
16
+ details?: T;
17
+ };
18
+ };
19
+ export type ToolCompletedResult<T> = Omit<Static<typeof Message.ToolCompletedSchema>, "result"> & {
20
+ result: Omit<Static<typeof Message.ToolSuccessResult>, "details"> & {
21
+ details?: T;
22
+ };
23
+ };
24
+ export type ToolErrorResult<T> = Omit<Static<typeof Message.ToolErrorSchema>, "result"> & {
25
+ result: Omit<Static<typeof Message.ToolErrorResult>, "details"> & {
26
+ details?: T;
27
+ };
28
+ };
29
+ export type ToolTerminalResult<T> = ToolCompletedResult<T> | ToolErrorResult<T>;
30
+
31
+ // callback for tool execution updates.
32
+ // consumers can narrow on status if needed
33
+ // type inference:
34
+ // U - update
35
+ export type ToolUpdateCallback<U = any> = (result: ToolRunningResult<U>) => void;
36
+
37
+ export interface AgentTool<TParameters extends TSchema = TSchema, U = any, R = U> extends Message.Tool<TParameters> {
38
+ // A human-readable label for the tool for display
39
+ label: string;
40
+ execute: (
41
+ callID: string,
42
+ params: Static<TParameters>,
43
+ signal?: AbortSignal,
44
+ onUpdate?: ToolUpdateCallback<U>,
45
+ ) => Promise<ToolTerminalResult<R>>;
46
+ }
47
+ export type AnyAgentTool = AgentTool<any, any, any>;
48
+
49
+ export interface State<TTool extends AnyAgentTool = AnyAgentTool> {
50
+ systemPrompt: string;
51
+ model: Model.Value;
52
+ thinkingLevel: Stream.ThinkingLevel;
53
+ tools: TTool[];
54
+ messages: Message.Message[];
55
+ isStreaming: boolean;
56
+ streamMessage: Message.Message | null;
57
+ pendingToolCalls: Set<string>;
58
+ error?: string;
59
+ }
60
+
61
+ // AgentContext is like Message.Context but uses AgentTool
62
+ export interface AgentContext<TTool extends AnyAgentTool = AnyAgentTool> {
63
+ systemPrompt: string;
64
+ messages: Message.Message[];
65
+ tools?: TTool[];
66
+ }
67
+
68
+ export const ToolCallInFlightSchema = Type.Object({
69
+ callID: Type.String(),
70
+ name: Type.String(),
71
+ rawArgs: Type.Record(Type.String(), Type.Any()),
72
+ args: Type.Optional(Type.Any()), // post validation
73
+ });
74
+ export type ToolCallInFlight = Static<typeof ToolCallInFlightSchema>;
75
+ }