create-theokit 1.0.10 → 1.0.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: theokit-agents
|
|
3
|
-
description: TheoKit agent/LLM integration —
|
|
3
|
+
description: TheoKit agent/LLM integration — two streaming surfaces (decorator vs manual), @Tool, @Toolbox, memory
|
|
4
4
|
user-invocable: false
|
|
5
5
|
paths:
|
|
6
6
|
- "**/*agent*"
|
|
@@ -13,31 +13,51 @@ paths:
|
|
|
13
13
|
|
|
14
14
|
# TheoKit Agents & Tools
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Two Streaming Surfaces — pick one per endpoint
|
|
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)
|
|
17
26
|
|
|
18
27
|
```typescript
|
|
19
|
-
|
|
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
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Surface 2: Decorator (@Agent)
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// server/agents/assistant.agent.ts
|
|
46
|
+
import { Agent, MainLoop, Tool, Toolbox } from '@theokit/agents'
|
|
20
47
|
|
|
21
48
|
@Agent({
|
|
22
|
-
model: 'openai/gpt-4o-mini',
|
|
23
|
-
systemPrompt: 'You are a helpful
|
|
49
|
+
model: 'openai/gpt-4o-mini',
|
|
50
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
24
51
|
})
|
|
25
|
-
@Memory({ provider: 'built-in', scope: 'per-user' }) // Optional
|
|
26
|
-
@Budget({ maxCostUsd: 1.00, window: 'daily' }) // Optional
|
|
27
52
|
export class AssistantAgent {
|
|
28
53
|
@MainLoop({ strategy: 'react', maxIterations: 5 })
|
|
29
54
|
async run() {
|
|
30
|
-
// Framework handles the LLM loop
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Hook('before:llm-call')
|
|
34
|
-
async onBeforeLLM(ctx) {
|
|
35
|
-
// Intercept before each LLM call
|
|
55
|
+
// Framework handles the LLM loop via @theokit/sdk
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
```
|
|
39
59
|
|
|
40
|
-
Convention: `AssistantAgent`
|
|
60
|
+
Convention: `AssistantAgent` → `POST /api/agents/assistant`
|
|
41
61
|
|
|
42
62
|
## @Tool Decorator
|
|
43
63
|
|
|
@@ -50,56 +70,63 @@ export class TaskTools {
|
|
|
50
70
|
@Tool({
|
|
51
71
|
name: 'list_tasks',
|
|
52
72
|
description: 'List all tasks, optionally filtered by status',
|
|
53
|
-
input: z.object({
|
|
54
|
-
done: z.boolean().optional(),
|
|
55
|
-
}),
|
|
73
|
+
input: z.object({ done: z.boolean().optional() }),
|
|
56
74
|
})
|
|
57
75
|
async listTasks({ done }: { done?: boolean }) {
|
|
58
|
-
|
|
59
|
-
return done !== undefined ? all.filter(t => t.done === done) : all
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@Tool({
|
|
63
|
-
name: 'create_task',
|
|
64
|
-
description: 'Create a new task with a title',
|
|
65
|
-
input: z.object({
|
|
66
|
-
title: z.string().min(1),
|
|
67
|
-
}),
|
|
68
|
-
})
|
|
69
|
-
async createTask({ title }: { title: string }) {
|
|
70
|
-
return db.insert(tasks).values({ title }).returning().get()
|
|
76
|
+
return db.select().from(tasks).all()
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
```
|
|
74
80
|
|
|
75
|
-
##
|
|
81
|
+
## Client — useAgentStream (React hook)
|
|
82
|
+
|
|
83
|
+
Works with BOTH surfaces. Transport: `fetch` POST + `ReadableStream` (SSE).
|
|
76
84
|
|
|
77
85
|
```typescript
|
|
78
86
|
import { useAgentStream } from 'theokit/client'
|
|
79
87
|
|
|
80
88
|
function ChatUI() {
|
|
81
|
-
const { status, events, send } = useAgentStream('/api/agents/assistant')
|
|
89
|
+
const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
|
|
82
90
|
|
|
83
91
|
return (
|
|
84
92
|
<div>
|
|
85
|
-
{
|
|
93
|
+
{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>}
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
86
100
|
<button onClick={() => send({ message: 'Hello' })}>Send</button>
|
|
87
101
|
</div>
|
|
88
102
|
)
|
|
89
103
|
}
|
|
90
104
|
```
|
|
91
105
|
|
|
106
|
+
### Non-React: consumeAgentStream
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { consumeAgentStream } from 'theokit/client'
|
|
110
|
+
|
|
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
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
92
117
|
## Rules
|
|
93
118
|
|
|
94
119
|
- Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
|
|
95
120
|
- Tool `input` uses Zod schema — same pattern as defineRoute
|
|
96
121
|
- `@UseGuards()` works on agents (shared with HTTP pipeline)
|
|
97
|
-
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings
|
|
122
|
+
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings)
|
|
98
123
|
- Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
|
|
124
|
+
- Pick ONE surface per endpoint — don't mix defineAgentEndpoint with @Agent for the same route
|
|
99
125
|
|
|
100
126
|
## Anti-patterns
|
|
101
127
|
|
|
102
|
-
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent
|
|
128
|
+
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent or defineAgentEndpoint
|
|
103
129
|
- NEVER reimplement tool calling loop — the SDK handles it
|
|
104
|
-
- NEVER store conversations manually — use @Memory
|
|
130
|
+
- NEVER store conversations manually — use @Memory (decorator) or SDK persistence
|
|
105
131
|
- NEVER infer tool capability from method name — always provide explicit `name` + `description`
|
|
132
|
+
- NEVER mix both surfaces for the same endpoint — pick manual OR decorator
|
|
@@ -43,18 +43,21 @@ import { createAppClient } from 'theokit/client'
|
|
|
43
43
|
|
|
44
44
|
const client = createAppClient()
|
|
45
45
|
|
|
46
|
-
// Proxy pattern — method names match route structure
|
|
47
46
|
const tasks = await client.tasks.GET()
|
|
48
47
|
const task = await client.tasks[':id'].GET({ params: { id: 1 } })
|
|
49
48
|
const created = await client.tasks.POST({ body: { title: 'New' } })
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
## Agent Streaming
|
|
51
|
+
## Agent Streaming
|
|
52
|
+
|
|
53
|
+
Three client APIs, all from `theokit/client`:
|
|
54
|
+
|
|
55
|
+
### useAgentStream (React hook — most common)
|
|
53
56
|
|
|
54
57
|
```typescript
|
|
55
58
|
import { useAgentStream } from 'theokit/client'
|
|
56
59
|
|
|
57
|
-
function
|
|
60
|
+
function ChatUI() {
|
|
58
61
|
const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
|
|
59
62
|
|
|
60
63
|
return (
|
|
@@ -66,12 +69,34 @@ function ChatComponent() {
|
|
|
66
69
|
{event.type === 'tool_call' && <p>Using tool: {event.name}</p>}
|
|
67
70
|
</div>
|
|
68
71
|
))}
|
|
69
|
-
<
|
|
72
|
+
<button onClick={() => send({ message: 'Hello' })}>Send</button>
|
|
70
73
|
</div>
|
|
71
74
|
)
|
|
72
75
|
}
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
### consumeAgentStream (non-React, async iterable)
|
|
79
|
+
|
|
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)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { parseSSEChunk } from 'theokit/client'
|
|
95
|
+
|
|
96
|
+
// Parse a single SSE line into an AgentEvent (or null)
|
|
97
|
+
const event = parseSSEChunk('data: {"type":"message","content":"Hello"}')
|
|
98
|
+
```
|
|
99
|
+
|
|
75
100
|
## Path Aliases
|
|
76
101
|
|
|
77
102
|
```typescript
|
|
@@ -83,7 +108,7 @@ Configured in `tsconfig.json` — works in both server and app code.
|
|
|
83
108
|
|
|
84
109
|
## Anti-patterns
|
|
85
110
|
|
|
86
|
-
- NEVER use `fetch('/api/...')`
|
|
111
|
+
- NEVER use raw `fetch('/api/...')` — use `theoFetch` for type safety
|
|
87
112
|
- NEVER create pages outside `app/` — they won't be discovered by the router
|
|
88
113
|
- NEVER import server code directly in `app/` — use theoFetch or server actions
|
|
89
114
|
- NEVER use `useEffect` + `fetch` for data loading — use theoFetch or useAgentStream
|