@tabbybyte/kimten 0.1.5 → 0.3.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
@@ -14,8 +14,8 @@ It’s meant to feel like a smart helper, not a framework.
14
14
 
15
15
  ## ✅ What it does
16
16
 
17
- - Runs a simple agent loop (bounded by `hops`)
18
- - Lets the model call your tools (`toys`)
17
+ - Runs a simple, single-agent loop (bounded by `hops`)
18
+ - Lets the LLM model (the brain) call your tool functions (the toys)
19
19
  - Keeps short-term conversation memory (in-process, per instance)
20
20
  - Supports optional structured output via Zod
21
21
 
@@ -24,19 +24,19 @@ It’s meant to feel like a smart helper, not a framework.
24
24
  - No planners/graphs/state machines
25
25
  - No streaming API surface
26
26
  - No persistence or long-term memory
27
- - No plugin system or orchestration runtime
27
+ - No plugin system or multi-agent orchestration
28
28
 
29
29
  ---
30
30
 
31
31
  ## ✨ Why Kimten?
32
32
 
33
- Use it when you just want an agent loop with tools and a little memory, without adopting a larger framework.
33
+ Use it when you just want an agent loop with toys and a little memory, without adopting a larger framework.
34
34
 
35
35
  Good fits:
36
36
 
37
37
  - CLI helpers
38
38
  - small automations
39
- - local tools
39
+ - local toys
40
40
  - scripting
41
41
  - quick AI utilities
42
42
  - “just let the model call a function” use cases
@@ -68,9 +68,19 @@ const cat = Kimten({
68
68
  brain: openai('gpt-4o-mini'), // or, any other available model
69
69
 
70
70
  toys: {
71
- add: async ({ a, b }) => a + b,
71
+ randomNumber: {
72
+ description: 'Generate a random integer between min and max (inclusive).',
73
+ inputSchema: z.object({ min: z.number().int(), max: z.number().int() }),
74
+ async execute({ min, max }) {
75
+ const low = Math.min(min, max);
76
+ const high = Math.max(min, max);
77
+ return Math.floor(Math.random() * (high - low + 1)) + low;
78
+ },
79
+ },
72
80
  },
73
81
 
82
+ personality: 'You are a helpful assistant.',
83
+
74
84
  hops: 10,
75
85
  });
76
86
 
@@ -89,7 +99,7 @@ cat.forget();
89
99
 
90
100
  ---
91
101
 
92
- ## 🧠 Mental Model
102
+ ## 💭 Mental Model
93
103
 
94
104
  Kimten is basically:
95
105
 
@@ -115,35 +125,36 @@ Each instance keeps short-term chat memory, so follow-up prompts naturally refer
115
125
  Create a new instance.
116
126
 
117
127
  #### Required
118
- * `brain` → AI SDK model instance
128
+ * 🧠 `brain` → AI SDK model instance
119
129
 
120
130
  #### Optional
121
131
 
122
- * `toys` → object map of tool definitions. Each entry can be:
123
- * async function shorthand: `async (args) => result`
132
+ * 🎱 `toys` → object map of toy (tool) definitions. Each entry is:
124
133
  * object form: `{ inputSchema?, description?, strict?, execute }`
125
134
  default: `{}`
126
- * `personality` → system prompt / behavior description (default: `"You are a helpful assistant."`)
127
- * `hops` → max agent loop steps (default: `10`)
128
- prevents infinite zoomies 🌀
135
+ * 🕵️‍♂️ `personality` → system instructions / prompt for overall behavior description (default: `'You are a helpful assistant.'`)
136
+ * 🌀 `hops` → max agent loop steps (default: `10`) - prevents infinite zoomies
129
137
 
130
- #### Tool semantics
138
+ #### Toy semantics
131
139
 
132
- - Tool inputs are validated only if you provide `inputSchema` (shorthand tools accept anything).
133
- - Tool results should be JSON-serializable; `undefined` becomes `null`.
134
- - If a tool throws, Kimten returns `{ error, toolName }` as the tool result (it does not re-throw).
140
+ - Toy inputs are validated only if you provide `inputSchema`.
141
+ - Toy results should be JSON-serializable; `undefined` becomes `null`.
142
+ - If a toy function throws, Kimten returns `{ error, toolName }` as the toy result (it does not re-throw).
143
+ - Under the hood, each toy is implemented as an AI SDK tool.
135
144
 
