create-theokit 1.0.14 → 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/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 -106
- 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
|
}
|
|
@@ -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
|
+
}
|