agentick 0.2.0 → 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.
Files changed (2) hide show
  1. package/README.md +578 -231
  2. package/package.json +6 -6
package/README.md CHANGED
@@ -1,82 +1,68 @@
1
1
  # agentick
2
2
 
3
- **React for AI agents.**
3
+ **The component framework for AI.**
4
4
 
5
- A React reconciler where the render target is a language model. No prompt templates, no YAML chains, no Jinja. You build the context window with JSX — the same components, hooks, and composition you already know — and the framework compiles it into what the model sees.
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ [![React](https://img.shields.io/badge/React_19-reconciler-blue?style=for-the-badge&logo=react&logoColor=white)](https://react.dev/)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A520-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
9
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](https://github.com/agenticklabs/agentick/pulls)
6
10
 
7
- You're not configuring a chatbot. You're building the application through which the model sees and experiences the world.
8
-
9
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg?style=for-the-badge)](LICENSE)
11
+ A React reconciler where the render target is a language model. You build the context window with JSX — the same components, hooks, and composition you already know — and the framework compiles it into what the model sees.
10
12
 
11
13
  ```tsx
12
- import { createApp, System, Timeline, Message, Section,
13
- createTool, useContinuation } from "@agentick/core";
14
+ import { createApp, System, Timeline, createTool, useContinuation } from "@agentick/core";
14
15
  import { openai } from "@agentick/openai";
15
16
  import { z } from "zod";
16
17
 
17
- // Tools are components — they render state into model context
18
18
  const Search = createTool({
19
19
  name: "search",
20
20
  description: "Search the knowledge base",
21
21
  input: z.object({ query: z.string() }),
22
- handler: async ({ query }, com) => {
22
+ handler: async ({ query }) => {
23
23
  const results = await knowledgeBase.search(query);
24
- const sources = com.getState("sources") ?? [];
25
- com.setState("sources", [...sources, ...results.map((r) => r.title)]);
26
24
  return [{ type: "text", text: JSON.stringify(results) }];
27
25
  },
28
- // render() injects live state into the context window every tick
29
- render: (tickState, com) => {
30
- const sources = com.getState("sources");
31
- return sources?.length ? (
32
- <Section id="sources" audience="model">
33
- Sources found so far: {sources.join(", ")}
34
- </Section>
35
- ) : null;
36
- },
37
26
  });
38
27
 
39
- // Agents are functions that return JSX
40
- function ResearchAgent({ topic }: { topic: string }) {
41
- // The model auto-continues when it makes tool calls.
42
- // Hooks add your own stop conditions.
43
- useContinuation((result) => {
44
- if (result.tick >= 20) result.stop("too-many-ticks");
45
- });
28
+ function ResearchAgent() {
29
+ useContinuation((result) => result.tick < 10);
46
30
 
47
31
  return (
48
32
  <>
49
- <System>
50
- You are a research agent. Search thoroughly, then write a summary.
51
- </System>
52
-
53
- {/* You control exactly how conversation history renders */}
54
- <Timeline>
55
- {(history, pending) => <>
56
- {history.map((entry, i) =>
57
- i < history.length - 4
58
- ? <CompactMessage key={i} entry={entry} />
59
- : <Message key={i} {...entry.message} />
60
- )}
61
- {pending.map((msg, i) => <Message key={`p-${i}`} {...msg.message} />)}
62
- </>}
63
- </Timeline>
64
-
33
+ <System>Search thoroughly, then write a summary.</System>
34
+ <Timeline />
65
35
  <Search />
66
36
  </>
67
37
  );
68
38
  }
69
39
 
70
- const model = openai({ model: "gpt-4o" });
71
- const app = createApp(ResearchAgent, { model });
40
+ const app = createApp(ResearchAgent, { model: openai({ model: "gpt-4o" }) });
72
41
  const result = await app.run({
73
- props: { topic: "quantum computing" },
74
- messages: [{ role: "user", content: [{ type: "text", text: "What's new in quantum computing?" }] }],
42
+ messages: [
43
+ { role: "user", content: [{ type: "text", text: "What's new in quantum computing?" }] },
44
+ ],
75
45
  });
76
-
77
46
  console.log(result.response);
78
47
  ```
79
48
 
49
+ ## Quick Start
50
+
51
+ ```bash
52
+ npm install agentick @agentick/openai zod
53
+ ```
54
+
55
+ Add to `tsconfig.json`:
56
+
57
+ ```json
58
+ {
59
+ "compilerOptions": {
60
+ "jsx": "react-jsx",
61
+ "jsxImportSource": "react"
62
+ }
63
+ }
64
+ ```
65
+
80
66
  ## Why Agentick
81
67
 
82
68
  Every other AI framework gives you a pipeline. A chain. A graph. You slot your prompt into a template, bolt on some tools, and hope the model figures it out.
@@ -87,12 +73,120 @@ There are no prompt templates because JSX _is_ the template language. There are
87
73
 
88
74
  This is application development, not chatbot configuration.
89
75
 
