@ghuri/agents 0.0.0-stage → 0.1.0-alpha.1
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 +21 -0
- package/README.md +458 -2
- package/dist/index.d.ts +871 -0
- package/dist/index.js +1995 -0
- package/dist/index.js.map +1 -0
- package/package.json +98 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ghuri contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,459 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ghuri/agents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI agents for [Ghuri](https://github.com/ghurijs/ghuri) whose tools are **the models, serializers, viewsets
|
|
4
|
+
and permission classes you already wrote for your REST API** — plus a built-in **MCP server** so Claude
|
|
5
|
+
Desktop, Claude Code and other MCP clients can use the same tools. Built on the
|
|
6
|
+
[Vercel AI SDK](https://ai-sdk.dev) (v7): Workers AI, Anthropic and OpenAI out of the box.
|
|
7
|
+
|
|
8
|
+
> Status: alpha (`0.1.0-alpha.0`). APIs may change.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @ghuri/agents@next ai zod
|
|
12
|
+
# plus the provider(s) you use:
|
|
13
|
+
npm install workers-ai-provider # 'workers-ai:…' models (Cloudflare)
|
|
14
|
+
npm install @ai-sdk/anthropic # 'anthropic:…'
|
|
15
|
+
npm install @ai-sdk/openai # 'openai:…'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
1. [Concepts](#1-concepts)
|
|
21
|
+
2. [Quick start (Workers AI on Cloudflare)](#2-quick-start-workers-ai-on-cloudflare)
|
|
22
|
+
3. [Models and providers](#3-models-and-providers)
|
|
23
|
+
4. [Tools from your API: ModelTools, ViewSetTools, RouterTools](#4-tools-from-your-api)
|
|
24
|
+
5. [Custom tools](#5-custom-tools)
|
|
25
|
+
6. [Running agents from code](#6-running-agents-from-code)
|
|
26
|
+
7. [Conversation memory](#7-conversation-memory)
|
|
27
|
+
8. [HTTP endpoints and streaming to a frontend](#8-http-endpoints-and-streaming-to-a-frontend)
|
|
28
|
+
9. [MCP server (Claude Desktop / Claude Code)](#9-mcp-server)
|
|
29
|
+
10. [Background runs (Queues)](#10-background-runs)
|
|
30
|
+
11. [Security model](#11-security-model)
|
|
31
|
+
12. [Settings](#12-settings)
|
|
32
|
+
13. [Testing your agents](#13-testing-your-agents)
|
|
33
|
+
|
|
34
|
+
## 1. Concepts
|
|
35
|
+
|
|
36
|
+
| Piece | What it is |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `Agent` | A class with a `model`, `instructions`, `tools` and `permissionClasses`. `run()` / `stream()` drive the AI SDK tool loop. |
|
|
39
|
+
| Tools | `ModelTools(Model)`, `ViewSetTools(ViewSet)`, `RouterTools(router)` generate tools (with JSON Schemas from your serializers) that execute through the **same REST pipeline** as HTTP; `@tool` / `tool()` for your own. |
|
|
40
|
+
| User | Every run has a user (`run(input, { user })`). Every tool executes **as that user**: your permission classes, querysets and `filter` scoping decide what the model may see or change. |
|
|
41
|
+
| Memory | `AgentsConfig` stores `Conversation`s and `Message`s with the ORM (D1, Postgres, SQLite). |
|
|
42
|
+
| `AgentRouter` | Mounts agents as HTTP endpoints (JSON or SSE in the AI SDK UI message stream format) and as an MCP server. |
|
|
43
|
+
|
|
44
|
+
## 2. Quick start (Workers AI on Cloudflare)
|
|
45
|
+
|
|
46
|
+
`wrangler.jsonc` — add the Workers AI binding:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
{
|
|
50
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
51
|
+
"ai": { "binding": "AI" }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`src/settings.ts`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { AuthConfig, AuthenticationMiddleware, ContentTypesConfig, SessionMiddleware, SessionsConfig, AuthTokenConfig } from '@ghuri/auth'
|
|
59
|
+
import { SessionAuthentication, TokenAuthentication } from '@ghuri/auth/rest'
|
|
60
|
+
import { AgentsConfig } from '@ghuri/agents'
|
|
61
|
+
|
|
62
|
+
export default defineSettings((env) => ({
|
|
63
|
+
// …
|
|
64
|
+
INSTALLED_APPS: [ContentTypesConfig, AuthConfig, SessionsConfig, AuthTokenConfig, ShopConfig, AgentsConfig],
|
|
65
|
+
AGENTS: {
|
|
66
|
+
DEFAULT_MODEL: 'workers-ai:@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
67
|
+
AUTHENTICATION_CLASSES: [TokenAuthentication, SessionAuthentication],
|
|
68
|
+
},
|
|
69
|
+
}))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`src/shop/agents.ts`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { Agent, ModelTools, ToolError, ViewSetTools, tool } from '@ghuri/agents'
|
|
76
|
+
import { IsAuthenticated } from '@ghuri/rest'
|
|
77
|
+
import { z } from 'zod'
|
|
78
|
+
import { Order } from './models'
|
|
79
|
+
import { OrderSerializer } from './serializers'
|
|
80
|
+
import { TicketViewSet } from './views'
|
|
81
|
+
|
|
82
|
+
export class SupportAgent extends Agent {
|
|
83
|
+
static agentName = 'support'
|
|
84
|
+
instructions = 'You help customers with their orders. Be brief.'
|
|
85
|
+
tools = [
|
|
86
|
+
new ModelTools(Order, {
|
|
87
|
+
actions: ['list', 'retrieve'],
|
|
88
|
+
serializer: OrderSerializer,
|
|
89
|
+
filter: (qs, user) => qs.filter({ customer: user.pk }), // users only ever see their own orders
|
|
90
|
+
searchFields: ['number'],
|
|
91
|
+
orderingFields: ['createdAt'],
|
|
92
|
+
}),
|
|
93
|
+
new ViewSetTools(TicketViewSet, { actions: ['create'] }), // tickets_create, with TicketViewSet's rules
|
|
94
|
+
]
|
|
95
|
+
permissionClasses = [IsAuthenticated]
|
|
96
|
+
|
|
97
|
+
@tool({ description: 'Request a refund for an order', input: z.object({ orderId: z.number() }) })
|
|
98
|
+
async refund({ orderId }: { orderId: number }, { user }) {
|
|
99
|
+
const order = await Order.objects.filter({ id: orderId, customer: user.pk }).first()
|
|
100
|
+
if (!order) throw new ToolError(`Order ${orderId} not found.`)
|
|
101
|
+
// …
|
|
102
|
+
return { ok: true }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`src/urls.ts`:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { AgentRouter } from '@ghuri/agents'
|
|
111
|
+
import { include, path } from '@ghuri/core'
|
|
112
|
+
import { SupportAgent } from './shop/agents'
|
|
113
|
+
|
|
114
|
+
export const agents = new AgentRouter()
|
|
115
|
+
agents.register('support', SupportAgent)
|
|
116
|
+
|
|
117
|
+
export const urlpatterns = [
|
|
118
|
+
path('agents/', include(agents.urls)), // POST /agents/support/chat, GET /agents/support/conversations/
|
|
119
|
+
path('mcp/', agents.mcp({ name: 'shop' })), // Streamable HTTP MCP endpoint
|
|
120
|
+
]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The agents app ships its migration (`Conversation`, `Message`; the user foreign key follows
|
|
124
|
+
`AUTH_USER_MODEL`): create the tables with `ghuri migrate`.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
curl -X POST https://shop.example.com/agents/support/chat \
|
|
128
|
+
-H 'Authorization: Token <key>' -H 'Content-Type: application/json' \
|
|
129
|
+
-d '{"message": "Where is my last order?"}'
|
|
130
|
+
# {"conversationId":"7c0e…","text":"Your order A-1042 shipped yesterday.","toolCalls":[…],"usage":{…}}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> `@tool` works as a standard TC39 decorator (TypeScript 5, esbuild, wrangler) and as a legacy one (Vitest's
|
|
134
|
+
> oxc with `oxc: { decorator: { legacy: true } }`, as `ghuri startproject` configures; jiti / the `ghuri` CLI).
|
|
135
|
+
> Prefer no decorators? See [Custom tools](#5-custom-tools).
|
|
136
|
+
|
|
137
|
+
## 3. Models and providers
|
|
138
|
+
|
|
139
|
+
`model` is a `'provider:model-id'` string or any AI SDK `LanguageModel` instance. Unset → `AGENTS.DEFAULT_MODEL`.
|
|
140
|
+
|
|
141
|
+
| Prefix | Package (loaded lazily) | Credentials |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `workers-ai:` | `workers-ai-provider` | the `AI` binding (`AGENTS.AI_BINDING`) |
|
|
144
|
+
| `anthropic:` | `@ai-sdk/anthropic` | `AGENTS.PROVIDERS.anthropic.apiKey`, else env `ANTHROPIC_API_KEY` |
|
|
145
|
+
| `openai:` | `@ai-sdk/openai` | `AGENTS.PROVIDERS.openai.apiKey`, else env `OPENAI_API_KEY` |
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
class Analyst extends Agent {
|
|
149
|
+
model = 'anthropic:claude-sonnet-4-5'
|
|
150
|
+
temperature = 0.2
|
|
151
|
+
maxSteps = 8 // tool round trips (default AGENTS.MAX_STEPS = 10)
|
|
152
|
+
providerOptions = { anthropic: { thinking: { type: 'enabled', budgetTokens: 4000 } } }
|
|
153
|
+
}
|
|
154
|
+
// or an instance: model = createOpenAI({ apiKey, baseURL })('gpt-5-mini')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Extra options in `AGENTS.PROVIDERS[name]` are passed to the provider factory (`baseURL`, `headers`, Workers AI
|
|
158
|
+
`gateway`…). Provider packages are imported only when a model of that provider is first resolved, so unused
|
|
159
|
+
providers need not be installed (bundlers skip missing ones). Add providers with `registerProvider`:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { registerProvider } from '@ghuri/agents'
|
|
163
|
+
registerProvider('google', async (id, { config, env }) => {
|
|
164
|
+
const { createGoogleGenerativeAI } = await import('@ai-sdk/google')
|
|
165
|
+
return createGoogleGenerativeAI({ apiKey: config.apiKey ?? env.GOOGLE_API_KEY })(id)
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## 4. Tools from your API
|
|
170
|
+
|
|
171
|
+
All generated tools run through `router.registry[i].invoke()` — the full DRF pipeline without HTTP:
|
|
172
|
+
authentication is forced to the run's user, then **permission classes, throttles, `getQueryset()`,
|
|
173
|
+
filters, pagination, serializer validation and representation** apply exactly as for an HTTP request.
|
|
174
|
+
4xx answers become tool errors the model can read (`"You do not have permission to perform this action. (HTTP 403)"`).
|
|
175
|
+
|
|
176
|
+
### ModelTools
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
new ModelTools(Order, {
|
|
180
|
+
actions: ['list', 'retrieve', 'create', 'update', 'delete'], // default ['list', 'retrieve']
|
|
181
|
+
serializer: OrderSerializer, // default: ModelSerializer with fields '__all__'
|
|
182
|
+
filter: (qs, user) => qs.filter({ customer: user.pk }), // applies to every action
|
|
183
|
+
searchFields: ['number'], // → `search` argument
|
|
184
|
+
orderingFields: ['createdAt', 'total'], // → `ordering` argument
|
|
185
|
+
filterFields: ['status'], // → exact-match arguments
|
|
186
|
+
pageSize: 20, // → `page`, `pageSize` arguments
|
|
187
|
+
permissionClasses: [IsAuthenticated], // default REST_FRAMEWORK.DEFAULT_PERMISSION_CLASSES
|
|
188
|
+
saveKwargs: (user) => ({ customer: user.pk }), // values forced on create / update
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Generates `orders_list`, `orders_retrieve`, `orders_create`, `orders_update` (partial update: only the given
|
|
193
|
+
fields change) and `orders_delete`. Input schemas come from the serializer (`jsonSchema({ mode: 'input' })`), so
|
|
194
|
+
read-only fields are not offered and choices become `enum`s. Tools carry MCP annotations
|
|
195
|
+
(`readOnlyHint`, `destructiveHint`, `idempotentHint`).
|
|
196
|
+
|
|
197
|
+
### ViewSetTools
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
new ViewSetTools(PostViewSet, { actions: ['list', 'retrieve', 'create', 'publish'] }) // posts_list, …, posts_publish
|
|
201
|
+
new ViewSetTools(router.registry[0]) // every routed action
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Any routed action works, including `@action`s (detail actions take an `id`; bodies use the action's serializer
|
|
205
|
+
when it sets `serializerClass`, otherwise a free-form `data` object). `'delete'` is accepted as an alias of `destroy`.
|
|
206
|
+
Options: `prefix` (tool name prefix), `basename`, `descriptions: { list: '…' }`.
|
|
207
|
+
|
|
208
|
+
### RouterTools
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
new RouterTools(router) // the whole API
|
|
212
|
+
new RouterTools(router, { include: ['posts', 'comments'], readOnly: true })
|
|
213
|
+
new RouterTools(router, { exclude: ['users', 'posts_destroy'], actions: { posts: ['list', 'create'] } })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 5. Custom tools
|
|
217
|
+
|
|
218
|
+
Three equivalent forms; tools receive `(input, ctx)` where `ctx.user` is the requesting user:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
class OpsAgent extends Agent {
|
|
222
|
+
// 1. decorator
|
|
223
|
+
@tool({ description: 'Restart a job', input: z.object({ jobId: z.string() }) })
|
|
224
|
+
async restartJob({ jobId }, { user }) { … }
|
|
225
|
+
|
|
226
|
+
// 2. no decorators: options here, implementation in the method of the same name
|
|
227
|
+
static tools = { cancelJob: tool({ description: 'Cancel a job', input: z.object({ jobId: z.string() }) }) }
|
|
228
|
+
async cancelJob({ jobId }, ctx) { … }
|
|
229
|
+
|
|
230
|
+
// 3. standalone, in the tools list
|
|
231
|
+
tools = [tool({ name: 'now', description: 'Current server time', execute: () => new Date().toISOString() })]
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`input` is a Zod schema (validated; descriptions and defaults reach the model) or a plain JSON Schema object.
|
|
236
|
+
Throw `ToolError` for failures the model should see; other exceptions are reported to the model as
|
|
237
|
+
"The tool failed with an internal error." (the real message only when `DEBUG`) and logged. Implement the
|
|
238
|
+
`Toolkit` interface (`getTools(): AgentTool[]`) to package your own tool collections.
|
|
239
|
+
|
|
240
|
+
## 6. Running agents from code
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const result = await new SupportAgent().run('Where is order A-1042?', { user })
|
|
244
|
+
result.text // final answer
|
|
245
|
+
result.toolCalls // [{ toolName, input, output } | { toolName, input, error }]
|
|
246
|
+
result.messages // AI SDK messages added by this run
|
|
247
|
+
result.usage // tokens
|
|
248
|
+
result.conversationId // pass it back to continue
|
|
249
|
+
|
|
250
|
+
await SupportAgent.run('…', { user, conversationId }) // static shortcut
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Options: `user`, `conversationId`, `signal`, `memory` (force on/off), `checkPermissions` (check the agent's
|
|
254
|
+
`permissionClasses` — the HTTP and MCP layers always do), `context` (free-form values for hooks).
|
|
255
|
+
|
|
256
|
+
Instructions may depend on the run:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
instructions = (ctx: AgentRunContext) => `You help ${ctx.user.username}. Today is ${new Date().toDateString()}.`
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Hooks:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
class Audited extends Agent {
|
|
266
|
+
async beforeRun(ctx) { /* edit ctx.messages, throw to abort */ }
|
|
267
|
+
async onToolCall(call, ctx) { // before every tool call
|
|
268
|
+
if (call.toolName.endsWith('_delete') && !ctx.user.isStaff) throw new ToolError('Ask a staff member.')
|
|
269
|
+
}
|
|
270
|
+
async afterRun(result, ctx) { await AuditLog.objects.create({ … }) }
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 7. Conversation memory
|
|
275
|
+
|
|
276
|
+
With `AgentsConfig` installed, every run is stored as a `Conversation` (owner, agent name, title, metadata,
|
|
277
|
+
timestamps) with its `Message`s (role + AI SDK content parts — text, tool calls with inputs, tool results and
|
|
278
|
+
errors). Passing `conversationId` loads the history first. Conversations belong to their user: another user's
|
|
279
|
+
id fails with `ConversationNotFound` (404 over HTTP). Set `memory = false` on an agent (or `run(…, { memory: false })`)
|
|
280
|
+
for stateless runs. `loadMessages()`, `saveMessages()`, `createConversation()` are exported for custom flows.
|
|
281
|
+
|
|
282
|
+
The conversation's `agent` column is the agent name: the `AgentRouter.register()` name over HTTP/MCP, otherwise
|
|
283
|
+
`static agentName` (falling back to the class name — set `agentName` explicitly, minifiers rename classes).
|
|
284
|
+
|
|
285
|
+
## 8. HTTP endpoints and streaming to a frontend
|
|
286
|
+
|
|
287
|
+
`include(agents.urls)` serves, per registered agent:
|
|
288
|
+
|
|
289
|
+
| Route | |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `POST <name>/chat` | body `{ message, conversationId? }` (or an AI SDK `useChat` body with `messages`). JSON `{ conversationId, text, toolCalls, usage, finishReason }`, or **SSE** in the AI SDK UI message stream format with `Accept: text/event-stream` or `?stream=1`. |
|
|
292
|
+
| `GET <name>/conversations/` | the user's conversations `{ count, results }` (`?limit=&offset=`). |
|
|
293
|
+
| `GET <name>/conversations/<id>/` | one conversation with its messages (owner only). |
|
|
294
|
+
| `DELETE <name>/conversations/<id>/` | delete it (owner only). |
|
|
295
|
+
|
|
296
|
+
When the model can't be used (provider package missing, no API key, no Workers AI binding) the chat answers
|
|
297
|
+
**503** `{ "detail": "The agent's language model is not available." }` — with the reason when `DEBUG` is on.
|
|
298
|
+
|
|
299
|
+
Authentication uses `AGENTS.AUTHENTICATION_CLASSES` (default: `REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`);
|
|
300
|
+
access uses the agent's `permissionClasses` (default `AGENTS.PERMISSION_CLASSES`, i.e. `[IsAuthenticated]`). With
|
|
301
|
+
session authentication, unsafe requests need the CSRF token (`X-CSRFToken`) like any API view.
|
|
302
|
+
|
|
303
|
+
The SSE stream's `start` chunk carries `messageMetadata: { conversationId }` (also in the `x-conversation-id`
|
|
304
|
+
header) and closes only after the conversation was saved. With React and `@ai-sdk/react`:
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
import { useChat } from '@ai-sdk/react'
|
|
308
|
+
import { DefaultChatTransport } from 'ai'
|
|
309
|
+
|
|
310
|
+
const conversationId = useRef<string>()
|
|
311
|
+
const { messages, sendMessage, status } = useChat({
|
|
312
|
+
transport: new DefaultChatTransport({
|
|
313
|
+
api: '/agents/support/chat?stream=1',
|
|
314
|
+
headers: { 'X-CSRFToken': getCookie('csrftoken') },
|
|
315
|
+
prepareSendMessagesRequest: ({ messages }) => ({
|
|
316
|
+
body: { message: textOf(messages.at(-1)), conversationId: conversationId.current },
|
|
317
|
+
}),
|
|
318
|
+
}),
|
|
319
|
+
onFinish: ({ message }) => { conversationId.current ??= message.metadata?.conversationId },
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Programmatically: `const s = await agent.stream(input, { user })` → `s.toUIMessageStreamResponse()`,
|
|
324
|
+
`s.toUIMessageStream()`, `s.textStream`, `await s.done` (final `AgentRunResult`).
|
|
325
|
+
|
|
326
|
+
## 9. MCP server
|
|
327
|
+
|
|
328
|
+
`agents.mcp(options)` returns URL patterns for a **Streamable HTTP** MCP endpoint (JSON-RPC 2.0 over POST with
|
|
329
|
+
JSON responses; GET answers 405 — no server-initiated stream). Implemented in-house, Workers-safe.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
path('mcp/', agents.mcp({
|
|
333
|
+
name: 'shop', version: '1.0.0',
|
|
334
|
+
instructions: 'Tools for the Example shop.',
|
|
335
|
+
tools: [new RouterTools(router, { readOnly: true }), new ModelTools(Order, { filter: … })],
|
|
336
|
+
agents: true, // registered agents as ask_<name> tools (or a list of names, or false)
|
|
337
|
+
// authenticationClasses / permissionClasses: default AGENTS.AUTHENTICATION_CLASSES / AGENTS.PERMISSION_CLASSES
|
|
338
|
+
}))
|
|
339
|
+
// without an AgentRouter: new McpServer({ tools, agents: { support: SupportAgent } }).urls (classes or factories)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Supported: `initialize` (protocol versions `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`; the client's
|
|
343
|
+
version is echoed when supported), `notifications/*` (202), `ping`, `tools/list` (JSON Schema `inputSchema`,
|
|
344
|
+
annotations), `tools/call` (text + `structuredContent`; tool failures are results with `isError: true`),
|
|
345
|
+
empty `resources/list`, `resources/templates/list`, `prompts/list`, batches, and JSON-RPC errors
|
|
346
|
+
(`-32700`, `-32600`, `-32601`, `-32602`, `-32603`). Requests without valid credentials get HTTP 401 with
|
|
347
|
+
`WWW-Authenticate`; browser `Origin`s other than your host / `CSRF_TRUSTED_ORIGINS` are refused (DNS rebinding);
|
|
348
|
+
unknown `MCP-Protocol-Version` headers get 400.
|
|
349
|
+
|
|
350
|
+
Create a token for the user the client acts as (`ghuri drf_create_token alice`, or `Token.objects.create({ user })`).
|
|
351
|
+
|
|
352
|
+
**Claude Code** — `claude mcp add --transport http shop https://shop.example.com/mcp/ --header "Authorization: Token <key>"`,
|
|
353
|
+
or in `.mcp.json`:
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"mcpServers": {
|
|
358
|
+
"shop": {
|
|
359
|
+
"type": "http",
|
|
360
|
+
"url": "https://shop.example.com/mcp/",
|
|
361
|
+
"headers": { "Authorization": "Token ${SHOP_TOKEN}" }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Claude Desktop** (`claude_desktop_config.json`, through the `mcp-remote` bridge):
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"mcpServers": {
|
|
372
|
+
"shop": {
|
|
373
|
+
"command": "npx",
|
|
374
|
+
"args": ["-y", "mcp-remote", "https://shop.example.com/mcp/", "--header", "Authorization:${SHOP_AUTH}"],
|
|
375
|
+
"env": { "SHOP_AUTH": "Token 0123456789abcdef0123456789abcdef01234567" }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Clients that only send `Authorization: Bearer <token>`: add a keyword variant of `TokenAuthentication`
|
|
382
|
+
(or use `JWTAuthentication`):
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
class BearerTokenAuthentication extends TokenAuthentication { keyword = 'Bearer' }
|
|
386
|
+
AGENTS: { AUTHENTICATION_CLASSES: [TokenAuthentication, BearerTokenAuthentication] }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Mount the endpoint with a trailing slash and use that exact URL (`APPEND_SLASH` only redirects GETs).
|
|
390
|
+
|
|
391
|
+
## 10. Background runs
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// wrangler.jsonc: "queues": { "producers": [{ "binding": "AGENTS_QUEUE", "queue": "agents" }],
|
|
395
|
+
// "consumers": [{ "queue": "agents" }] }
|
|
396
|
+
agents.consumeQueue('agents') // once, e.g. in an AppConfig.ready()
|
|
397
|
+
const { conversationId } = await agents.create('support').enqueue('Summarize my week', { user })
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The consumer reloads the user (`AUTH_USER_MODEL`), runs the agent and stores the result in the conversation
|
|
401
|
+
(created up front so you can poll `GET …/conversations/<id>/`). Failed runs are retried by the queue.
|
|
402
|
+
|
|
403
|
+
## 11. Security model
|
|
404
|
+
|
|
405
|
+
- **Tools act as the user, never as the server.** Generated tools force-authenticate the run's user and go
|
|
406
|
+
through your viewset's permission classes, `getQueryset()` / `filter` scoping, object permissions and serializer
|
|
407
|
+
validation. Anonymous runs are unauthenticated requests. The model cannot do anything the user could not do
|
|
408
|
+
with your REST API — but it can do *anything* the user could, so expose only the actions you want
|
|
409
|
+
(`ModelTools` is read-only by default) and keep `filter` scoping on multi-tenant data.
|
|
410
|
+
- **Endpoints require authentication by default** (`AGENTS.PERMISSION_CLASSES = [IsAuthenticated]`); set
|
|
411
|
+
`[AllowAny]` deliberately for public agents (and consider throttling — LLM calls cost money).
|
|
412
|
+
- **Errors**: permission / validation / not-found failures are shown to the model; unexpected exceptions are not
|
|
413
|
+
(generic message unless `DEBUG`).
|
|
414
|
+
- **Conversations** are owner-only; anonymous conversations are reachable only by their unguessable UUID.
|
|
415
|
+
- **Prompt injection**: data returned by tools (e.g. user-written text) can steer the model. Gate destructive or
|
|
416
|
+
irreversible tools with `onToolCall`, keep `maxSteps` low, and audit with `afterRun`.
|
|
417
|
+
- **MCP**: token or session authentication through REST authentication classes; `Origin` validation; tools run as
|
|
418
|
+
the token's user.
|
|
419
|
+
|
|
420
|
+
## 12. Settings
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
AGENTS: {
|
|
424
|
+
DEFAULT_MODEL: 'workers-ai:@cf/meta/llama-3.3-70b-instruct-fp8-fast', // or a LanguageModel
|
|
425
|
+
AI_BINDING: 'AI', // Workers AI binding name
|
|
426
|
+
PROVIDERS: { anthropic: { apiKey: env.ANTHROPIC_API_KEY }, openai: { apiKey: env.OPENAI_API_KEY } },
|
|
427
|
+
MAX_STEPS: 10, // default tool-loop steps
|
|
428
|
+
AUTHENTICATION_CLASSES: [TokenAuthentication, SessionAuthentication], // default: REST defaults
|
|
429
|
+
PERMISSION_CLASSES: [IsAuthenticated], // default for agents / MCP
|
|
430
|
+
QUEUE_BINDING: 'AGENTS_QUEUE', // enqueue() producer binding
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## 13. Testing your agents
|
|
435
|
+
|
|
436
|
+
Use the AI SDK mock model — no network:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { MockLanguageModelV4 } from 'ai/test'
|
|
440
|
+
|
|
441
|
+
const model = new MockLanguageModelV4({
|
|
442
|
+
doGenerate: [
|
|
443
|
+
{ content: [{ type: 'tool-call', toolCallId: '1', toolName: 'orders_list', input: '{}' }],
|
|
444
|
+
finishReason: { unified: 'tool-calls', raw: undefined }, usage, warnings: [] },
|
|
445
|
+
{ content: [{ type: 'text', text: 'You have 2 orders.' }],
|
|
446
|
+
finishReason: { unified: 'stop', raw: undefined }, usage, warnings: [] },
|
|
447
|
+
],
|
|
448
|
+
})
|
|
449
|
+
class TestAgent extends SupportAgent { model = model }
|
|
450
|
+
const result = await new TestAgent().run('orders?', { user })
|
|
451
|
+
expect(result.toolCalls[0].output.count).toBe(2)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
For HTTP / MCP tests, mount the router in `ROOT_URLCONF` and use `@ghuri/core/testing` / `@ghuri/rest/testing`
|
|
455
|
+
clients (`client.forceAuthenticate(user)` or a real `Authorization: Token …` header).
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT
|