@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 +347 -0
- package/package.json +60 -0
- package/src/agent/agent.ts +75 -0
- package/src/agent/loop.ts +786 -0
- package/src/agent.ts +11 -0
- package/src/event/event.ts +271 -0
- package/src/index.ts +10 -0
- package/src/llm.ts +29 -0
- package/src/message/message.ts +280 -0
- package/src/model/catalog.ts +95 -0
- package/src/model/model.ts +253 -0
- package/src/provider/provider.ts +43 -0
- package/src/provider/providers/anthropic/index.ts +769 -0
- package/src/provider/register.ts +14 -0
- package/src/provider/stream.ts +299 -0
- package/src/stream.ts +72 -0
- package/src/utils/eventstream.ts +95 -0
- package/src/utils/helpers.ts +24 -0
- package/src/utils/jsonparse.ts +32 -0
- package/src/utils/overflow.ts +121 -0
- package/src/utils/sanitize.ts +25 -0
- package/src/utils/validation.ts +63 -0
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
|
+
}
|