76
+ ## Built-in Components
77
+
78
+ Everything in the component tree compiles to what the model sees. Components are the building blocks — compose them to construct the context window.
79
+
80
+ ### Structure
81
+
82
+ | Component | Description |
83
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `<Timeline>` | Conversation history. Accepts a render function for full control, or renders with sensible defaults. Token budget compaction via `maxTokens`, `strategy`, `filter`, `limit`, `roles`. |
85
+ | `<Timeline.Provider>` | Context provider exposing timeline entries to descendants via `useTimelineContext()`. |
86
+ | `<Timeline.Messages>` | Renders messages from `Timeline.Provider` context. Optional `renderEntry` prop for custom rendering. |
87
+ | `<Section>` | Structured context block injected every tick. `audience` controls visibility: `"model"`, `"user"`, or `"all"`. |
88
+ | `<Model>` | Model configuration. Pass `engine` prop, or use adapter-specific components like `<OpenAIModel>` or `<GoogleModel>`. |
89
+
90
+ ### Messages
91
+
92
+ | Component | Description |
93
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------- |
94
+ | `<System>` | System/instruction message. |
95
+ | `<User>` | User message. |
96
+ | `<Assistant>` | Assistant message. |
97
+ | `<Message>` | Generic message — takes `role` prop. The primitive underlying all role-specific components. |
98
+ | `<ToolResult>` | Tool execution result. Requires `toolCallId`. |
99
+ | `<Event>` | Persisted application event. Use for structured logging that survives in the timeline. |
100
+ | `<Ephemeral>` | Non-persisted context. Visible during compilation but not saved to history. `position`: `"start"`, `"before-user"`, `"end"`. |
101
+ | `<Grounding>` | Semantic wrapper for grounding context (ephemeral). `audience`: `"model"`, `"user"`, `"both"`. |
102
+
103
+ ### Event Blocks
104
+
105
+ Use inside `<Event>` messages for structured event content:
106
+
107
+ | Component | Description |
108
+ | --------------- | ------------------------------------------------------------------------ |
109
+ | `<UserAction>` | User action block. Props: `action`, `actor?`, `target?`, `details?`. |
110
+ | `<SystemEvent>` | System event block. Props: `event`, `source?`, `data?`. |
111
+ | `<StateChange>` | State change block. Props: `entity`, `field?`, `from`, `to`, `trigger?`. |
112
+
113
+ ### Semantic Formatting
114
+
115
+ Compile to renderer-appropriate output (markdown, XML, etc.):
116
+
117
+ | Component | Description |
118
+ | ---------------------- | ------------------------------------------------------------------------------------------- |
119
+ | `<H1>`, `<H2>`, `<H3>` | Heading levels 1–3. |
120
+ | `<Header level={n}>` | Generic heading (levels 1–6). |
121
+ | `<Paragraph>` | Text paragraph block. |
122
+ | `<List>` | List container. `ordered` for numbered, `task` for checkboxes. `title` for a heading. |
123
+ | `<ListItem>` | List item. `checked` prop for task lists. |
124
+ | `<Table>` | Table. `headers`/`rows` props for data, or `<Row>`/`<Column>` children for JSX composition. |
125
+ | `<Row>` | Table row. `header` prop for header rows. |
126
+ | `<Column>` | Table column. `align`: `"left"`, `"center"`, `"right"`. |
127
+
128
+ ### Content Blocks
129
+
130
+ Typed content for composing rich message content:
131
+
132
+ | Component | Description |
133
+ | ------------ | ------------------------------------------------------------------------------------------- |
134
+ | `<Text>` | Text content. Children or `text` prop. Supports inline formatting: `<b>`, `<em>`, `<code>`. |
135
+ | `<Image>` | Image. `source: MediaSource` (URL or base64). |
136
+ | `<Document>` | Document attachment. `source: MediaSource`, `title?`. |
137
+ | `<Audio>` | Audio content. `source: MediaSource`, `transcript?`. |
138
+ | `<Video>` | Video content. `source: MediaSource`, `transcript?`. |
139
+ | `<Code>` | Code block. `language` prop (typescript, python, etc.). |
140
+ | `<Json>` | JSON data block. `data` prop for objects, or `text`/children for raw JSON strings. |
141
+
90
142
  ## The Context Is Yours
91
143
 
92
- The core insight: **only what you render gets sent to the model.** `<Timeline>` isn't a magic black box — it accepts a render function with `(history, pending)`, and you decide exactly how every message appears in the context window. Skip a message? The model never sees it. Rewrite it? That's what the model reads.
144
+ The core insight: **only what you render gets sent to the model.** `<Timeline>` isn't a magic black box — it accepts a render function, and you decide exactly how every message appears in the context window. Skip a message? The model never sees it. Rewrite it? That's what the model reads.
145
+
146
+ ```tsx
147
+ <Timeline>
148
+ {(history, pending) => (
149
+ <>
150
+ {history.map((entry, i) => {
151
+ const msg = entry.message;
152
+ const isOld = i < history.length - 6;
153
+
154
+ if (isOld && msg.role === "user") {
155
+ const textOnly = msg.content
156
+ .filter((b) => b.type === "text")
157
+ .map((b) => b.text)
158
+ .join(" ");
159
+ return (
160
+ <Message key={i} role="user">
161
+ [Earlier: {textOnly.slice(0, 100)}...]
162
+ </Message>
163
+ );
164
+ }
165
+
166
+ if (isOld && msg.role === "assistant") {
167
+ return (
168
+ <Message key={i} role="assistant">
169
+ [Previous response]
170
+ </Message>
171
+ );
172
+ }
173
+
174
+ return <Message key={i} {...msg} />;
175
+ })}
176
+ {pending.map((msg, i) => (
177
+ <Message key={`p-${i}`} {...msg.message} />
178
+ ))}
179
+ </>
180
+ )}
181
+ </Timeline>
182
+ ```
183
+
184
+ Images from 20 messages ago eating your context window? Render them as `[Image: beach sunset]`. Tool results from early in the conversation? Collapse them. Recent messages? Full detail. You write the function, you decide.
93
185
 
94
186
  ### Default — Just Works
95
187
 
188
+ With no children, `<Timeline />` renders conversation history with sensible defaults:
189
+
96
190
  ```tsx
97
191
  function SimpleAgent() {
98
192
  return (
@@ -104,80 +198,54 @@ function SimpleAgent() {
104
198
  }
105
199
  ```
106
200
 
