create-theokit 1.0.13 → 1.0.15
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/dist/cli.js +2 -12
- package/dist/cli.js.map +1 -1
- package/package.json +4 -1
- package/templates/default/README.md.tmpl +37 -50
- package/templates/default/app/layout.tsx +79 -3
- package/templates/default/app/page.tsx +281 -61
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +15 -0
- package/templates/default/eslint.config.mjs +1 -5
- package/templates/default/package.json.tmpl +10 -9
- package/templates/default/server/routes/chat.ts +69 -0
- package/templates/default/theo.config.ts +1 -3
- package/templates/default/tsconfig.json +7 -14
- package/templates/default/types/jobs.d.ts +25 -0
- package/templates/services/agent-node/Dockerfile.tmpl +20 -0
- package/templates/services/agent-node/README.md +38 -0
- package/templates/services/agent-node/package.json.tmpl +18 -0
- package/templates/services/agent-node/src/index.ts +58 -0
- package/templates/services/agent-node/tsconfig.json +13 -0
- package/templates/default/AGENTS.md +0 -91
- package/templates/default/app/globals.css +0 -377
- package/templates/default/app.ts +0 -15
- package/templates/default/drizzle.config.ts +0 -10
- package/templates/default/server/db/index.ts +0 -10
- package/templates/default/server/db/schema.ts +0 -13
- package/templates/default/server/index.ts +0 -2
- package/templates/default/tests/tasks.test.ts +0 -18
|
@@ -1,68 +1,288 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ChatThread,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ChatComposer,
|
|
8
|
+
ToolCallCard,
|
|
9
|
+
AgentStreaming,
|
|
10
|
+
AgentErrorCard,
|
|
11
|
+
EmptyState,
|
|
12
|
+
QuickActionChips,
|
|
13
|
+
ContextWindowBar,
|
|
14
|
+
CommandPalette,
|
|
15
|
+
Avatar,
|
|
16
|
+
Tooltip,
|
|
17
|
+
Button,
|
|
18
|
+
ScrollArea,
|
|
19
|
+
type Message,
|
|
20
|
+
type QuickAction,
|
|
21
|
+
type CommandItem,
|
|
22
|
+
type ToolCallStatus,
|
|
23
|
+
} from '@theokit/ui'
|
|
24
|
+
import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
|
|
25
|
+
import { useAgentStream } from 'theokit/client'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default scaffold — an Agent Surface, composed entirely from TheoUI.
|
|
29
|
+
*
|
|
30
|
+
* ChatThread / ChatMessage → conversation
|
|
31
|
+
* ToolCallCard → expandable tool invocations
|
|
32
|
+
* AgentStreaming → streaming indicator
|
|
33
|
+
* AgentErrorCard → error display
|
|
34
|
+
* ChatComposer → bottom input bar
|
|
35
|
+
* EmptyState → first-load screen
|
|
36
|
+
* ContextWindowBar → context usage at top
|
|
37
|
+
* CommandPalette → ⌘K quick actions
|
|
38
|
+
* Avatar → assistant face in messages
|
|
39
|
+
* Tooltip → hints on icons
|
|
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).
|
|
44
|
+
*/
|
|
45
|
+
|
|
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
|
+
const QUICK_ACTIONS: QuickAction[] = [
|
|
60
|
+
{ id: 'summarize', label: 'Summarize this page', icon: Sparkles },
|
|
61
|
+
{ id: 'tools', label: 'Show available tools', icon: Wrench },
|
|
62
|
+
{ id: 'reset', label: 'Start a new conversation', icon: RotateCcw },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const COMMAND_ITEMS: CommandItem[] = QUICK_ACTIONS.map((a) => ({
|
|
66
|
+
id: a.id,
|
|
67
|
+
label: a.label,
|
|
68
|
+
icon: a.icon,
|
|
69
|
+
group: 'Quick actions',
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
// Mock context-window usage — replace with real model state.
|
|
73
|
+
const CONTEXT_USED = 4_200
|
|
74
|
+
const CONTEXT_TOTAL = 200_000
|
|
75
|
+
const MODEL_NAME = 'mock-llm'
|
|
76
|
+
|
|
77
|
+
const ASSISTANT_AVATAR = (
|
|
78
|
+
<Avatar size="sm" tone="primary">
|
|
79
|
+
<Avatar.Fallback>TH</Avatar.Fallback>
|
|
80
|
+
</Avatar>
|
|
81
|
+
)
|
|
82
|
+
const USER_AVATAR = (
|
|
83
|
+
<Avatar size="sm" tone="muted">
|
|
84
|
+
<Avatar.Fallback>YOU</Avatar.Fallback>
|
|
85
|
+
</Avatar>
|
|
86
|
+
)
|
|
87
|
+
|
|
1
88
|
export default function Page() {
|
|
89
|
+
const [composerValue, setComposerValue] = useState('')
|
|
90
|
+
const [userMessages, setUserMessages] = useState<ConversationItem[]>([])
|
|
91
|
+
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
92
|
+
const { events, send, status, reset } = useAgentStream<{ message: string }>('/api/chat')
|
|
93
|
+
|
|
94
|
+
// ⌘K / Ctrl+K opens the CommandPalette.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
function onKey(e: KeyboardEvent) {
|
|
97
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
setPaletteOpen((v) => !v)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
window.addEventListener('keydown', onKey)
|
|
103
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
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])
|
|
143
|
+
|
|
144
|
+
function handleSubmit(value: string) {
|
|
145
|
+
const trimmed = value.trim()
|
|
146
|
+
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
|
+
])
|
|
152
|
+
send({ message: trimmed })
|
|
153
|
+
setComposerValue('')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleQuickAction(id: string) {
|
|
157
|
+
setPaletteOpen(false)
|
|
158
|
+
if (id === 'reset') {
|
|
159
|
+
setUserMessages([])
|
|
160
|
+
reset()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
const action = QUICK_ACTIONS.find((a) => a.id === id)
|
|
164
|
+
if (action) handleSubmit(action.label)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isStreaming = status === 'streaming'
|
|
168
|
+
const isEmpty = items.length === 0 && !isStreaming
|
|
169
|
+
const hasError = status === 'error'
|
|
170
|
+
|
|
2
171
|
return (
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
172
|
+
<>
|
|
173
|
+
<ContextWindowBar
|
|
174
|
+
used={CONTEXT_USED}
|
|
175
|
+
total={CONTEXT_TOTAL}
|
|
176
|
+
trailing={MODEL_NAME}
|
|
177
|
+
label="Context window"
|
|
178
|
+
compact
|
|
179
|
+
className="border-border/60 border-b px-6 py-2"
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<ScrollArea className="flex-1">
|
|
183
|
+
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
|
|
184
|
+
{isEmpty ? (
|
|
185
|
+
<EmptyState
|
|
186
|
+
eyebrow="Theo Agent"
|
|
187
|
+
icon={Sparkles}
|
|
188
|
+
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."
|
|
190
|
+
action={<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />}
|
|
191
|
+
/>
|
|
192
|
+
) : (
|
|
193
|
+
<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
|
+
})}
|
|
233
|
+
{isStreaming && <AgentStreaming model={MODEL_NAME} />}
|
|
234
|
+
</ChatThread>
|
|
235
|
+
)}
|
|
53
236
|
</div>
|
|
237
|
+
</ScrollArea>
|
|
54
238
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
239
|
+
<div className="border-border/60 border-t bg-background/50 backdrop-blur">
|
|
240
|
+
<div className="mx-auto w-full max-w-3xl px-6 py-4">
|
|
241
|
+
{hasError && (
|
|
242
|
+
<div className="mb-3">
|
|
243
|
+
<AgentErrorCard
|
|
244
|
+
kind="network"
|
|
245
|
+
title="Stream ended with an error"
|
|
246
|
+
description="The connection to the agent endpoint was interrupted. Reset to try again."
|
|
247
|
+
action={
|
|
248
|
+
<Button variant="ghost" size="sm" onClick={() => reset()}>
|
|
249
|
+
Reset
|
|
250
|
+
</Button>
|
|
251
|
+
}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
<ChatComposer
|
|
256
|
+
value={composerValue}
|
|
257
|
+
onValueChange={setComposerValue}
|
|
258
|
+
onSubmit={handleSubmit}
|
|
259
|
+
running={isStreaming}
|
|
260
|
+
placeholder="Ask the agent…"
|
|
261
|
+
leadingActions={
|
|
262
|
+
<Tooltip label="Open command palette (⌘K)" side="top">
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
variant="ghost"
|
|
266
|
+
size="icon"
|
|
267
|
+
onClick={() => setPaletteOpen(true)}
|
|
268
|
+
aria-label="Open command palette"
|
|
269
|
+
>
|
|
270
|
+
<Command className="size-4" />
|
|
271
|
+
</Button>
|
|
272
|
+
</Tooltip>
|
|
273
|
+
}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
65
276
|
</div>
|
|
66
|
-
|
|
277
|
+
|
|
278
|
+
<CommandPalette
|
|
279
|
+
open={paletteOpen}
|
|
280
|
+
onOpenChange={setPaletteOpen}
|
|
281
|
+
items={COMMAND_ITEMS}
|
|
282
|
+
onSelect={handleQuickAction}
|
|
283
|
+
placeholder="Run a command…"
|
|
284
|
+
emptyMessage="No matching commands."
|
|
285
|
+
/>
|
|
286
|
+
</>
|
|
67
287
|
)
|
|
68
288
|
}
|
|
@@ -114,6 +114,19 @@ for await (const event of stream) {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## SDK Ecosystem — "you are here" map
|
|
118
|
+
|
|
119
|
+
Before writing custom tools, check if they already exist:
|
|
120
|
+
|
|
121
|
+
| Package | What it provides | When to use |
|
|
122
|
+
|---------|-----------------|-------------|
|
|
123
|
+
| `@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 |
|
|
125
|
+
| `@theokit/di-agent` | DI-powered agent with decorator injection | When using dependency injection pattern |
|
|
126
|
+
| `@theokit/di` | Core DI container (`@Injectable`, `@Inject`) | When `@theokit/di-agent` needs explicit bindings |
|
|
127
|
+
|
|
128
|
+
**`defineTool()` in `@theokit/sdk` is the primitive API.** For coding agents, `@theokit/sdk-tools` has batteries-included tools that wrap `defineTool()` with file system access, search, shell execution, etc. Don't reimplement what `sdk-tools` already provides.
|
|
129
|
+
|
|
117
130
|
## Rules
|
|
118
131
|
|
|
119
132
|
- Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
|
|
@@ -122,11 +135,13 @@ for await (const event of stream) {
|
|
|
122
135
|
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings)
|
|
123
136
|
- Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
|
|
124
137
|
- Pick ONE surface per endpoint — don't mix defineAgentEndpoint with @Agent for the same route
|
|
138
|
+
- Check `@theokit/sdk-tools` BEFORE writing custom tools — it may already exist
|
|
125
139
|
|
|
126
140
|
## Anti-patterns
|
|
127
141
|
|
|
128
142
|
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent or defineAgentEndpoint
|
|
129
143
|
- NEVER reimplement tool calling loop — the SDK handles it
|
|
144
|
+
- NEVER reimplement file/search/shell tools — use `@theokit/sdk-tools` (readFile, writeFile, search, etc.)
|
|
130
145
|
- NEVER store conversations manually — use @Memory (decorator) or SDK persistence
|
|
131
146
|
- NEVER infer tool capability from method name — always provide explicit `name` + `description`
|
|
132
147
|
- NEVER mix both surfaces for the same endpoint — pick manual OR decorator
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import tseslint from 'typescript-eslint'
|
|
2
2
|
import prettierConfig from 'eslint-config-prettier'
|
|
3
|
-
import drizzle from 'eslint-plugin-drizzle'
|
|
4
3
|
|
|
5
4
|
export default tseslint.config(
|
|
6
|
-
{ ignores: ['dist/', 'node_modules/'
|
|
5
|
+
{ ignores: ['dist/', 'node_modules/'] },
|
|
7
6
|
...tseslint.configs.recommended,
|
|
8
7
|
prettierConfig,
|
|
9
8
|
{
|
|
10
|
-
plugins: { drizzle },
|
|
11
9
|
rules: {
|
|
12
10
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
13
11
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
14
|
-
'drizzle/enforce-delete-with-where': 'error',
|
|
15
|
-
'drizzle/enforce-update-with-where': 'error',
|
|
16
12
|
},
|
|
17
13
|
},
|
|
18
14
|
)
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
"build": "theokit build",
|
|
9
9
|
"start": "theokit start",
|
|
10
10
|
"test": "vitest run",
|
|
11
|
-
"db:migrate": "theokit db migrate",
|
|
12
|
-
"db:generate": "theokit db generate",
|
|
13
11
|
"lint": "eslint .",
|
|
14
12
|
"lint:fix": "eslint . --fix",
|
|
15
13
|
"format": "prettier --write .",
|
|
@@ -17,25 +15,28 @@
|
|
|
17
15
|
"typecheck": "tsc --noEmit"
|
|
18
16
|
},
|
|
19
17
|
"dependencies": {
|
|
20
|
-
"theokit": "^0.
|
|
18
|
+
"theokit": "^0.6.0",
|
|
19
|
+
"@theokit/sdk": "^1.1.0",
|
|
20
|
+
"@theokit/ui": "^0.14.0",
|
|
21
|
+
"lucide-react": "^0.469.0",
|
|
21
22
|
"react": "^19.0.0",
|
|
22
23
|
"react-dom": "^19.0.0",
|
|
23
24
|
"react-router": "^7.0.0",
|
|
24
|
-
"zod": "^4.0.0"
|
|
25
|
-
"drizzle-orm": "^0.44.0",
|
|
26
|
-
"better-sqlite3": "^12.0.0"
|
|
25
|
+
"zod": "^4.0.0"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
|
-
"@types/better-sqlite3": "^7.6.0",
|
|
30
28
|
"@types/react": "^19.0.0",
|
|
31
29
|
"@types/react-dom": "^19.0.0",
|
|
32
|
-
"
|
|
30
|
+
"tailwindcss": "^4.0.0",
|
|
31
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
33
32
|
"eslint": "^9.0.0",
|
|
34
33
|
"eslint-config-prettier": "^10.0.0",
|
|
35
|
-
"eslint-plugin-drizzle": "^0.2.0",
|
|
36
34
|
"prettier": "^3.0.0",
|
|
37
35
|
"typescript": "^5.5.0",
|
|
38
36
|
"typescript-eslint": "^8.0.0",
|
|
39
37
|
"vitest": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"pnpm": {
|
|
40
|
+
"onlyBuiltDependencies": ["esbuild", "better-sqlite3", "workerd"]
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
defineAgentEndpoint,
|
|
4
|
+
defineAgentTool,
|
|
5
|
+
streamAgentRun,
|
|
6
|
+
createConversationHistory,
|
|
7
|
+
type AgentEvent,
|
|
8
|
+
} from 'theokit/server'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Chat agent endpoint — persistent conversation via createConversationHistory.
|
|
12
|
+
*
|
|
13
|
+
* Each browser tab gets a stable conversation id cookie on first visit;
|
|
14
|
+
* subsequent requests resume the same agent. Conversation turns auto-persist
|
|
15
|
+
* in `<cwd>/.theokit/agents/<conversationId>/messages.jsonl` (SDK owns
|
|
16
|
+
* storage). Tools: current_time example. Memory facts: opt-in via
|
|
17
|
+
* options.memory (off by default).
|
|
18
|
+
*
|
|
19
|
+
* Provider: OPENROUTER_API_KEY (preferred — gateway to many models) OR
|
|
20
|
+
* ANTHROPIC_API_KEY (direct Anthropic).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const currentTime = defineAgentTool({
|
|
24
|
+
name: 'current_time',
|
|
25
|
+
description: 'Get the current ISO timestamp on the server.',
|
|
26
|
+
inputSchema: z.object({}),
|
|
27
|
+
handler: () => new Date().toISOString(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const POST = defineAgentEndpoint({
|
|
31
|
+
async *handler({ body, request, cookieHeaders, signal }): AsyncGenerator<AgentEvent> {
|
|
32
|
+
const safeBody =
|
|
33
|
+
body !== null && typeof body === 'object' && !Array.isArray(body)
|
|
34
|
+
? (body as { message?: string })
|
|
35
|
+
: {}
|
|
36
|
+
const { message = '' } = safeBody
|
|
37
|
+
// Provider resolution centralizada (Strategy pattern) — theokit/server resolve
|
|
38
|
+
// apiKey + baseUrl + provider automático via OPENROUTER_API_KEY / OPENAI_API_KEY /
|
|
39
|
+
// ANTHROPIC_API_KEY presente no env. Wire protocol: OpenAI Chat Completions
|
|
40
|
+
// (universal — todos os providers implementam essa API). Consumer NÃO tem
|
|
41
|
+
// conditionals sobre provider — é responsabilidade do framework.
|
|
42
|
+
// Wrap full agent lifecycle in try/catch — provider errors (invalid KEY,
|
|
43
|
+
// 401, rate-limit, model-not-found, 5xx) MUST surface as AgentEvent
|
|
44
|
+
// 'error' so the client renders an actionable message instead of a
|
|
45
|
+
// silent SSE closure. Dogfood chaos Phase 12 validates this contract.
|
|
46
|
+
try {
|
|
47
|
+
const { agent } = await createConversationHistory({
|
|
48
|
+
request,
|
|
49
|
+
response: { headers: cookieHeaders },
|
|
50
|
+
options: {
|
|
51
|
+
// Model id is prefixed with the provider namespace. When using
|
|
52
|
+
// OPENROUTER_API_KEY (default), prefixes route to the correct
|
|
53
|
+
// upstream — `openai/`, `anthropic/`, `google/`, `meta-llama/`,
|
|
54
|
+
// `mistralai/`, `groq/`, etc. See https://openrouter.ai/models.
|
|
55
|
+
// Without the prefix the SDK falls back to a stub response.
|
|
56
|
+
model: { id: 'openai/gpt-4o-mini' },
|
|
57
|
+
tools: [currentTime],
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
const run = await agent.send(message, { signal })
|
|
61
|
+
yield* streamAgentRun(run)
|
|
62
|
+
// Intentionally NO agent.dispose() — the agent stays registered so the
|
|
63
|
+
// next request from the same conversation resumes it (continuity).
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
66
|
+
yield { type: 'error', message: `Agent error: ${msg}` }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
})
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "
|
|
6
|
-
"experimentalDecorators": true,
|
|
7
|
-
"emitDecoratorMetadata": true,
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
8
6
|
"strict": true,
|
|
9
|
-
"
|
|
7
|
+
"noEmit": true,
|
|
10
8
|
"esModuleInterop": true,
|
|
11
9
|
"skipLibCheck": true,
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"@/*": ["./*"],
|
|
16
|
-
"@/server/*": ["./server/*"],
|
|
17
|
-
"@/app/*": ["./app/*"]
|
|
18
|
-
}
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
19
13
|
},
|
|
20
|
-
"include": ["
|
|
21
|
-
"exclude": ["node_modules", "dist"]
|
|
14
|
+
"include": ["app/**/*.ts", "app/**/*.tsx", "server/**/*.ts"]
|
|
22
15
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JobRegistry augmentation — REQUIRED for typed `ctx.queue.enqueue` calls.
|
|
3
|
+
*
|
|
4
|
+
* Without this augmentation, `ctx.queue.enqueue('foo', ...)` errors with:
|
|
5
|
+
* "Type 'foo' is not assignable to type 'never'"
|
|
6
|
+
*
|
|
7
|
+
* This is the canonical TheoKit jobs onboarding bug (EC-110). To add a
|
|
8
|
+
* job:
|
|
9
|
+
*
|
|
10
|
+
* 1. Create `server/jobs/<name>.ts` exporting `defineJob('<name>', ...)`
|
|
11
|
+
* 2. Add `'<name>': { ...inputShape }` below
|
|
12
|
+
* 3. Use `ctx.queue.enqueue('<name>', { ...input })` from any route handler
|
|
13
|
+
*
|
|
14
|
+
* See: docs/concepts/jobs.md
|
|
15
|
+
*/
|
|
16
|
+
declare module 'theokit/server' {
|
|
17
|
+
interface JobRegistry {
|
|
18
|
+
// Add your jobs here. Examples (uncomment and customize):
|
|
19
|
+
//
|
|
20
|
+
// 'process-document': { documentId: string }
|
|
21
|
+
// 'send-email': { to: string; subject: string; body: string }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM node:22-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install pnpm (or use npm if pnpm not available in image)
|
|
6
|
+
RUN corepack enable && corepack prepare pnpm@latest --activate || true
|
|
7
|
+
|
|
8
|
+
# Copy manifest and install
|
|
9
|
+
COPY package.json ./
|
|
10
|
+
RUN pnpm install --frozen-lockfile || npm install
|
|
11
|
+
|
|
12
|
+
# Copy source
|
|
13
|
+
COPY . .
|
|
14
|
+
|
|
15
|
+
EXPOSE 8002
|
|
16
|
+
|
|
17
|
+
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
|
|
18
|
+
CMD wget --spider -q http://localhost:8002/health || exit 1
|
|
19
|
+
|
|
20
|
+
CMD ["pnpm", "start"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Agent service (Node / Hono)
|
|
2
|
+
|
|
3
|
+
This is a TheoKit polyglot sidecar generated by `create-theokit --backend node`.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **Node.js 22+** (matches TheoKit's floor)
|
|
8
|
+
- **pnpm** or **npm** (TheoKit's CLI prefers pnpm)
|
|
9
|
+
|
|
10
|
+
## Run it
|
|
11
|
+
|
|
12
|
+
The TheoKit app boots this service automatically via `pnpm dev` (per `theo.config.ts > services`).
|
|
13
|
+
|
|
14
|
+
To run standalone:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd services/agent-node
|
|
18
|
+
pnpm install
|
|
19
|
+
pnpm dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Endpoints
|
|
23
|
+
|
|
24
|
+
- `GET /health` — healthcheck (returns `{"status":"ok"}`)
|
|
25
|
+
- `POST /echo` — example: `{ "message": "..." }` → `{ "echo": "..." }`
|
|
26
|
+
|
|
27
|
+
## What is wired
|
|
28
|
+
|
|
29
|
+
- Native fetch handler via Hono (works on TheoCloud and local)
|
|
30
|
+
- JSON-line stdout logs
|
|
31
|
+
- W3C `traceparent` propagation from incoming headers
|
|
32
|
+
- TheoKit injects `THEOKIT_SERVICE_NAME` + `THEOKIT_SERVICE_PORT` env vars
|
|
33
|
+
|
|
34
|
+
## Add a tool the TheoKit agent can call
|
|
35
|
+
|
|
36
|
+
Add another `app.post('/tool/<name>', ...)` handler. From the TheoKit TS side,
|
|
37
|
+
`services.worker.<name>({...})` is auto-typed when an OpenAPI URL is reachable
|
|
38
|
+
(Hey API integration — Phase 5).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}-agent-node",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "tsx watch src/index.ts",
|
|
7
|
+
"start": "tsx src/index.ts",
|
|
8
|
+
"build": "echo 'no build step; tsx runs TS directly'"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"hono": "^4.6.0",
|
|
12
|
+
"@hono/node-server": "^1.13.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"tsx": "^4.19.0",
|
|
16
|
+
"typescript": "^5.7.0"
|
|
17
|
+
}
|
|
18
|
+
}
|