136
145
  #### Returns
137
146
 
138
- * `play(input, schema?)`
147
+ * `play(input, schema?, context?)`
139
148
 
140
149
  * runs the agent
141
150
  * uses short-term memory automatically
142
151
  * optional Zod schema for structured output
152
+ * optional plain object context injected into the current call prompt as JSON (with basic redaction/truncation guards)
153
+ * context is ephemeral per `play()` call and is not persisted in memory
143
154
 
144
155
  * `forget()`
145
156
 
146
- * clears short-term memory/context
157
+ * clears short-term memory
147
158
 
148
159
  ---
149
160
 
@@ -153,32 +164,25 @@ Create a new instance.
153
164
 
154
165
  For the `brain` part, feel free to use any compatible provider and their models.
155
166
 
156
- Refer to the AI SDK docs: **[providers and models](https://ai-sdk.dev/docs/foundations/providers-and-models)**.
167
+ Note that not all providers (and models) may work out the box with Kimten, particularly for structured output.
157
168
 
158
- ### Add tools freely
169
+ 💡 Refer to the AI SDK docs for details: **[providers and models](https://ai-sdk.dev/docs/foundations/providers-and-models)**.
159
170
 
160
- Tools can stay simple, just normal async functions:
161
-
162
- ```js
163
- toys: {
164
- readFile,
165
- writeFile,
166
- fetchJson,
167
- runCommand,
168
- }
169
- ```
171
+ ### Add toys freely
170
172
 
171
- For stronger arg validation and better tool selection, use object form:
173
+ Define `toys` in object form for strong arg validation and proper selection by the LLM:
172
174
 
173
175
  ```js
174
176
  import { z } from 'zod';
175
177
 
176
178
  toys: {
177
- add: {
178
- description: 'Add two numbers.',
179
- inputSchema: z.object({ a: z.number(), b: z.number() }),
180
- async execute({ a, b }) {
181
- return a + b;
179
+ randomNumber: {
180
+ description: 'Generate a random integer between min and max (inclusive).',
181
+ inputSchema: z.object({ min: z.number().int(), max: z.number().int() }),
182
+ async execute({ min, max }) {
183
+ const low = Math.min(min, max);
184
+ const high = Math.max(min, max);
185
+ return Math.floor(Math.random() * (high - low + 1)) + low;
182
186
  },
183
187
  },
184
188
  }
package/index.d.ts CHANGED
@@ -2,16 +2,16 @@ import type { ZodTypeAny, infer as ZodInfer } from 'zod';
2
2
 
3
3
  export type BrainModel = Record<string, unknown>;
4
4
 
5
- export type ToyFn = (args: any) => any | Promise<any>;
5
+ export type ToolExecute = (args: any) => any | Promise<any>;
6
6
 
7
7
  export type ToyDefinition = {
8
8
  inputSchema?: ZodTypeAny;
9
9
  description?: string;
10
10
  strict?: boolean;
11
- execute: ToyFn;
11
+ execute: ToolExecute;
12
12
  };
13
13
 
14
- export type Toys = Record<string, ToyFn | ToyDefinition>;
14
+ export type Toys = Record<string, ToyDefinition>;
15
15
 
16
16
  export type KimtenConfig = {
17
17
  brain: BrainModel;
@@ -21,8 +21,12 @@ export type KimtenConfig = {
21
21
  };
22
22
 
23
23
  export type KimtenAgent = {
24
- play(input: string): Promise<string>;
25
- play<S extends ZodTypeAny>(input: string, schema: S): Promise<ZodInfer<S>>;
24
+ play(input: string, schema?: null, context?: Record<string, unknown> | null): Promise<string>;
25
+ play<S extends ZodTypeAny>(
26
+ input: string,
27
+ schema: S,
28
+ context?: Record<string, unknown> | null
29
+ ): Promise<ZodInfer<S>>;
26
30
  forget(): void;
27
31
  };
28
32
 
@@ -30,4 +34,3 @@ export declare function Kimten(config: KimtenConfig): KimtenAgent;
30
34
 
31
35
  declare const _default: typeof Kimten;
32
36
  export default _default;
33
-
package/lib/kimten.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ToolLoopAgent, stepCountIs, Output } from 'ai';
2
2
  import { createMemory } from './memory.js';
3
- import { buildMessages } from './prompt.js';
4
3
  import { normalizeToys } from './tools.js';
5
4
 
6
5
  const DEFAULT_PERSONALITY = 'You are a helpful assistant.';
6
+ const CONTEXT_CHAR_LIMIT = 4000;
7
7
 
8
8
  /**
9
9
  * @typedef {import('zod').ZodTypeAny} ZodSchema
@@ -23,9 +23,9 @@ const DEFAULT_PERSONALITY = 'You are a helpful assistant.';
23
23
  */
24
24
 
25
25
  /**
26
- * Shorthand tool form: any async/sync function.
26
+ * Tool execute function.
27
27
  *
28
- * @callback ToyFn
28
+ * @callback ToolExecute
29
29
  * @param {any} args
30
30
  * @returns {any | Promise<any>}
31
31
  */
@@ -37,13 +37,13 @@ const DEFAULT_PERSONALITY = 'You are a helpful assistant.';
37
37
  * @property {import('zod').ZodTypeAny} [inputSchema]
38
38
  * @property {string} [description]
39
39
  * @property {boolean} [strict]
40
- * @property {ToyFn} execute
40
+ * @property {ToolExecute} execute
41
41
  */
42
42
 
43
43
  /**
44
44
  * Tool registry map.
45
45
  *
46
- * @typedef {Record<string, ToyFn | ToyDefinition>} Toys
46
+ * @typedef {Record<string, ToyDefinition>} Toys
47
47
  */
48
48
 
49
49
  /**
@@ -60,7 +60,7 @@ const DEFAULT_PERSONALITY = 'You are a helpful assistant.';
60
60
  * Returned Kimten instance.
61
61
  *
62
62
  * @typedef {object} KimtenAgent
63
- * @property {(input: string, schema?: ZodSchema | null) => Promise<any>} play Run the agent loop.
63
+ * @property {(input: string, schema?: ZodSchema | null, context?: Record<string, unknown> | null) => Promise<any>} play Run the agent loop.
64
64
  * @property {() => void} forget Clear short-term memory.
65
65
  */
66
66
 
@@ -92,6 +92,45 @@ function validateConfig(config) {
92
92
  };
93
93
  }
94
94
 
95
+ function serializeContext(context) {
96
+ if (context === null || context === undefined) {
97
+ return '';
98
+ }
99
+
100
+ if (typeof context !== 'object' || Array.isArray(context)) {
101
+ throw new TypeError('Kimten play(input, schema, context) expects context to be a plain object when provided.');
102
+ }
103
+
104
+ const redacted = JSON.stringify(
105
+ context,
106
+ (key, value) => {
107
+ const lowered = String(key).toLowerCase();
108
+ if (
109
+ lowered.includes('password') ||
110
+ lowered.includes('token') ||
111
+ lowered.includes('secret') ||
112
+ lowered.includes('apikey') ||
113
+ lowered.includes('api_key')
114
+ ) {
115
+ return '[REDACTED]';
116
+ }
117
+
118
+ return value;
119
+ },
120
+ 2
121
+ );
122
+
123
+ if (typeof redacted !== 'string') {
124
+ return '';
125
+ }
126
+
127
+ if (redacted.length <= CONTEXT_CHAR_LIMIT) {
128
+ return redacted;
129
+ }
130
+
131
+ return `${redacted.slice(0, CONTEXT_CHAR_LIMIT)}\n...(truncated)`;
132
+ }
133
+
95
134
  /**
96
135
  * Create a tiny tool-using agent with short-term memory.
97
136
  *
@@ -144,19 +183,41 @@ export function Kimten(config) {
144
183
  *
145
184
  * @param {string} input
146
185
  * @param {ZodSchema | null} [schema]
186
+ * @param {Record<string, unknown> | null} [context]
147
187
  * @returns {Promise<any>}
148
188
  */
149
- async function play(input, schema = null) {
189
+ async function play(input, schema = null, context = null) {
150
190
  if (typeof input !== 'string') {
151
191
  throw new TypeError('Kimten play(input) expects input to be a string.');
152
192
  }
153
193
 
194
+ // Serialize provided context (redacts sensitive keys and truncates if too long).
195
+ const serializedContext = serializeContext(context);
196
+
197
+ // If we have context, embed it before the user's message so the agent sees both.
198
+ const effectiveInput = serializedContext
199
+ ? `Context (JSON):\n${serializedContext}\n\nUser message:\n${input}`
200
+ : input;
201
+
202
+ // Store the raw user message (no context) in short-term memory.
154
203
  memory.add({ role: 'user', content: input });
155
204
 
205
+ // Retrieve conversation so far from memory.
206
+ const fetchedMessages = memory.list();
207
+
208
+ // If the last message is the user message we just added, we may need to replace it with
209
+ // a version that includes the serialized context, so the agent can see that information.
210
+ // This way we keep the raw user message in memory, but provide the agent with the enriched version.
211
+ const lastMessage = fetchedMessages[fetchedMessages.length - 1];
212
+ const messages = serializedContext && lastMessage?.role === 'user'
213
+ ? [...fetchedMessages.slice(0, -1), { ...lastMessage, content: effectiveInput }]
214
+ : fetchedMessages;
215
+
216
+ // Choose a structured agent when a schema is provided, otherwise use the text agent.
156
217
  const agent = schema ? getStructuredAgent(schema) : textAgent;
157
- const result = await agent.generate({
158
- messages: buildMessages(personality, memory.list()),
159
- });
218
+
219
+ // Run the agent loop with the prepared messages.
220
+ const result = await agent.generate({ messages });
160
221
 
161
222
  const assistantContent =
162
223
  schema
package/lib/tools.js CHANGED
@@ -6,9 +6,9 @@ import { z } from 'zod';
6
6
  */
7
7
 
8
8
  /**
9
- * Shorthand tool form: any async/sync function.
9
+ * Tool execute function.
10
10
  *
11
- * @callback ToyFn
11
+ * @callback ToolExecute
12
12
  * @param {any} args
13
13
  * @returns {any | Promise<any>}
14
14
  */
@@ -20,13 +20,13 @@ import { z } from 'zod';
20
20
  * @property {ZodSchema} [inputSchema]
21
21
  * @property {string} [description]
22
22
  * @property {boolean} [strict]
23
- * @property {ToyFn} execute
23
+ * @property {ToolExecute} execute
24
24
  */
25
25
 
26
26
  /**
27
27
  * Tool registry map.
28
28
  *
29
- * @typedef {Record<string, ToyFn | ToyDefinition>} Toys
29
+ * @typedef {Record<string, ToyDefinition>} Toys
30
30
  */
31
31
 
32
32
  function isPlainObject(value) {
@@ -41,8 +41,7 @@ function isPlainObject(value) {
41
41
  /**
42
42
  * Normalize a toy registry into AI SDK tool objects.
43
43
  *
44
- * - Shorthand entry: `async (args) => result`
45
- * - Object form: `{ inputSchema?, description?, strict?, execute }`
44
+ * - Object form only: `{ inputSchema?, description?, strict?, execute }`
46
45
  *
47
46
  * Tool execution is wrapped so thrown errors become JSON-safe results:
48
47
  * `{ error, toolName }` (Kimten does not re-throw tool errors).
@@ -56,7 +55,7 @@ export function normalizeToys(toys) {
56
55
  }
57
56
 
58
57
  if (!isPlainObject(toys)) {
59
- throw new TypeError('Kimten config "toys" must be an object map of functions or tool definitions.');
58
+ throw new TypeError('Kimten config "toys" must be an object map of tool definitions.');
60
59
  }
61
60
 
62
61
  const wrapped = {};
@@ -86,16 +85,9 @@ export function normalizeToys(toys) {
86
85
  }
87
86
 
88
87
  function normalizeToyDefinition(name, entry) {
89
- if (typeof entry === 'function') {
90
- return {
91
- inputSchema: z.any(),
92
- execute: entry,
93
- };
94
- }
95
-
96
88
  if (!isPlainObject(entry)) {
97
89
  throw new TypeError(
98
- `Kimten tool "${name}" must be a function or an object with execute(args).`
90
+ `Kimten tool "${name}" must be an object with execute(args).`
99
91
  );
100
92
  }
101
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabbybyte/kimten",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/lib/prompt.js DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Build AI SDK messages for a generation call.
3
- *
4
- * @param {string} personality System prompt / instructions.
5
- * @param {Array<{ role: string, content: any }>} history Prior conversation messages.
6
- * @returns {Array<{ role: string, content: any }>}
7
- */
8
- export function buildMessages(personality, history) {
9
- return [
10
- { role: 'system', content: personality },
11
- ...history,
12
- ];
13
- }