107
- `<Timeline />` with no children renders conversation history with sensible defaults.
108
-
109
- ### Custom Rendering — Control What the Model Sees
110
-
111
- The render function receives `history` (completed entries) and `pending` (messages queued this tick). Only what you return from this function enters the model's context:
112
-
113
- ```tsx
114
- <Timeline>
115
- {(history, pending) => <>
116
- {history.map((entry, i) => {
117
- const msg = entry.message;
118
- const isOld = i < history.length - 6;
119
-
120
- // Old user messages — drop images, keep text summaries
121
- if (isOld && msg.role === "user") {
122
- const textOnly = msg.content
123
- .filter((b) => b.type === "text")
124
- .map((b) => b.text)
125
- .join(" ");
126
- return <Message key={i} role="user">[Earlier: {textOnly.slice(0, 100)}...]</Message>;
127
- }
128
-
129
- // Old assistant messages — collapse
130
- if (isOld && msg.role === "assistant") {
131
- return <Message key={i} role="assistant">[Previous response]</Message>;
132
- }
133
-
134
- // Recent messages — full fidelity
135
- return <Message key={i} {...msg} />;
136
- })}
137
- {pending.map((msg, i) => <Message key={`p-${i}`} {...msg.message} />)}
138
- </>}
139
- </Timeline>
140
- ```
141
-
142
- Images from 20 messages ago eating your context window? Render them as `[Image: beach sunset]`. Tool results from early in the conversation? Collapse them. Recent messages? Full detail. You write the function, you decide.
143
-
144
201
  ### Composability — It's React
145
202
 
146
- That render logic getting complex? Extract it into a component. It's React — components compose:
203
+ That render logic getting complex? Extract it into a component:
147
204
 
148
205
  ```tsx
149
- // A reusable component for rendering older messages compactly
150
206
  function CompactMessage({ entry }: { entry: COMTimelineEntry }) {
151
207
  const msg = entry.message;
152
208
 
153
- // Walk content blocks — handle each type differently
154
- const summary = msg.content.map((block) => {
155
- switch (block.type) {
156
- case "text": return block.text.slice(0, 80);
157
- case "image": return `[Image: ${block.source?.description ?? "image"}]`;
158
- case "tool_use": return `[Called ${block.name}]`;
159
- case "tool_result": return `[Result from ${block.name}]`;
160
- default: return "";
161
- }
162
- }).filter(Boolean).join(" | ");
209
+ const summary = msg.content
210
+ .map((block) => {
211
+ switch (block.type) {
212
+ case "text":
213
+ return block.text.slice(0, 80);
214
+ case "image":
215
+ return `[Image: ${block.source?.description ?? "image"}]`;
216
+ case "tool_use":
217
+ return `[Called ${block.name}]`;
218
+ case "tool_result":
219
+ return `[Result from ${block.name}]`;
220
+ default:
221
+ return "";
222
+ }
223
+ })
224
+ .filter(Boolean)
225
+ .join(" | ");
163
226
 
164
227
  return <Message role={msg.role}>{summary}</Message>;
165
228
  }
166
229
 
167
- // Use it in your Timeline
168
230
  function Agent() {
169
231
  return (
170
232
  <>
171
233
  <System>You are helpful.</System>
172
234
  <Timeline>
173
- {(history, pending) => <>
174
- {history.map((entry, i) =>
175
- i < history.length - 4
176
- ? <CompactMessage key={i} entry={entry} />
177
- : <Message key={i} {...entry.message} />
178
- )}
179
- {pending.map((msg, i) => <Message key={`p-${i}`} {...msg.message} />)}
180
- </>}
235
+ {(history, pending) => (
236
+ <>
237
+ {history.map((entry, i) =>
238
+ i < history.length - 4 ? (
239
+ <CompactMessage key={i} entry={entry} />
240
+ ) : (
241
+ <Message key={i} {...entry.message} />
242
+ ),
243
+ )}
244
+ {pending.map((msg, i) => (
245
+ <Message key={`p-${i}`} {...msg.message} />
246
+ ))}
247
+ </>
248
+ )}
181
249
  </Timeline>
182
250
  </>
183
251
  );
@@ -206,7 +274,7 @@ function NarrativeAgent() {
206
274
 
207
275
  The framework doesn't care how you structure the context. Multiple messages, one message, XML, prose — anything that compiles to content blocks gets sent.
208
276
 
209
- ### Sections — Structured Context for the Model
277
+ ### Sections — Structured Context
210
278
 
211
279
  ```tsx
