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.
- package/LICENSE +201 -0
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/package.json +7 -8
- package/templates/default/CLAUDE.md +2 -2
- package/templates/default/README.md.tmpl +7 -6
- package/templates/default/agents/chat.ts +22 -0
- package/templates/default/app/layout.tsx +3 -11
- package/templates/default/app/page.test.tsx +36 -0
- package/templates/default/app/page.tsx +51 -116
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +79 -45
- package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +24 -26
- package/templates/default/dot-claude/skills/theokit-ui/SKILL.md +32 -28
- package/templates/default/package.json.tmpl +13 -4
- package/templates/default/tsconfig.json +4 -2
- package/templates/default/server/routes/chat.ts +0 -69
|
@@ -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
|
-
|
|
23
|
-
} from '@theokit/ui'
|
|
23
|
+
} from '@usetheo/ui'
|
|
24
24
|
import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
|
|
25
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
//
|
|
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 = '
|
|
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<
|
|
79
|
+
const [userMessages, setUserMessages] = useState<UIMessage[]>([])
|
|
91
80
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
92
|
-
const {
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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 —
|
|
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
|
-
##
|
|
16
|
+
## Server Surface — agents/*.ts (zero-config convention)
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
//
|
|
29
|
-
import {
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
61
|
+
## Tools — defineAgentTool
|
|
61
62
|
|
|
62
|
-
|
|
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 —
|
|
82
|
-
|
|
83
|
-
Works with BOTH surfaces. Transport: `fetch` POST + `ReadableStream` (SSE).
|
|
106
|
+
## Client — useAgent (React hook)
|
|
84
107
|
|
|
85
108
|
```typescript
|
|
86
|
-
import {
|
|
109
|
+
import { useAgent } from 'theokit/client'
|
|
87
110
|
|
|
88
111
|
function ChatUI() {
|
|
89
|
-
const {
|
|
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
|
-
{
|
|
95
|
-
<div key={
|
|
96
|
-
{
|
|
97
|
-
|
|
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
|
-
|
|
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 {
|
|
142
|
+
import { consumeUIMessageStream } from 'theokit/client'
|
|
110
143
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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`, `
|
|
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
|
|
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 —
|
|
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,
|
|
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
|
-
|
|
53
|
+
Two client APIs from `theokit/client`:
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### useAgent (React hook — most common)
|
|
56
56
|
|
|
57
57
|
```typescript
|
|
58
|
-
import {
|
|
58
|
+
import { useAgent } from 'theokit/client'
|
|
59
59
|
|
|
60
60
|
function ChatUI() {
|
|
61
|
-
const {
|
|
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
|
-
{
|
|
67
|
-
<div key={
|
|
68
|
-
{
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
87
|
+
import { consumeUIMessageStream } from 'theokit/client'
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
112
|
+
- NEVER use `useEffect` + `fetch` for data loading — use theoFetch or `useAgent`
|