create-theokit 1.0.15 → 1.0.17

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.
@@ -0,0 +1,36 @@
1
+ // @vitest-environment jsdom
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { ChatThread, ChatMessage, type UIMessage } from '@theokit/ui'
5
+ import Page from './page'
6
+
7
+ /**
8
+ * Guards the chat page against `@theokit/ui` API drift (issue #80): a fresh scaffold
9
+ * must both type-check AND render. Renders the real React tree in jsdom.
10
+ */
11
+ describe('default chat page', () => {
12
+ it('renders the empty state + quick actions + composer on first load', () => {
13
+ render(<Page />)
14
+ expect(screen.getByText('What should we build today?')).toBeDefined()
15
+ expect(screen.getByText('Summarize this page')).toBeDefined()
16
+ expect(screen.getByText('Show available tools')).toBeDefined()
17
+ expect(screen.getByText('Start a new conversation')).toBeDefined()
18
+ expect(screen.getByPlaceholderText('Ask the agent…')).toBeDefined()
19
+ expect(screen.getByLabelText('Open command palette')).toBeDefined()
20
+ })
21
+
22
+ it('ChatMessage accepts a UIMessage and renders its message container (auto-dispatch)', () => {
23
+ const assistant: UIMessage = {
24
+ id: 'a-0',
25
+ role: 'assistant',
26
+ parts: [{ type: 'text', text: 'Reading src/index.ts', state: 'done' }],
27
+ }
28
+ const { container } = render(
29
+ <ChatThread>
30
+ <ChatMessage message={assistant} />
31
+ </ChatThread>,
32
+ )
33
+ expect(container.querySelector('[data-slot="chat-message"]')).not.toBeNull()
34
+ expect(container.querySelector('[data-theo-chat-message="assistant"]')).not.toBeNull()
35
+ })
36
+ })
@@ -5,57 +5,45 @@ import {
5
5
  ChatThread,
6
6
  ChatMessage,
7
7
  ChatComposer,
8
- ToolCallCard,
9
8
  AgentStreaming,
10
9
  AgentErrorCard,
11
- EmptyState,
12
10
  QuickActionChips,
13
11
  ContextWindowBar,
12
+ type UIMessage,
13
+ type QuickAction,
14
+ } from '@theokit/ui'
15
+ import {
16
+ EmptyState,
14
17
  CommandPalette,
15
18
  Avatar,
16
19
  Tooltip,
17
20
  Button,
18
21
  ScrollArea,
19
- type Message,
20
- type QuickAction,
21
22
  type CommandItem,
22
- type ToolCallStatus,
23
- } from '@theokit/ui'
23
+ } from '@usetheo/ui'
24
24
  import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
25
- import { useAgentStream } from 'theokit/client'
25
+ import { useAgent } from 'theokit/client'
26
26
 
27
27
  /**
28
28
  * Default scaffold — an Agent Surface, composed entirely from TheoUI.
29
29
  *
30
- * ChatThread / ChatMessage → conversation
31
- * ToolCallCard → expandable tool invocations
30
+ * ChatThread / ChatMessage → conversation (ChatMessage auto-dispatches text,
31
+ * tool-call, and reasoning parts of each UIMessage)
32
32
  * AgentStreaming → streaming indicator
33
33
  * AgentErrorCard → error display
34
34
  * ChatComposer → bottom input bar
35
35
  * EmptyState → first-load screen
36
36
  * ContextWindowBar → context usage at top
37
37
  * CommandPalette → ⌘K quick actions
38
- * Avatar → assistant face in messages
38
+ * Avatar → assistant/user face in messages
39
39
  * Tooltip → hints on icons
40
40
  *
41
- * `useAgentStream` handles SSE consumption, AbortController cleanup, and
42
- * StrictMode safety. Replace the mock at server/routes/chat.ts with your
43
- * real LLM provider (OpenAI / Anthropic / local).
41
+ * `useAgent` binds to the `agents/chat.ts` endpoint, consumes the ai-sdk
42
+ * `UIMessageStream`, and handles AbortController cleanup + StrictMode safety.
43
+ * `messages` are the reconstructed ASSISTANT `UIMessage[]`; user turns are tracked
44
+ * locally and interleaved. Edit `agents/chat.ts` to pick your model / add tools.
44
45
  */
45
46
 