212
280
  function AgentWithContext({ userId }: { userId: string }) {
@@ -215,11 +283,9 @@ function AgentWithContext({ userId }: { userId: string }) {
215
283
  return (
216
284
  <>
217
285
  <System>You are a support agent.</System>
218
-
219
286
  <Section id="user-context" audience="model">
220
287
  Customer: {profile?.name}, Plan: {profile?.plan}, Since: {profile?.joinDate}
221
288
  </Section>
222
-
223
289
  <Timeline />
224
290
  <TicketTool />
225
291
  </>
@@ -229,43 +295,106 @@ function AgentWithContext({ userId }: { userId: string }) {
229
295
 
230
296
  `<Section>` injects structured context that the model sees every tick — live data, computed state, whatever you need. The `audience` prop controls visibility (`"model"`, `"user"`, or `"all"`).
231
297
 
232
- ## Hooks Control Everything
298
+ ## Hooks
299
+
300
+ Hooks are real React hooks — `useState`, `useEffect`, `useMemo` — plus lifecycle hooks that fire at each phase of the agent execution loop.
301
+
302
+ ### All Hooks
303
+
304
+ #### Lifecycle
305
+
306
+ | Hook | Description |
307
+ | --------------------- | -------------------------------------------------------------------------------------------------- |
308
+ | `useOnMount(cb)` | Run once when component first mounts. |
309
+ | `useOnUnmount(cb)` | Run once when component unmounts. |
310
+ | `useOnTickStart(cb)` | Run at start of each tick (tick 2+). Receives `(tickState, ctx)`. |
311
+ | `useOnTickEnd(cb)` | Run at end of each tick. Receives `(result, ctx)`. Return `false` or call `result.stop()` to halt. |
312
+ | `useAfterCompile(cb)` | Run after compilation completes. Receives `(compiled, ctx)`. |
313
+ | `useContinuation(cb)` | Control whether execution continues. Sugar for `useOnTickEnd`. |
314
+
315
+ #### State & Signals
316
+
317
+ | Hook | Description |
318
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------- |
319
+ | `useSignal(initial)` | Reactive signal. `.set()`, `.update()`, `.subscribe()`. Reads outside render, triggers reconciliation on write. |
320
+ | `useComputed(fn, deps)` | Computed signal. Auto-updates when dependencies change. |
321
+ | `useComState(key, default?)` | Reactive COM state. Bidirectional sync with the context object model. |
322
+
323
+ Standalone signal factories (no hook rules — use anywhere):
324
+
325
+ | Function | Description |
326
+ | ----------------- | ------------------------------------------------------------------------------------------ |
327
+ | `signal(initial)` | Create a signal. |
328
+ | `computed(fn)` | Create a computed signal. |
329
+ | `effect(fn)` | Run side effect with automatic dependency tracking. Returns `EffectRef` with `.dispose()`. |
330
+ | `batch(fn)` | Batch signal updates — effects fire once after all updates. |
331
+ | `untracked(fn)` | Read signals without tracking as dependencies. |
332
+
333
+ #### Data
334
+
335
+ | Hook | Description |
336
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
337
+ | `useData(key, fetcher, deps?)` | Async data fetch with resolve-then-render. Throws promise on first render, returns cached value on re-render. |
338
+ | `useInvalidateData()` | Returns `(pattern: string \| RegExp) => void` to invalidate cached data. |
339
+
340
+ #### Knobs (Model-Visible State)
341
+
342
+ Knobs are reactive values the model can see _and set_ via tool calls:
343
+
344
+ | API | Description |
345
+ | --------------------------- | ------------------------------------------------------------------------------ |
346
+ | `knob(default, opts?)` | Create a knob descriptor at config level. |
347
+ | `useKnob(name, descriptor)` | Hook returning `[resolvedValue, setValue]`. |
348
+ | `<Knobs />` | Component that renders all knobs as a `<Section>` + registers `set_knob` tool. |
349
+ | `<Knobs.Provider>` | Context provider for custom knob rendering. |
350
+ | `<Knobs.Controls>` | Renders knob controls from `Knobs.Provider` context. |
351
+ | `isKnob(value)` | Type guard for knob descriptors. |
233
352
 
234
- Hooks are where the real power lives. They're real React hooks — `useState`, `useEffect`, `useMemo` — plus lifecycle hooks that fire at each phase of execution.
353
+ #### Context & Environment
235
354
 
236
- ### `useContinuation` — Add Stop Conditions
355
+ | Hook | Description |
356
+ | -------------------------- | ------------------------------------------------------------------ |
357
+ | `useCom()` | Access the COM (context object model) — state, timeline, channels. |
358
+ | `useTickState()` | Current tick state: `{tick, previous, queuedMessages}`. |
359
+ | `useRuntimeStore()` | Runtime data store (hooks, knobs, lifecycle callbacks). |
360
+ | `useFormatter()` | Access message formatter context. |
361
+ | `useContextInfo()` | Real-time context utilization: token counts, utilization %. |
362
+ | `useTimelineContext()` | Timeline context (requires `Timeline.Provider` ancestor). |
363
+ | `useConversationHistory()` | Full conversation history from COM (no provider needed). |
237
364
 
238
- The agent loop auto-continues when the model makes tool calls. `useContinuation` lets you add your own stop conditions:
365
+ #### React (re-exported)
366
+
367
+ All standard React hooks work in agent components: `useState`, `useEffect`, `useReducer`, `useMemo`, `useCallback`, `useRef`.
368
+
369
+ ### Stop Conditions
370
+
371
+ The agent loop auto-continues when the model makes tool calls. `useContinuation` adds your own stop conditions:
239
372
 
240
373
  ```tsx
241
- // Stop after a done marker
242
374
  useContinuation((result) => !result.text?.includes("<DONE>"));
243
375
 
244
- // Stop after too many ticks or too many tokens
245
376
  useContinuation((result) => {
246
- if (result.tick >= 10) { result.stop("max-ticks"); return false; }
377
+ if (result.tick >= 10) {
378
+ result.stop("max-ticks");
379
+ return false;
380
+ }
247
381
  if (result.usage && result.usage.totalTokens > 100_000) {
248
- result.stop("token-budget"); return false;
382
+ result.stop("token-budget");
383
+ return false;
249
384
  }
250
385
  });
251
386
  ```
252
387
 
253
- ### `useOnTickEnd` — Run Code After Every Model Response
388
+ ### Between-Tick Logic
254
389
 
255
- `useContinuation` is sugar for `useOnTickEnd`. Use the full version when you need to do real work between ticks:
390
+ `useContinuation` is sugar for `useOnTickEnd`. Use the full version when you need to do real work:
256
391
 
257
392
  ```tsx
258
393
  function VerifiedAgent() {
259
394
  useOnTickEnd(async (result) => {
260
- // Log every tick
261
- analytics.track("tick", { tokens: result.usage?.totalTokens });
262
-
263
- // When the model is done (no more tool calls), verify before accepting
264
395
  if (result.text && !result.toolCalls.length) {
265
396
  const quality = await verifyWithModel(result.text);
266
- if (!quality.acceptable) {
267
- result.continue("failed-verification"); // force another tick
268
- }
397
+ if (!quality.acceptable) result.continue("failed-verification");
269
398
  }
270
399
  });
271
400
 
@@ -278,7 +407,7 @@ function VerifiedAgent() {
278
407
  }
279
408
  ```
280
409
 
281
- ### Build Your Own Hooks
410
+ ### Custom Hooks
282
411
 
283
412
  Custom hooks work exactly like React — they're just functions that call other hooks:
284
413
 
@@ -313,48 +442,18 @@ function CarefulAgent() {
313
442
  return (
314
443
  <>
315
444
  <System>You have a token budget. Be concise.</System>
316
- <Section id="budget" audience="model">Tokens used: {spent}</Section>
445
+ <Section id="budget" audience="model">
446
+ Tokens used: {spent}
447
+ </Section>
317
448
  <Timeline />
318
449
  </>
319
450
  );
320
451
  }
321
452
  ```
322
453
 
323
- ## Everything Is Dual-Use
454
+ ## Tools Render State
324
455
 
325
- `createTool` and `createAdapter` (used under the hood by `openai()`, `google()`, etc.) return objects that work both as JSX components and as direct function calls:
326
-
327
- ```tsx
328
- const Search = createTool({ name: "search", ... });
329
- const model = openai({ model: "gpt-4o" });
330
-
331
- // As JSX — self-closing tags in the component tree
332
- <model temperature={0.2} />
333
- <Search />
334
-
335
- // As direct calls — use programmatically
336
- const handle = await model.generate(input);
337
- const output = await Search.run({ query: "test" });
338
- ```
339
-
340
- Context is maintained with AsyncLocalStorage, so tools and hooks can access session state from anywhere — no prop drilling required.
341
-
342
- ## More Examples
343
-
344
- ### One-Shot Run
345
-
346
- ```tsx
347
- import { run, System, Timeline } from "@agentick/core";
348
- import { openai } from "@agentick/openai";
349
-
350
- const result = await run(
351
- <><System>You are helpful.</System><Timeline /></>,
352
- { model: openai({ model: "gpt-4o" }), messages: [{ role: "user", content: [{ type: "text", text: "Hello!" }] }] },
353
- );
354
- console.log(result.response);
355
- ```
356
-
357
- ### Stateful Tool with Render
456
+ Tools aren't just functions the model calls they render their state back into the context window. The model sees the current state _every time it thinks_, not just in the tool response.
358
457
 
359
458
  ```tsx
360
459
  const TodoTool = createTool({
@@ -365,12 +464,11 @@ const TodoTool = createTool({
365
464
  text: z.string().optional(),
366
465
  id: z.number().optional(),
367
466
  }),
368
- handler: async ({ action, text, id }) => {
467
+ handler: async ({ action, text, id }, ctx) => {
369
468
  if (action === "add") todos.push({ id: todos.length, text, done: false });
370
469
  if (action === "complete") todos[id!].done = true;
371
470
  return [{ type: "text", text: "Done." }];
372
471
  },
373
- // render() injects live state into the model's context every tick
374
472
  render: () => (
375
473
  <Section id="todos" audience="model">
376
474
  Current todos: {JSON.stringify(todos)}
@@ -379,15 +477,45 @@ const TodoTool = createTool({
379
477
  });
380
478
  ```
381
479
 
382
- The model sees the current todo list _every time it thinks_ — not just in the tool response, but as persistent context. When it decides what to do next, the state is right there.
480
+ Everything is dual-use tools and models work as JSX components in the tree _and_ as direct function calls:
481
+
482
+ ```tsx
483
+ // JSX — in the component tree
484
+ <Search />
485
+ <model temperature={0.2} />
486
+
487
+ // Direct calls — use programmatically
488
+ const output = await Search.run({ query: "test" });
489
+ const handle = await model.generate(input);
490
+ ```
491
+
492
+ ### Tool Types
493
+
494
+ Tools have execution types and intents that control routing and behavior:
383
495
 
384
- ### Multi-Turn Session
496
+ | Execution Type | Description |
497
+ | -------------- | ---------------------------------------- |
498
+ | `SERVER` | Executes on server (default). |
499
+ | `CLIENT` | Executes in browser. |
500
+ | `MCP` | Routed to Model Context Protocol server. |
501
+ | `PROVIDER` | Handled by model provider natively. |
502
+
503
+ | Intent | Description |
504
+ | --------- | -------------------------- |
505
+ | `COMPUTE` | Returns data (default). |
506
+ | `ACTION` | Performs side effects. |
507
+ | `RENDER` | Produces UI/visualization. |
508
+
509
+ ## Sessions
385
510
 
386
511
  ```tsx
387
512
  const app = createApp(Agent, { model: openai({ model: "gpt-4o" }) });
388
513
  const session = await app.session("conv-1");
389
514
 
390
- const msg = (text: string) => ({ role: "user" as const, content: [{ type: "text" as const, text }] });
515
+ const msg = (text: string) => ({
516
+ role: "user" as const,
517
+ content: [{ type: "text" as const, text }],
518
+ });
391
519
 
392
520
  await session.send({ messages: [msg("Hi there!")] });
393
521
  await session.send({ messages: [msg("Tell me a joke")] });
@@ -397,23 +525,35 @@ for await (const event of session.send({ messages: [msg("Another one")] })) {
397
525
  if (event.type === "content_delta") process.stdout.write(event.delta);
398
526
  }
399
527
 
400
- session.close();
528
+ await session.close();
529
+ ```
530
+
531
+ Sessions are long-lived conversation contexts. Each `send()` creates an **execution** (one user message → model response cycle). Each model API call within an execution is a **tick**. Multi-tick executions happen automatically with tool use.
532
+
401
533
  ```
534
+ Session
535
+ ├── Execution 1 (user: "Hello")
536
+ │ └── Tick 1 → model response
537
+ ├── Execution 2 (user: "Use calculator")
538
+ │ ├── Tick 1 → tool_use (calculator)
539
+ │ └── Tick 2 → final response
540
+ └── Execution 3 ...
541
+ ```
542
+
543
+ Session states: `idle` → `running` → `idle` (or `closed`).
402
544
 
403
545
  ### Dynamic Model Selection
404
546
 
405
- Models are JSX components — conditionally render them to switch models mid-session:
547
+ Models are JSX components — conditionally render them:
406
548
 
407
549
  ```tsx
408
550
  const gpt = openai({ model: "gpt-4o" });
409
551
  const gemini = google({ model: "gemini-2.5-pro" });
410
552
 
411
553
  function AdaptiveAgent({ task }: { task: string }) {
412
- const needsCreativity = task.includes("creative");
413
-
414
554
  return (
415
555
  <>
416
- {needsCreativity ? <gemini temperature={0.9} /> : <gpt temperature={0.2} />}
556
+ {task.includes("creative") ? <gemini temperature={0.9} /> : <gpt temperature={0.2} />}
417
557
  <System>Handle this task: {task}</System>
418
558
  <Timeline />
419
559
  </>
@@ -421,50 +561,238 @@ function AdaptiveAgent({ task }: { task: string }) {
421
561
  }
422
562
  ```
423
563
 
424
- ## Packages
564
+ ## Execution Environments
565
+
566
+ The context window is JSX. But what _consumes_ that context — and how tool calls _execute_ — is pluggable.
567
+
568
+ An `ExecutionEnvironment` is a swappable backend that sits between the compiled context and execution. It transforms what the model sees, intercepts how tools run, and manages its own lifecycle state. Your agent code doesn't change — the environment changes the execution model underneath it.
425
569
 
426
- | Package | Description |
427
- | --------------------- | ------------------------------------------------------------ |
428
- | `@agentick/core` | Reconciler, components, hooks, tools, sessions |
429
- | `@agentick/kernel` | Execution kernel — procedures, context, middleware, channels |
430
- | `@agentick/shared` | Platform-independent types and utilities |
431
- | `@agentick/openai` | OpenAI adapter (GPT-4o, o1, etc.) |
432
- | `@agentick/google` | Google AI adapter (Gemini) |
433
- | `@agentick/ai-sdk` | Vercel AI SDK adapter (any provider) |
434
- | `@agentick/gateway` | Multi-app server with auth, routing, and channels |
435
- | `@agentick/express` | Express.js integration |
436
- | `@agentick/nestjs` | NestJS integration |
437
- | `@agentick/client` | TypeScript client for gateway connections |
438
- | `@agentick/react` | React hooks for building UIs over sessions |
439
- | `@agentick/devtools` | Fiber tree inspector, tick scrubber, token tracker |
440
- | `@agentick/cli` | CLI for running agents |
441
- | `@agentick/server` | Server utilities |
442
- | `@agentick/socket.io` | Socket.IO transport |
443
-
444
- ```
445
- ┌─────────────────────────────────────────────────────────────────┐
446
- │ Applications │
447
- (express, nestjs, cli, user apps)
448
- └──────────────────────────┬──────────────────────────────────────┘
449
-
450
- ┌──────────────────────────┴──────────────────────────────────────┐
451
- │ Framework Layer │
452
- │ @agentick/core @agentick/gateway @agentick/client │
453
- │ @agentick/express @agentick/devtools │
454
- └──────────────────────────┬──────────────────────────────────────┘
455
-
456
- ┌──────────────────────────┴──────────────────────────────────────┐
457
- │ Adapter Layer │
458
- │ @agentick/openai @agentick/google @agentick/ai-sdk │
459
- └──────────────────────────┬──────────────────────────────────────┘
460
-
461
- ┌──────────────────────────┴──────────────────────────────────────┐
462
- │ Foundation Layer │
463
- │ @agentick/kernel @agentick/shared │
464
- │ (Node.js only) (Platform-independent) │
465
- └─────────────────────────────────────────────────────────────────┘
570
+ ```tsx
571
+ import { type ExecutionEnvironment } from "@agentick/core";
572
+
573
+ const repl: ExecutionEnvironment = {
574
+ name: "repl",
575
+
576
+ // The model sees command descriptions instead of tool schemas
577
+ prepareModelInput(compiled, tools) {
578
+ return { ...compiled, tools: [executeTool] };
579
+ },
580
+
581
+ // "execute" calls go to a sandbox; everything else runs normally
582
+ async executeToolCall(call, tool, next) {
583
+ if (call.name === "execute") return sandbox.run(call.input.code);
584
+ return next();
585
+ },
586
+
587
+ onSessionInit(session) {
588
+ sandbox.create(session.id);
589
+ },
590
+ onDestroy(session) {
591
+ sandbox.destroy(session.id);
592
+ },
593
+ };
594
+
595
+ const app = createApp(Agent, { model, environment: repl });
596
+ ```
597
+
598
+ Same agent, same JSX, different execution model. Build once — run against standard tool_use in production, a sandboxed REPL for code execution, a human-approval gateway for sensitive operations.
599
+
600
+ All hooks are optional. Without an environment, standard model → tool_use behavior applies. Environments are inherited by spawned child sessions — override per-child via `SpawnOptions`:
601
+
602
+ ```tsx
603
+ await session.spawn(CodeAgent, { messages }, { environment: sandboxEnv });
604
+ ```
605
+
606
+ ## Testing
607
+
608
+ Agentick includes a full testing toolkit. Render agents, compile context, mock models, and assert on behavior — all without making real API calls.
609
+
610
+ ### `renderAgent` — Full Execution
611
+
612
+ Render an agent in a test environment with a mock model:
613
+
614
+ ```tsx
615
+ import { renderAgent, cleanup } from "@agentick/core/testing";
616
+ import { afterEach, test, expect } from "vitest";
617
+
618
+ afterEach(cleanup);
619
+
620
+ test("research agent searches then summarizes", async () => {
621
+ const { send, model } = renderAgent(<ResearchAgent />);
622
+
623
+ // Queue model responses
624
+ model.addResponse({ text: "", toolCalls: [{ name: "search", input: { query: "quantum" } }] });
625
+ model.addResponse({ text: "Here's a summary of quantum computing..." });
626
+
627
+ const result = await send("What's new in quantum computing?");
628
+
629
+ expect(result.response).toContain("summary");
630
+ expect(model.calls).toHaveLength(2); // two ticks
631
+ });
632
+ ```
633
+
634
+ ### `compileAgent` — Inspect Context
635
+
636
+ Compile an agent without executing to inspect what the model would see:
637
+
638
+ ```tsx
639
+ test("agent includes user context in system prompt", async () => {
640
+ const { sections, tools } = compileAgent(<AgentWithContext userId="user-123" />);
641
+
642
+ expect(sections).toContainEqual(expect.objectContaining({ id: "user-context" }));
643
+ expect(tools.map((t) => t.name)).toContain("create_ticket");
644
+ });
645
+ ```
646
+
647
+ ### Test Adapter
648
+
649
+ Create a mock model adapter for fine-grained control over streaming:
650
+
651
+ ```tsx
652
+ import { createTestAdapter } from "@agentick/core/testing";
653
+
654
+ const adapter = createTestAdapter();
655
+
656
+ // Simulate streaming chunks
657
+ adapter.stream([
658
+ { type: "text", text: "Hello " },
659
+ { type: "text", text: "world!" },
660
+ { type: "finish", stopReason: "end_turn" },
661
+ ]);
662
+ ```
663
+
664
+ ### Mocks & Helpers
665
+
666
+ | Utility | Description |
667
+ | ----------------------------- | ----------------------------------------------------- |
668
+ | `createMockApp()` | Mock app for client/transport tests. |
669
+ | `createMockSession()` | Mock session with send/close/abort. |
670
+ | `createMockExecutionHandle()` | Mock execution handle (async iterable + result). |
671
+ | `createTestEnvironment()` | Mock execution environment with call tracking. |
672
+ | `createMockCom()` | Mock COM for hook tests. |
673
+ | `createMockTickState()` | Mock tick state. |
674
+ | `createMockTickResult()` | Mock tick result for `useOnTickEnd` tests. |
675
+ | `makeTimelineEntry()` | Create timeline entries for assertions. |
676
+ | `act(fn)` / `actSync(fn)` | Execute in act context. |
677
+ | `waitFor(fn)` | Poll until condition is met. |
678
+ | `flushMicrotasks()` | Flush pending microtasks. |
679
+ | `createDeferred()` | Create deferred promise with external resolve/reject. |
680
+
681
+ ## Terminal UI
682
+
683
+ `@agentick/tui` provides an Ink-based terminal interface for chatting with agents — locally or over the network.
684
+
685
+ ```bash
686
+ npm install @agentick/tui
687
+ ```
688
+
689
+ ### Local — In-Process
690
+
691
+ Connect directly to an app. No server needed:
692
+
693
+ ```tsx
694
+ import { createTUI } from "@agentick/tui";
695
+ import { createApp, System, Timeline } from "@agentick/core";
696
+ import { openai } from "@agentick/openai";
697
+
698
+ function Agent() {
699
+ return (
700
+ <>
701
+ <System>You are helpful.</System>
702
+ <Timeline />
703
+ </>
704
+ );
705
+ }
706
+
707
+ const app = createApp(Agent, { model: openai({ model: "gpt-4o" }) });
708
+ const tui = createTUI({ app });
709
+ await tui.start();
466
710
  ```
467
711
 
712
+ ### Remote — Over SSE
713
+
714
+ Connect to a running gateway or express server:
715
+
716
+ ```tsx
717
+ const tui = createTUI({ url: "http://localhost:3000/api" });
718
+ await tui.start();
719
+ ```
720
+
721
+ ### CLI
722
+
723
+ ```bash
724
+ # Run a local agent file
725
+ agentick-tui --app ./my-agent.tsx
726
+
727
+ # Connect to a remote server
728
+ agentick-tui --url http://localhost:3000/api
729
+
730
+ # Custom export name
731
+ agentick-tui --app ./agents.tsx --export SalesAgent
732
+
733
+ # Custom UI component
734
+ agentick-tui --app ./my-agent.tsx --ui ./dashboard.tsx
735
+ ```
736
+
737
+ ### Pluggable UI
738
+
739
+ The TUI ships with a default `Chat` component, but you can provide your own:
740
+
741
+ ```tsx
742
+ import { createTUI, type TUIComponent } from "@agentick/tui";
743
+ import { useSession, useStreamingText } from "@agentick/react";
744
+
745
+ const Dashboard: TUIComponent = ({ sessionId }) => {
746
+ const { send } = useSession({ sessionId });
747
+ const { text, isStreaming } = useStreamingText({ sessionId });
748
+ // ... your Ink components
749
+ };
750
+
751
+ const tui = createTUI({ app, ui: Dashboard });
752
+ ```
753
+
754
+ All building-block components are exported for custom UIs: `MessageList`, `StreamingMessage`, `ToolCallIndicator`, `ToolConfirmationPrompt`, `InputBar`, `ErrorDisplay`.
755
+
756
+ ## React Hooks for UIs
757
+
758
+ `@agentick/react` provides hooks for building browser or terminal UIs over agent sessions. These are pure React — no browser APIs — so they work in both React DOM and Ink.
759
+
760
+ ```bash
761
+ npm install @agentick/react
762
+ ```
763
+
764
+ | Hook | Description |
765
+ | ------------------------- | -------------------------------------------------------------- |
766
+ | `useClient()` | Access the Agentick client from context. |
767
+ | `useConnection()` | SSE connection state: `{state, isConnected, isConnecting}`. |
768
+ | `useSession(opts?)` | Session accessor: `{send, abort, close, subscribe, accessor}`. |
769
+ | `useEvents(opts?)` | Subscribe to stream events. Returns `{event, clear()}`. |
770
+ | `useStreamingText(opts?)` | Accumulated streaming text: `{text, isStreaming, clear()}`. |
771
+ | `useContextInfo(opts?)` | Context utilization info (token counts, %). |
772
+
773
+ Wrap your app in `<AgentickProvider client={client}>` to provide the client context.
774
+
775
+ ## Packages
776
+
777
+ | Package | Description |
778
+ | --------------------- | ----------------------------------------------------------------- |
779
+ | `@agentick/core` | Reconciler, components, hooks, tools, sessions, testing utilities |
780
+ | `@agentick/kernel` | Execution kernel — procedures, context, middleware, channels |
781
+ | `@agentick/shared` | Platform-independent types and utilities |
782
+ | `@agentick/openai` | OpenAI adapter (GPT-4o, o1, etc.) |
783
+ | `@agentick/google` | Google AI adapter (Gemini) |
784
+ | `@agentick/ai-sdk` | Vercel AI SDK adapter (any provider) |
785
+ | `@agentick/gateway` | Multi-app server with auth, routing, and channels |
786
+ | `@agentick/express` | Express.js integration |
787
+ | `@agentick/nestjs` | NestJS integration |
788
+ | `@agentick/client` | TypeScript client for gateway connections |
789
+ | `@agentick/react` | React hooks for building UIs over sessions |
790
+ | `@agentick/tui` | Terminal UI — Ink-based chat interface for local or remote agents |
791
+ | `@agentick/devtools` | Fiber tree inspector, tick scrubber, token tracker |
792
+ | `@agentick/cli` | CLI for running agents |
793
+ | `@agentick/server` | Server utilities |
794
+ | `@agentick/socket.io` | Socket.IO transport |
795
+
468
796
  ## Adapters
469
797
 
470
798
  Three built-in, same interface. Or build your own — implement `prepareInput`, `mapChunk`, `execute`, and `executeStream`. See [`packages/adapters/README.md`](packages/adapters/README.md).
@@ -479,14 +807,50 @@ const gemini = google({ model: "gemini-2.5-pro" });
479
807
  const sdk = aiSdk({ model: yourAiSdkModel });
480
808
  ```
481
809
 
810
+ Adapters return a `ModelClass` — callable _and_ a JSX component:
811
+
812
+ ```tsx
813
+ // As JSX — configure model in the component tree
814
+ <gpt temperature={0.2} maxTokens={1000} />;
815
+
816
+ // As function — call programmatically
817
+ const handle = await gpt.generate(input);
818
+ ```
819
+
482
820
  ## DevTools
483
821
 
822
+ ### Agentick DevTools
823
+
484
824
  ```tsx
485
825
  const app = createApp(Agent, { model, devTools: true });
486
826
  ```
487
827
 
488
828
  Fiber tree inspector, tick-by-tick scrubber, token usage tracking, real-time execution timeline. Record full sessions for replay with `session({ recording: 'full' })`.
489
829
 
830
+ ### React DevTools
831
+
832
+ Agentick is built on `react-reconciler` — the same foundation as React DOM and React Native. This means [React DevTools](https://github.com/facebook/react/tree/main/packages/react-devtools) works out of the box. You can inspect the component tree that compiles into the model's context window, live.
833
+
834
+ ```sh
835
+ npm install --save-dev react-devtools-core
836
+ ```
837
+
838
+ ```tsx
839
+ import { enableReactDevTools } from "@agentick/core";
840
+
841
+ enableReactDevTools(); // connects to standalone DevTools on port 8097
842
+ ```
843
+
844
+ ```sh
845
+ # Terminal 1: start React DevTools
846
+ npx react-devtools
847
+
848
+ # Terminal 2: run your agent
849
+ node my-agent.js
850
+ ```
851
+
852
+ You'll see the full component tree — `<System>`, `<Timeline>`, `<Section>`, your custom components, tools — in the same inspector you use for web and mobile apps. Inspect props, watch state changes between ticks, and see exactly what compiles into the context window.
853
+
490
854
  ## Gateway
491
855
 
492
856
  Deploy multiple apps behind a single server with auth, routing, and channel adapters:
@@ -501,23 +865,6 @@ const gateway = createGateway({
501
865
  });
502
866
  ```
503
867
 
504
- ## Quick Start
505
-
506
- ```bash
507
- npm install agentick @agentick/openai zod
508
- ```
509
-
510
- **TypeScript config** — add to `tsconfig.json`:
511
-
512
- ```json
513
- {
514
- "compilerOptions": {
515
- "jsx": "react-jsx",
516
- "jsxImportSource": "react"
517
- }
518
- }
519
- ```
520
-
521
868
  ## License
522
869
 
523
870
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentick",
3
- "version": "0.2.0",
4
- "description": "Build agents like you build apps.",
3
+ "version": "0.3.0",
4
+ "description": "The component framework for AI.",
5
5
  "keywords": [
6
6
  "agent",
7
7
  "ai",
@@ -9,7 +9,7 @@
9
9
  "jsx",
10
10
  "react"
11
11
  ],
12
- "license": "ISC",
12
+ "license": "MIT",
13
13
  "author": "Ryan Lindgren",
14
14
  "repository": {
15
15
  "type": "git",
@@ -31,9 +31,9 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@agentick/core": "0.2.0",
35
- "@agentick/agent": "0.2.0",
36
- "@agentick/guardrails": "0.2.0"
34
+ "@agentick/agent": "0.3.0",
35
+ "@agentick/core": "0.3.0",
36
+ "@agentick/guardrails": "0.2.1"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc -p tsconfig.build.json",