46
- type ConversationItem =
47
- | { kind: 'message'; id: string; role: 'user' | 'assistant'; content: string; timestamp: string }
48
- | {
49
- kind: 'tool'
50
- id: string
51
- tool: string
52
- target?: string
53
- status: ToolCallStatus
54
- output?: string
55
- timestamp: string
56
- }
57
- | { kind: 'error'; id: string; message: string; timestamp: string }
58
-
59
47
  const QUICK_ACTIONS: QuickAction[] = [
60
48
  { id: 'summarize', label: 'Summarize this page', icon: Sparkles },
61
49
  { id: 'tools', label: 'Show available tools', icon: Wrench },
@@ -69,10 +57,11 @@ const COMMAND_ITEMS: CommandItem[] = QUICK_ACTIONS.map((a) => ({
69
57
  group: 'Quick actions',
70
58
  }))
71
59
 
72
- // Mock context-window usage — replace with real model state.
60
+ // Display-only context-window hint. The agent's real model lives in `agents/chat.ts`
61
+ // (`model: 'openai/gpt-4o-mini'`); wire real token counts from the stream when you need them.
73
62
  const CONTEXT_USED = 4_200
74
63
  const CONTEXT_TOTAL = 200_000
75
- const MODEL_NAME = 'mock-llm'
64
+ const MODEL_NAME = 'gpt-4o-mini'
76
65
 
77
66
  const ASSISTANT_AVATAR = (
78
67
  <Avatar size="sm" tone="primary">
@@ -87,9 +76,9 @@ const USER_AVATAR = (
87
76
 
88
77
  export default function Page() {
89
78
  const [composerValue, setComposerValue] = useState('')
90
- const [userMessages, setUserMessages] = useState<ConversationItem[]>([])
79
+ const [userMessages, setUserMessages] = useState<UIMessage[]>([])
91
80
  const [paletteOpen, setPaletteOpen] = useState(false)
92
- const { events, send, status, reset } = useAgentStream<{ message: string }>('/api/chat')
81
+ const { messages, send, status, reset } = useAgent<{ message: string }>('/api/agents/chat')
93
82
 
94
83
  // ⌘K / Ctrl+K opens the CommandPalette.
95
84
  useEffect(() => {
@@ -103,52 +92,29 @@ export default function Page() {
103
92
  return () => window.removeEventListener('keydown', onKey)
104
93
  }, [])
105
94
 
106
- const items = useMemo<ConversationItem[]>(() => {
107
- const ts = new Date().toISOString()
108
- const agentItems: ConversationItem[] = events.map((event, i) => {
109
- const id = `e-${i}`
110
- switch (event.type) {
111
- case 'message':
112
- return { kind: 'message', id, role: 'assistant', content: event.content, timestamp: ts }
113
- case 'tool_call':
114
- return {
115
- kind: 'tool',
116
- id,
117
- tool: event.name,
118
- target:
119
- typeof event.args === 'object' && event.args !== null
120
- ? Object.entries(event.args as Record<string, unknown>)
121
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
122
- .join(' ')
123
- : undefined,
124
- status: 'running',
125
- timestamp: ts,
126
- }
127
- case 'tool_result':
128
- return {
129
- kind: 'tool',
130
- id,
131
- tool: event.name,
132
- status: 'success',
133
- output:
134
- typeof event.data === 'string' ? event.data : JSON.stringify(event.data, null, 2),
135
- timestamp: ts,
136
- }
137
- case 'error':
138
- return { kind: 'error', id, message: event.message, timestamp: ts }
139
- }
140
- })
141
- return [...userMessages, ...agentItems]
142
- }, [userMessages, events])
95
+ // Interleave local user turns with the reconstructed assistant UIMessages,
96
+ // turn by turn. `ChatMessage` renders each message's parts (text/tool/reasoning).
97
+ const thread = useMemo<UIMessage[]>(() => {
98
+ const out: UIMessage[] = []
99
+ const turns = Math.max(userMessages.length, messages.length)
100
+ for (let i = 0; i < turns; i++) {
101
+ const user = userMessages[i]
102
+ const assistant = messages[i]
103
+ if (user) out.push(user)
104
+ if (assistant) out.push(assistant)
105
+ }
106
+ return out
107
+ }, [userMessages, messages])
143
108
 
144
109
  function handleSubmit(value: string) {
145
110
  const trimmed = value.trim()
146
111
  if (!trimmed) return
147
- const id = `u-${userMessages.length}`
148
- setUserMessages((prev) => [
149
- ...prev,
150
- { kind: 'message', id, role: 'user', content: trimmed, timestamp: new Date().toISOString() },
151
- ])
112
+ const userMessage: UIMessage = {
113
+ id: `u-${String(userMessages.length)}`,
114
+ role: 'user',
115
+ parts: [{ type: 'text', text: trimmed }],
116
+ }
117
+ setUserMessages((prev) => [...prev, userMessage])
152
118
  send({ message: trimmed })
153
119
  setComposerValue('')
154
120
  }
@@ -161,11 +127,12 @@ export default function Page() {
161
127
  return
162
128
  }
163
129
  const action = QUICK_ACTIONS.find((a) => a.id === id)
164
- if (action) handleSubmit(action.label)
130
+ // Quick-action labels are strings; only a string can be sent as a prompt.
131
+ if (action && typeof action.label === 'string') handleSubmit(action.label)
165
132
  }
166
133
 
167
134
  const isStreaming = status === 'streaming'
168
- const isEmpty = items.length === 0 && !isStreaming
135
+ const isEmpty = thread.length === 0 && !isStreaming
169
136
  const hasError = status === 'error'
170
137
 
171
138
  return (
@@ -186,50 +153,18 @@ export default function Page() {
186
153
  eyebrow="Theo Agent"
187
154
  icon={Sparkles}
188
155
  title="What should we build today?"
189
- description="Ask anything. This scaffold ships with a mock LLM at server/routes/chat.ts so you can see the wiring before plugging in a real model."
156
+ description="Ask anything. This scaffold ships an agent at agents/chat.ts — edit it to pick your model or add tools."
190
157
  action={<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />}
191
158
  />
192
159
  ) : (
193
160
  <ChatThread>
194
- {items.map((item) => {
195
- if (item.kind === 'message') {
196
- const message: Message = {
197
- id: item.id,
198
- role: item.role,
199
- content: item.content,
200
- timestamp: item.timestamp,
201
- model: item.role === 'assistant' ? MODEL_NAME : undefined,
202
- }
203
- return (
204
- <ChatMessage
205
- key={item.id}
206
- message={message}
207
- avatar={item.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
208
- />
209
- )
210
- }
211
- if (item.kind === 'tool') {
212
- return (
213
- <ToolCallCard
214
- key={item.id}
215
- tool={item.tool}
216
- icon={Wrench}
217
- target={item.target}
218
- status={item.status}
219
- output={item.output}
220
- timestamp={item.timestamp}
221
- />
222
- )
223
- }
224
- return (
225
- <AgentErrorCard
226
- key={item.id}
227
- kind="model"
228
- title="Agent error"
229
- description={item.message}
230
- />
231
- )
232
- })}
161
+ {thread.map((message) => (
162
+ <ChatMessage
163
+ key={message.id}
164
+ message={message}
165
+ avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
166
+ />
167
+ ))}
233
168
  {isStreaming && <AgentStreaming model={MODEL_NAME} />}
234
169
  </ChatThread>
235
170
  )}
@@ -243,8 +178,8 @@ export default function Page() {
243
178
  <AgentErrorCard
244
179
  kind="network"
245
180
  title="Stream ended with an error"
246
- description="The connection to the agent endpoint was interrupted. Reset to try again."
247
- action={
181
+ detail="The connection to the agent endpoint was interrupted. Reset to try again."
182
+ actions={
248
183
  <Button variant="ghost" size="sm" onClick={() => reset()}>
249
184
  Reset
250
185
  </Button>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: theokit-agents
3
- description: TheoKit agent/LLM integration — two streaming surfaces (decorator vs manual), @Tool, @Toolbox, memory
3
+ description: TheoKit agent/LLM integration — agents/*.ts convention (defineAgent), @Agent decorator (advanced/DI), defineAgentTool, useAgent client hook
4
4
  user-invocable: false
5
5
  paths:
6
6
  - "**/*agent*"
@@ -13,37 +13,38 @@ paths:
13
13
 
14
14
  # TheoKit Agents & Tools
15
15
 
16
- ## Two Streaming Surfaces — pick one per endpoint
16
+ ## Server Surface — agents/*.ts (zero-config convention)
17
17
 
18
- TheoKit ships two ways to create agent endpoints. Use ONE per endpoint, not both.
19
-
20
- | Surface | Server | Events | Client | When to use |
21
- |---------|--------|--------|--------|-------------|
22
- | **Manual** (recommended for most apps) | `defineAgentEndpoint({ handler: async function* })` | `AgentEvent` (Message, ToolCall, Result, Error) | `useAgentStream()` / `consumeAgentStream()` | Full control over the LLM loop — you write the generator |
23
- | **Decorator** (`@theokit/agents`) | `@Agent` class → auto-generated route | `AgentStreamEvent` (TextDelta, ToolCall, Done...) | (same client hooks work) | Declarative — framework manages LLM loop via `@MainLoop` |
24
-
25
- ### Surface 1: Manual (defineAgentEndpoint)
18
+ Create an `agents/<name>.ts` file at the project root. It is automatically served at
19
+ `POST /api/agents/<name>` (dev + build) with no manual route wiring.
26
20
 
27
21
  ```typescript
28
- // server/routes/agents/assistant.ts
29
- import { defineAgentEndpoint } from 'theokit/server/define'
30
- import type { AgentEvent } from 'theokit'
31
-
32
- export const POST = defineAgentEndpoint({
33
- handler: async function* ({ body }): AsyncGenerator<AgentEvent> {
34
- // You control the LLM loop
35
- yield { type: 'message', content: 'Thinking...' }
36
- const result = await callLLM(body.message)
37
- yield { type: 'message', content: result }
38
- },
22
+ // agents/chat.ts
23
+ import { defineAgent } from '@theokit/agents'
24
+ import { z } from 'zod'
25
+
26
+ export default defineAgent({
27
+ input: z.object({ message: z.string() }),
28
+ model: 'openai/gpt-4o-mini',
29
+ system: 'You are a helpful assistant.',
39
30
  })
40
31
  ```
41
32
 
42
- ### Surface 2: Decorator (@Agent)
33
+ The endpoint streams the ai-sdk `UIMessageStream` that `useAgent` (client hook) consumes.
34
+ `@theokit/sdk` runs the agent; conversation turns auto-persist per session — the SDK owns storage.
35
+
36
+ **Provider resolution:** `OPENROUTER_API_KEY` (preferred — routes to many models) OR
37
+ `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`. Set one in `.env`.
38
+
39
+ ## Advanced Surface — @Agent Decorator (DI / class-based)
40
+
41
+ When you need dependency injection or class-based composition, use the `@Agent` class
42
+ decorator from `@theokit/agents`. The class name determines the route:
43
+ `AssistantAgent` → `POST /api/agents/assistant`.
43
44
 
44
45
  ```typescript
45
46
  // server/agents/assistant.agent.ts
46
- import { Agent, MainLoop, Tool, Toolbox } from '@theokit/agents'
47
+ import { Agent, MainLoop } from '@theokit/agents'
47
48
 
48
49
  @Agent({
49
50
  model: 'openai/gpt-4o-mini',
@@ -57,9 +58,33 @@ export class AssistantAgent {
57
58
  }
58
59
  ```
59
60
 
60
- Convention: `AssistantAgent` → `POST /api/agents/assistant`
61
+ ## Tools — defineAgentTool
61
62
 
62
- ## @Tool Decorator
63
+ Declare typed tools with `defineAgentTool` (from `theokit/server`) and pass them to
64
+ `defineAgent`'s `tools` array.
65
+
66
+ ```typescript
67
+ // agents/chat.ts
68
+ import { defineAgent } from '@theokit/agents'
69
+ import { defineAgentTool } from 'theokit/server'
70
+ import { z } from 'zod'
71
+
72
+ const currentTimeTool = defineAgentTool({
73
+ name: 'current_time',
74
+ description: 'Return the current ISO timestamp',
75
+ inputSchema: z.object({}),
76
+ handler: async () => new Date().toISOString(),
77
+ })
78
+
79
+ export default defineAgent({
80
+ input: z.object({ message: z.string() }),
81
+ model: 'openai/gpt-4o-mini',
82
+ system: 'You are a helpful assistant.',
83
+ tools: [currentTimeTool],
84
+ })
85
+ ```
86
+
87
+ ### @Tool Decorator (advanced / class-based)
63
88
 
64
89
  ```typescript
65
90
  import { Toolbox, Tool } from '@theokit/agents'
@@ -78,23 +103,26 @@ export class TaskTools {
78
103
  }
79
104
  ```
80
105
 
81
- ## Client — useAgentStream (React hook)
82
-
83
- Works with BOTH surfaces. Transport: `fetch` POST + `ReadableStream` (SSE).
106
+ ## Client — useAgent (React hook)
84
107
 
85
108
  ```typescript
86
- import { useAgentStream } from 'theokit/client'
109
+ import { useAgent } from 'theokit/client'
87
110
 
88
111
  function ChatUI() {
89
- const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
112
+ const { messages, status, send, reset } = useAgent<{ message: string }>('/api/agents/chat')
90
113
 
91
114
  return (
92
115
  <div>
93
116
  {status === 'streaming' && <p>Thinking...</p>}
94
- {events.map(e => (
95
- <div key={e.id}>
96
- {e.type === 'message' && <p>{e.content}</p>}
97
- {e.type === 'tool_call' && <p>Using tool: {e.name}</p>}
117
+ {messages.map(message => (
118
+ <div key={message.id}>
119
+ {message.parts.map((part, i) => (
120
+ part.type === 'text'
121
+ ? <p key={i}>{part.text}</p>
122
+ : part.type === 'dynamic-tool'
123
+ ? <p key={i}>Using tool: {part.toolName}</p>
124
+ : null
125
+ ))}
98
126
  </div>
99
127
  ))}
100
128
  <button onClick={() => send({ message: 'Hello' })}>Send</button>
@@ -103,15 +131,23 @@ function ChatUI() {
103
131
  }
104
132
  ```
105
133
 
106
- ### Non-React: consumeAgentStream
134
+ `messages` is `UIMessage[]` (ai-sdk). Render `message.parts` — text parts
135
+ (`part.type === 'text'`, `part.text`) and tool parts (`part.type === 'dynamic-tool'`,
136
+ `part.toolName`, `part.state`, `part.output`). Do NOT switch on an `events`/`event.type`
137
+ pattern — the wire is `UIMessageStream`, not SSE events.
138
+
139
+ ### Non-React: consumeUIMessageStream
107
140
 
108
141
  ```typescript
109
- import { consumeAgentStream } from 'theokit/client'
142
+ import { consumeUIMessageStream } from 'theokit/client'
110
143
 
111
- const stream = consumeAgentStream('/api/agents/assistant', { body: { message: 'Hi' } })
112
- for await (const event of stream) {
113
- console.log(event.type, event.content)
114
- }
144
+ const response = await fetch('/api/agents/chat', {
145
+ method: 'POST',
146
+ body: JSON.stringify({ message: 'Hello' }),
147
+ })
148
+ consumeUIMessageStream(response, (message) => {
149
+ console.log(message.parts)
150
+ })
115
151
  ```
116
152
 
117
153
  ## SDK Ecosystem — "you are here" map
@@ -121,7 +157,7 @@ Before writing custom tools, check if they already exist:
121
157
  | Package | What it provides | When to use |
122
158
  |---------|-----------------|-------------|
123
159
  | `@theokit/sdk` | `Agent.create()`, `defineTool()` (primitive), `Run.stream()` | Core agent runtime — always installed |
124
- | `@theokit/sdk-tools` | Ready-made tools: `createReadFileTool`, `createWriteFileTool`, `createSearchTool`, etc. | **Check here FIRST** before writing custom tools for coding agents |
160
+ | `@theokit/sdk-tools` | Ready-made tools: `createReadFileTool`, `createWriteFileTool`, `createSearchTextTool`, `createGlobTool`, `createShellTool`, etc. | **Check here FIRST** before writing custom tools for coding agents |
125
161
  | `@theokit/di-agent` | DI-powered agent with decorator injection | When using dependency injection pattern |
126
162
  | `@theokit/di` | Core DI container (`@Injectable`, `@Inject`) | When `@theokit/di-agent` needs explicit bindings |
127
163
 
@@ -134,14 +170,12 @@ Before writing custom tools, check if they already exist:
134
170
  - `@UseGuards()` works on agents (shared with HTTP pipeline)
135
171
  - `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings)
136
172
  - Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
137
- - Pick ONE surface per endpoint — don't mix defineAgentEndpoint with @Agent for the same route
138
173
  - Check `@theokit/sdk-tools` BEFORE writing custom tools — it may already exist
139
174
 
140
175
  ## Anti-patterns
141
176
 
142
- - NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent or defineAgentEndpoint
177
+ - NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use `defineAgent` or `@Agent`
143
178
  - NEVER reimplement tool calling loop — the SDK handles it
144
179
  - NEVER reimplement file/search/shell tools — use `@theokit/sdk-tools` (readFile, writeFile, search, etc.)
145
- - NEVER store conversations manually — use @Memory (decorator) or SDK persistence
180
+ - NEVER store conversations manually — SDK persistence is automatic (the SDK owns storage)
146
181
  - NEVER infer tool capability from method name — always provide explicit `name` + `description`
147
- - NEVER mix both surfaces for the same endpoint — pick manual OR decorator
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: theokit-frontend
3
- description: TheoKit frontend — file-based routing, layouts, theoFetch typed client, useAgentStream, React patterns
3
+ description: TheoKit frontend — file-based routing, layouts, theoFetch typed client, useAgent, React patterns
4
4
  user-invocable: false
5
5
  paths:
6
6
  - "app/**"
@@ -50,23 +50,24 @@ const created = await client.tasks.POST({ body: { title: 'New' } })
50
50
 
51
51
  ## Agent Streaming
52
52
 
53
- Three client APIs, all from `theokit/client`:
53
+ Two client APIs from `theokit/client`:
54
54
 
55
- ### useAgentStream (React hook — most common)
55
+ ### useAgent (React hook — most common)
56
56
 
57
57
  ```typescript
58
- import { useAgentStream } from 'theokit/client'
58
+ import { useAgent } from 'theokit/client'
59
59
 
60
60
  function ChatUI() {
61
- const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
61
+ const { messages, status, send, reset } = useAgent<{ message: string }>('/api/agents/chat')
62
62
 
63
63
  return (
64
64
  <div>
65
65
  {status === 'streaming' && <p>Thinking...</p>}
66
- {events.map(event => (
67
- <div key={event.id}>
68
- {event.type === 'message' && <p>{event.content}</p>}
69
- {event.type === 'tool_call' && <p>Using tool: {event.name}</p>}
66
+ {messages.map(message => (
67
+ <div key={message.id}>
68
+ {message.parts.map((part, i) =>
69
+ part.type === 'text' ? <p key={i}>{part.text}</p> : null
70
+ )}
70
71
  </div>
71
72
  ))}
72
73
  <button onClick={() => send({ message: 'Hello' })}>Send</button>
@@ -75,26 +76,23 @@ function ChatUI() {
75
76
  }
76
77
  ```
77
78
 
78
- ### consumeAgentStream (non-React, async iterable)
79
+ `messages` is `UIMessage[]` (ai-sdk). Render `message.parts`: text parts
80
+ (`part.type === 'text'`, `part.text`) and tool parts (`part.type === 'dynamic-tool'`,
81
+ `part.toolName`, `part.state`, `part.output`). Do NOT switch on an `events`/`event.type`
82
+ pattern — the wire is `UIMessageStream`, not SSE events.
79
83
 
80
- ```typescript
81
- import { consumeAgentStream } from 'theokit/client'
82
-
83
- const stream = consumeAgentStream('/api/agents/assistant', {
84
- body: { message: 'Hello' },
85
- })
86
- for await (const event of stream) {
87
- console.log(event.type, event.content)
88
- }
89
- ```
90
-
91
- ### parseSSEChunk (low-level SSE parser)
84
+ ### consumeUIMessageStream (non-React)
92
85
 
93
86
  ```typescript
94
- import { parseSSEChunk } from 'theokit/client'
87
+ import { consumeUIMessageStream } from 'theokit/client'
95
88
 
96
- // Parse a single SSE line into an AgentEvent (or null)
97
- const event = parseSSEChunk('data: {"type":"message","content":"Hello"}')
89
+ const response = await fetch('/api/agents/chat', {
90
+ method: 'POST',
91
+ body: JSON.stringify({ message: 'Hello' }),
92
+ })
93
+ consumeUIMessageStream(response, (message) => {
94
+ console.log(message.parts)
95
+ })
98
96
  ```
99
97
 
100
98
  ## Path Aliases
@@ -111,4 +109,4 @@ Configured in `tsconfig.json` — works in both server and app code.
111
109
  - NEVER use raw `fetch('/api/...')` — use `theoFetch` for type safety
112
110
  - NEVER create pages outside `app/` — they won't be discovered by the router
113
111
  - NEVER import server code directly in `app/` — use theoFetch or server actions
114
- - NEVER use `useEffect` + `fetch` for data loading — use theoFetch or useAgentStream
112
+ - NEVER use `useEffect` + `fetch` for data loading — use theoFetch or `useAgent`