@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/dist/index.js
ADDED
|
@@ -0,0 +1,1995 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/agent.ts
|
|
8
|
+
import {
|
|
9
|
+
anonymousUser as anonymousUser2,
|
|
10
|
+
ImproperlyConfigured as ImproperlyConfigured3,
|
|
11
|
+
tryGetContext as tryGetContext6
|
|
12
|
+
} from "@ghuri/core";
|
|
13
|
+
import { NotAuthenticated, PermissionDenied } from "@ghuri/rest";
|
|
14
|
+
import {
|
|
15
|
+
tool as aiTool,
|
|
16
|
+
createUIMessageStreamResponse,
|
|
17
|
+
generateText,
|
|
18
|
+
jsonSchema,
|
|
19
|
+
stepCountIs,
|
|
20
|
+
streamText
|
|
21
|
+
} from "ai";
|
|
22
|
+
|
|
23
|
+
// src/memory.ts
|
|
24
|
+
import { apps as apps2 } from "@ghuri/core";
|
|
25
|
+
|
|
26
|
+
// src/errors.ts
|
|
27
|
+
import { GhuriError } from "@ghuri/core";
|
|
28
|
+
var AgentError = class extends GhuriError {
|
|
29
|
+
};
|
|
30
|
+
var ToolError = class extends AgentError {
|
|
31
|
+
/** HTTP-like status (403, 400, 404…) when the error came from a view. */
|
|
32
|
+
status;
|
|
33
|
+
/** Structured details (DRF error body, validation errors…). */
|
|
34
|
+
detail;
|
|
35
|
+
constructor(message, opts = {}) {
|
|
36
|
+
super(message, { cause: opts.cause });
|
|
37
|
+
this.status = opts.status;
|
|
38
|
+
this.detail = opts.detail;
|
|
39
|
+
}
|
|
40
|
+
toJSON() {
|
|
41
|
+
return {
|
|
42
|
+
error: this.message,
|
|
43
|
+
...this.status !== void 0 ? { status: this.status } : {},
|
|
44
|
+
...this.detail !== void 0 ? { detail: this.detail } : {}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var ToolInputError = class extends ToolError {
|
|
49
|
+
constructor(message, detail) {
|
|
50
|
+
super(message, { status: 400, detail });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var UnknownToolError = class extends AgentError {
|
|
54
|
+
};
|
|
55
|
+
var ModelResolutionError = class extends AgentError {
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/models.ts
|
|
59
|
+
import { apps, tryGetContext } from "@ghuri/core";
|
|
60
|
+
import {
|
|
61
|
+
CASCADE,
|
|
62
|
+
CharField,
|
|
63
|
+
DateTimeField,
|
|
64
|
+
ForeignKey,
|
|
65
|
+
JSONField,
|
|
66
|
+
Model,
|
|
67
|
+
UUIDField
|
|
68
|
+
} from "@ghuri/orm";
|
|
69
|
+
function userModel() {
|
|
70
|
+
const label = tryGetContext()?.settings.AUTH_USER_MODEL ?? "auth.User";
|
|
71
|
+
return apps.getModel(label);
|
|
72
|
+
}
|
|
73
|
+
function newConversationId() {
|
|
74
|
+
return crypto.randomUUID();
|
|
75
|
+
}
|
|
76
|
+
function emptyMetadata() {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
var Conversation = class extends Model({
|
|
80
|
+
id: new UUIDField({ primaryKey: true, default: newConversationId, editable: false }),
|
|
81
|
+
user: new ForeignKey(userModel, {
|
|
82
|
+
null: true,
|
|
83
|
+
blank: true,
|
|
84
|
+
onDelete: CASCADE,
|
|
85
|
+
relatedName: "agentConversations"
|
|
86
|
+
}),
|
|
87
|
+
agent: new CharField({ maxLength: 100, dbIndex: true }),
|
|
88
|
+
title: new CharField({ maxLength: 200, blank: true, default: "" }),
|
|
89
|
+
metadata: new JSONField({ default: emptyMetadata, blank: true }),
|
|
90
|
+
createdAt: new DateTimeField({ autoNowAdd: true }),
|
|
91
|
+
updatedAt: new DateTimeField({ autoNow: true })
|
|
92
|
+
}) {
|
|
93
|
+
static Meta = { appLabel: "agents", ordering: ["-updatedAt"], verboseName: "conversation" };
|
|
94
|
+
toString() {
|
|
95
|
+
return this.title || String(this.id);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var MESSAGE_ROLES = ["system", "user", "assistant", "tool"];
|
|
99
|
+
var Message = class extends Model({
|
|
100
|
+
conversation: new ForeignKey(() => Conversation, { onDelete: CASCADE, relatedName: "messages" }),
|
|
101
|
+
role: new CharField({ maxLength: 20, choices: MESSAGE_ROLES.map((r) => [r, r]) }),
|
|
102
|
+
content: new JSONField({ null: true }),
|
|
103
|
+
createdAt: new DateTimeField({ autoNowAdd: true })
|
|
104
|
+
}) {
|
|
105
|
+
static Meta = { appLabel: "agents", ordering: ["id"], verboseName: "message" };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/memory.ts
|
|
109
|
+
var ConversationNotFound = class extends ToolError {
|
|
110
|
+
constructor(message) {
|
|
111
|
+
super(message, { status: 404 });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function memoryAvailable() {
|
|
115
|
+
try {
|
|
116
|
+
return apps2.ready && apps2.isInstalled("agents");
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function ownerPk(user) {
|
|
122
|
+
return user?.isAuthenticated ? user.pk : null;
|
|
123
|
+
}
|
|
124
|
+
function isOwner(conversation, user) {
|
|
125
|
+
const owner = conversation.userId ?? null;
|
|
126
|
+
const pk = ownerPk(user);
|
|
127
|
+
if (owner === null || pk === null) return owner === null && pk === null;
|
|
128
|
+
return String(owner) === String(pk);
|
|
129
|
+
}
|
|
130
|
+
async function getConversation(agent, id, user) {
|
|
131
|
+
let conv = null;
|
|
132
|
+
try {
|
|
133
|
+
conv = await Conversation.objects.filter({ id: String(id), agent }).first();
|
|
134
|
+
} catch {
|
|
135
|
+
conv = null;
|
|
136
|
+
}
|
|
137
|
+
if (!conv || !isOwner(conv, user)) throw new ConversationNotFound(`Conversation '${id}' not found.`);
|
|
138
|
+
return conv;
|
|
139
|
+
}
|
|
140
|
+
async function createConversation(agent, user, opts = {}) {
|
|
141
|
+
const pk = ownerPk(user);
|
|
142
|
+
return Conversation.objects.create({
|
|
143
|
+
agent,
|
|
144
|
+
title: (opts.title ?? "").slice(0, 200),
|
|
145
|
+
metadata: opts.metadata ?? {},
|
|
146
|
+
...pk !== null ? { userId: pk } : {}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function loadMessages(conversation) {
|
|
150
|
+
const id = typeof conversation === "string" ? conversation : String(conversation.pk);
|
|
151
|
+
const rows = await Message.objects.filter({ conversation: id }).orderBy("id");
|
|
152
|
+
return rows.map((r) => ({ role: r.role, content: r.content }));
|
|
153
|
+
}
|
|
154
|
+
function jsonContent(content) {
|
|
155
|
+
return JSON.parse(
|
|
156
|
+
JSON.stringify(content, (_k, v) => {
|
|
157
|
+
if (v instanceof Uint8Array) {
|
|
158
|
+
let s = "";
|
|
159
|
+
for (const b of v) s += String.fromCharCode(b);
|
|
160
|
+
return btoa(s);
|
|
161
|
+
}
|
|
162
|
+
if (v instanceof URL) return v.toString();
|
|
163
|
+
return v;
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
async function saveMessages(conversation, messages) {
|
|
168
|
+
if (!messages.length) return;
|
|
169
|
+
await Message.objects.bulkCreate(
|
|
170
|
+
messages.map(
|
|
171
|
+
(m) => new Message({
|
|
172
|
+
conversationId: String(conversation.pk),
|
|
173
|
+
role: m.role,
|
|
174
|
+
content: jsonContent(m.content)
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
await conversation.save({ updateFields: ["updatedAt"] });
|
|
179
|
+
}
|
|
180
|
+
function titleFrom(input) {
|
|
181
|
+
let text = "";
|
|
182
|
+
if (typeof input === "string") text = input;
|
|
183
|
+
else {
|
|
184
|
+
const first = input.find((m) => m.role === "user");
|
|
185
|
+
const c = first?.content;
|
|
186
|
+
if (typeof c === "string") text = c;
|
|
187
|
+
else if (Array.isArray(c)) text = c.map((p) => p.type === "text" ? p.text : "").join(" ");
|
|
188
|
+
}
|
|
189
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
190
|
+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/providers.ts
|
|
194
|
+
import { tryGetContext as tryGetContext3 } from "@ghuri/core";
|
|
195
|
+
|
|
196
|
+
// src/settings.ts
|
|
197
|
+
import { tryGetContext as tryGetContext2 } from "@ghuri/core";
|
|
198
|
+
import { IsAuthenticated } from "@ghuri/rest";
|
|
199
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
200
|
+
function agentsSettings() {
|
|
201
|
+
const s = tryGetContext2()?.settings.AGENTS;
|
|
202
|
+
return s && typeof s === "object" ? s : {};
|
|
203
|
+
}
|
|
204
|
+
function defaultPermissionClasses() {
|
|
205
|
+
return agentsSettings().PERMISSION_CLASSES ?? [IsAuthenticated];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/providers.ts
|
|
209
|
+
var KEY = /* @__PURE__ */ Symbol.for("ghuri.agents.providers");
|
|
210
|
+
var g = globalThis;
|
|
211
|
+
if (!g[KEY]) g[KEY] = /* @__PURE__ */ new Map();
|
|
212
|
+
var providers = g[KEY];
|
|
213
|
+
function registerProvider(name, factory) {
|
|
214
|
+
providers.set(name, factory);
|
|
215
|
+
}
|
|
216
|
+
function unregisterProvider(name) {
|
|
217
|
+
return providers.delete(name);
|
|
218
|
+
}
|
|
219
|
+
function getProviders() {
|
|
220
|
+
return [...providers.keys()];
|
|
221
|
+
}
|
|
222
|
+
function processEnv(name) {
|
|
223
|
+
const p = globalThis.process;
|
|
224
|
+
return p?.env?.[name];
|
|
225
|
+
}
|
|
226
|
+
function apiKey(ctx, envName) {
|
|
227
|
+
const v = ctx.config.apiKey ?? ctx.env?.[envName] ?? processEnv(envName);
|
|
228
|
+
return typeof v === "string" && v ? v : void 0;
|
|
229
|
+
}
|
|
230
|
+
function missingPackage(pkg, provider, cause) {
|
|
231
|
+
return new ModelResolutionError(
|
|
232
|
+
`The '${provider}:' model provider needs the '${pkg}' package: npm install ${pkg}`,
|
|
233
|
+
{ cause }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
function withoutKnown(config) {
|
|
237
|
+
const { apiKey: _k, ...rest } = config;
|
|
238
|
+
return rest;
|
|
239
|
+
}
|
|
240
|
+
registerProvider("workers-ai", async (modelId, ctx) => {
|
|
241
|
+
const bindingName = ctx.settings.AI_BINDING ?? "AI";
|
|
242
|
+
const binding = ctx.env?.[bindingName];
|
|
243
|
+
if (!binding) {
|
|
244
|
+
throw new ModelResolutionError(
|
|
245
|
+
`Workers AI binding '${bindingName}' not found in env. Add "ai": { "binding": "${bindingName}" } to wrangler.jsonc or set AGENTS.AI_BINDING.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
let mod;
|
|
249
|
+
try {
|
|
250
|
+
mod = await import("workers-ai-provider");
|
|
251
|
+
} catch (err) {
|
|
252
|
+
throw missingPackage("workers-ai-provider", "workers-ai", err);
|
|
253
|
+
}
|
|
254
|
+
return mod.createWorkersAI({ ...withoutKnown(ctx.config), binding })(modelId);
|
|
255
|
+
});
|
|
256
|
+
registerProvider("anthropic", async (modelId, ctx) => {
|
|
257
|
+
let mod;
|
|
258
|
+
try {
|
|
259
|
+
mod = await import("@ai-sdk/anthropic");
|
|
260
|
+
} catch (err) {
|
|
261
|
+
throw missingPackage("@ai-sdk/anthropic", "anthropic", err);
|
|
262
|
+
}
|
|
263
|
+
return mod.createAnthropic({ ...withoutKnown(ctx.config), apiKey: apiKey(ctx, "ANTHROPIC_API_KEY") })(
|
|
264
|
+
modelId
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
registerProvider("openai", async (modelId, ctx) => {
|
|
268
|
+
let mod;
|
|
269
|
+
try {
|
|
270
|
+
mod = await import("@ai-sdk/openai");
|
|
271
|
+
} catch (err) {
|
|
272
|
+
throw missingPackage("@ai-sdk/openai", "openai", err);
|
|
273
|
+
}
|
|
274
|
+
return mod.createOpenAI({ ...withoutKnown(ctx.config), apiKey: apiKey(ctx, "OPENAI_API_KEY") })(modelId);
|
|
275
|
+
});
|
|
276
|
+
function isLanguageModel(value) {
|
|
277
|
+
return !!value && typeof value === "object" && typeof value.modelId === "string" && (typeof value.doGenerate === "function" || typeof value.doStream === "function");
|
|
278
|
+
}
|
|
279
|
+
async function resolveModel(model, env) {
|
|
280
|
+
const settings = agentsSettings();
|
|
281
|
+
const m = model ?? settings.DEFAULT_MODEL;
|
|
282
|
+
if (!m) {
|
|
283
|
+
throw new ModelResolutionError(
|
|
284
|
+
"No model configured: set `model` on the agent (e.g. 'workers-ai:@cf/meta/llama-3.3-70b-instruct-fp8-fast') or AGENTS.DEFAULT_MODEL."
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (typeof m !== "string") return m;
|
|
288
|
+
const i = m.indexOf(":");
|
|
289
|
+
if (i <= 0) {
|
|
290
|
+
throw new ModelResolutionError(
|
|
291
|
+
`Model '${m}' must be '<provider>:<model-id>' (providers: ${getProviders().join(", ")}).`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const name = m.slice(0, i);
|
|
295
|
+
const modelId = m.slice(i + 1);
|
|
296
|
+
const factory = providers.get(name);
|
|
297
|
+
if (!factory) {
|
|
298
|
+
throw new ModelResolutionError(
|
|
299
|
+
`Unknown model provider '${name}' in '${m}' (registered: ${getProviders().join(", ")}). Use registerProvider().`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return factory(modelId, {
|
|
303
|
+
env: env ?? tryGetContext3()?.env ?? {},
|
|
304
|
+
settings,
|
|
305
|
+
config: settings.PROVIDERS?.[name] ?? {}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/queue.ts
|
|
310
|
+
import {
|
|
311
|
+
anonymousUser,
|
|
312
|
+
apps as apps3,
|
|
313
|
+
ImproperlyConfigured,
|
|
314
|
+
onQueue,
|
|
315
|
+
tryGetContext as tryGetContext4
|
|
316
|
+
} from "@ghuri/core";
|
|
317
|
+
var AGENT_JOB_TYPE = "ghuri.agents.run";
|
|
318
|
+
async function enqueueRun(agent, input, opts = {}) {
|
|
319
|
+
const name = opts.binding ?? agentsSettings().QUEUE_BINDING ?? "AGENTS_QUEUE";
|
|
320
|
+
const queue = tryGetContext4()?.env?.[name];
|
|
321
|
+
if (!queue || typeof queue.send !== "function") {
|
|
322
|
+
throw new ImproperlyConfigured(
|
|
323
|
+
`Queue producer binding '${name}' not found: add it to wrangler.jsonc ("queues": { "producers": [...] }) or set AGENTS.QUEUE_BINDING.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
let conversationId = opts.conversationId ?? void 0;
|
|
327
|
+
if (!conversationId && agent.memory !== false && memoryAvailable()) {
|
|
328
|
+
const conv = await createConversation(agent.getName(), opts.user, { title: titleFrom(input) });
|
|
329
|
+
conversationId = String(conv.pk);
|
|
330
|
+
}
|
|
331
|
+
const job = {
|
|
332
|
+
type: AGENT_JOB_TYPE,
|
|
333
|
+
agent: agent.getName(),
|
|
334
|
+
input,
|
|
335
|
+
userId: ownerPk(opts.user),
|
|
336
|
+
...conversationId ? { conversationId } : {},
|
|
337
|
+
...opts.context ? { context: opts.context } : {}
|
|
338
|
+
};
|
|
339
|
+
await queue.send(job);
|
|
340
|
+
return { conversationId };
|
|
341
|
+
}
|
|
342
|
+
async function loadJobUser(userId) {
|
|
343
|
+
if (userId === null || userId === void 0) return anonymousUser;
|
|
344
|
+
const label = tryGetContext4()?.settings.AUTH_USER_MODEL ?? "auth.User";
|
|
345
|
+
const model = apps3.getModel(label);
|
|
346
|
+
return model.objects.get({ pk: userId });
|
|
347
|
+
}
|
|
348
|
+
function consumeAgentQueue(queue, create) {
|
|
349
|
+
return onQueue(queue, async (batch) => {
|
|
350
|
+
for (const msg of batch.messages) {
|
|
351
|
+
const job = msg.body;
|
|
352
|
+
if (!job || job.type !== AGENT_JOB_TYPE) continue;
|
|
353
|
+
try {
|
|
354
|
+
const agent = create(job.agent);
|
|
355
|
+
const user = await loadJobUser(job.userId);
|
|
356
|
+
await agent.run(job.input, {
|
|
357
|
+
user,
|
|
358
|
+
conversationId: job.conversationId ?? null,
|
|
359
|
+
context: job.context
|
|
360
|
+
});
|
|
361
|
+
msg.ack();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error(`[ghuri.agents] queued run of '${job.agent}' failed:`, err);
|
|
364
|
+
msg.retry();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/tools/execute.ts
|
|
371
|
+
import { HttpError, runWithContext, tryGetContext as tryGetContext5, ValidationError } from "@ghuri/core";
|
|
372
|
+
import { APIException } from "@ghuri/rest";
|
|
373
|
+
function inContext(ctx, fn) {
|
|
374
|
+
if (!ctx || tryGetContext5() === ctx) return fn();
|
|
375
|
+
return runWithContext(ctx, fn);
|
|
376
|
+
}
|
|
377
|
+
function isDebug() {
|
|
378
|
+
return !!tryGetContext5()?.settings.DEBUG;
|
|
379
|
+
}
|
|
380
|
+
function toToolError(err, toolName) {
|
|
381
|
+
if (err instanceof ToolError) return err;
|
|
382
|
+
if (err instanceof APIException) {
|
|
383
|
+
return new ToolError(`${err.message || "Request failed."} (HTTP ${err.statusCode})`, {
|
|
384
|
+
status: err.statusCode,
|
|
385
|
+
cause: err
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (err instanceof HttpError) {
|
|
389
|
+
return new ToolError(`${err.message} (HTTP ${err.status})`, {
|
|
390
|
+
status: err.status,
|
|
391
|
+
cause: err
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (err instanceof ValidationError) {
|
|
395
|
+
const detail = err.messageDict ?? err.messages;
|
|
396
|
+
return new ToolError(`Validation failed: ${JSON.stringify(detail ?? err.message)}`, {
|
|
397
|
+
status: 400,
|
|
398
|
+
detail,
|
|
399
|
+
cause: err
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (err instanceof Error && err.name === "AbortError") return new ToolError("Aborted.", { cause: err });
|
|
403
|
+
console.error(`[ghuri.agents] tool ${toolName ?? ""} failed:`, err);
|
|
404
|
+
const message = isDebug() && err instanceof Error ? err.message : "The tool failed with an internal error.";
|
|
405
|
+
return new ToolError(message, { status: 500, cause: err });
|
|
406
|
+
}
|
|
407
|
+
async function runTool(tool2, args, ctx) {
|
|
408
|
+
try {
|
|
409
|
+
const input = tool2.parse ? tool2.parse(args) : args ?? {};
|
|
410
|
+
return await tool2.execute(input, ctx);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
throw toToolError(err, tool2.name);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/tools/tool.ts
|
|
417
|
+
import { ImproperlyConfigured as ImproperlyConfigured2 } from "@ghuri/core";
|
|
418
|
+
|
|
419
|
+
// src/tools/schema.ts
|
|
420
|
+
import { z } from "zod";
|
|
421
|
+
function sanitizeToolName(name) {
|
|
422
|
+
const s = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
423
|
+
return (s || "tool").slice(0, 64);
|
|
424
|
+
}
|
|
425
|
+
function snakeCase(s) {
|
|
426
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
427
|
+
}
|
|
428
|
+
function isZodSchema(value) {
|
|
429
|
+
return !!value && typeof value === "object" && "_zod" in value && typeof value.safeParse === "function";
|
|
430
|
+
}
|
|
431
|
+
function toJSONSchema(input) {
|
|
432
|
+
if (input === void 0 || input === null) return { type: "object", properties: {} };
|
|
433
|
+
if (isZodSchema(input)) {
|
|
434
|
+
const schema = z.toJSONSchema(input, { io: "input", unrepresentable: "any" });
|
|
435
|
+
delete schema.$schema;
|
|
436
|
+
return schema;
|
|
437
|
+
}
|
|
438
|
+
if (typeof input === "object") return { ...input };
|
|
439
|
+
throw new TypeError("Tool input must be a Zod schema or a JSON Schema object.");
|
|
440
|
+
}
|
|
441
|
+
function makeParser(input) {
|
|
442
|
+
if (isZodSchema(input)) {
|
|
443
|
+
return (value) => {
|
|
444
|
+
const res = input.safeParse(value ?? {});
|
|
445
|
+
if (!res.success) {
|
|
446
|
+
const issues = res.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
447
|
+
const summary = issues.map((i) => i.path ? `${i.path}: ${i.message}` : i.message).join("; ");
|
|
448
|
+
throw new ToolInputError(`Invalid arguments: ${summary}`, issues);
|
|
449
|
+
}
|
|
450
|
+
return res.data;
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return (value) => {
|
|
454
|
+
if (value === void 0 || value === null) return {};
|
|
455
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
456
|
+
throw new ToolInputError("Tool arguments must be a JSON object.");
|
|
457
|
+
return value;
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function cleanInputSchema(schema) {
|
|
461
|
+
if (!schema) return { type: "object", properties: {} };
|
|
462
|
+
const { title: _t, ...rest } = schema;
|
|
463
|
+
return { ...rest, type: "object", properties: { ...schema.properties ?? {} } };
|
|
464
|
+
}
|
|
465
|
+
function parametersToProperties(params) {
|
|
466
|
+
const properties = {};
|
|
467
|
+
const required = [];
|
|
468
|
+
for (const p of params) {
|
|
469
|
+
if (p.in && p.in !== "query") continue;
|
|
470
|
+
const name = String(p.name);
|
|
471
|
+
properties[name] = {
|
|
472
|
+
...p.schema ?? { type: "string" },
|
|
473
|
+
...p.description ? { description: String(p.description) } : {}
|
|
474
|
+
};
|
|
475
|
+
if (p.required) required.push(name);
|
|
476
|
+
}
|
|
477
|
+
return { properties, required };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/tools/tool.ts
|
|
481
|
+
var TOOL = /* @__PURE__ */ Symbol.for("ghuri.agents.tool");
|
|
482
|
+
var TOOL_OPTIONS = /* @__PURE__ */ Symbol.for("ghuri.agents.toolOptions");
|
|
483
|
+
function mark(fn, name, opts) {
|
|
484
|
+
if (typeof fn !== "function") throw new TypeError("@tool() can only decorate methods.");
|
|
485
|
+
fn[TOOL] = { ...opts, name: opts.name ?? name };
|
|
486
|
+
}
|
|
487
|
+
function legacyMethodDescriptor(target, key, descriptor) {
|
|
488
|
+
if (typeof key !== "string" && typeof key !== "symbol") return void 0;
|
|
489
|
+
if (!target || typeof target !== "object" && typeof target !== "function") return void 0;
|
|
490
|
+
if (descriptor !== void 0 && descriptor !== target) return void 0;
|
|
491
|
+
const real = Object.getOwnPropertyDescriptor(target, key);
|
|
492
|
+
return real && typeof real.value === "function" ? real : void 0;
|
|
493
|
+
}
|
|
494
|
+
function tool(opts) {
|
|
495
|
+
if (!opts || typeof opts.description !== "string")
|
|
496
|
+
throw new TypeError("tool() requires a `description` string.");
|
|
497
|
+
const decorator = ((value, context, descriptor) => {
|
|
498
|
+
if (descriptor && typeof descriptor.value === "function") {
|
|
499
|
+
mark(descriptor.value, String(context), opts);
|
|
500
|
+
return descriptor;
|
|
501
|
+
}
|
|
502
|
+
if (typeof value === "function") {
|
|
503
|
+
const name = context && typeof context === "object" && "name" in context ? String(context.name) : value.name;
|
|
504
|
+
mark(value, name, opts);
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
const legacy = legacyMethodDescriptor(value, context, descriptor);
|
|
508
|
+
if (legacy) {
|
|
509
|
+
mark(legacy.value, String(context), opts);
|
|
510
|
+
return legacy;
|
|
511
|
+
}
|
|
512
|
+
throw new TypeError("@tool() can only decorate methods.");
|
|
513
|
+
});
|
|
514
|
+
Object.defineProperty(decorator, TOOL_OPTIONS, { value: opts });
|
|
515
|
+
return decorator;
|
|
516
|
+
}
|
|
517
|
+
function isToolDecorator(value) {
|
|
518
|
+
return typeof value === "function" && TOOL_OPTIONS in value;
|
|
519
|
+
}
|
|
520
|
+
function defineTool(name, opts, execute) {
|
|
521
|
+
return {
|
|
522
|
+
name: sanitizeToolName(opts.name ?? name),
|
|
523
|
+
description: opts.description,
|
|
524
|
+
inputSchema: toJSONSchema(opts.input),
|
|
525
|
+
...opts.output ? { outputSchema: opts.output } : {},
|
|
526
|
+
...opts.annotations ? { annotations: opts.annotations } : {},
|
|
527
|
+
parse: makeParser(opts.input),
|
|
528
|
+
execute
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function standaloneTool(decorator, key) {
|
|
532
|
+
const opts = decorator[TOOL_OPTIONS];
|
|
533
|
+
if (typeof opts.execute !== "function") {
|
|
534
|
+
throw new ImproperlyConfigured2(
|
|
535
|
+
`tool('${opts.name ?? key ?? opts.description}') has no \`execute\`: use it as a method decorator or pass execute.`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const name = opts.name ?? key;
|
|
539
|
+
if (!name) throw new ImproperlyConfigured2(`A standalone tool needs a \`name\` (${opts.description}).`);
|
|
540
|
+
return defineTool(name, opts, opts.execute);
|
|
541
|
+
}
|
|
542
|
+
function collectDeclaredTools(instance) {
|
|
543
|
+
const cls = instance.constructor;
|
|
544
|
+
const found = /* @__PURE__ */ new Map();
|
|
545
|
+
const chain = [];
|
|
546
|
+
for (let p = Object.getPrototypeOf(instance); p && p !== Object.prototype; p = Object.getPrototypeOf(p))
|
|
547
|
+
chain.unshift(p);
|
|
548
|
+
for (const proto of chain) {
|
|
549
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
550
|
+
if (key === "constructor") continue;
|
|
551
|
+
const fn = Object.getOwnPropertyDescriptor(proto, key)?.value;
|
|
552
|
+
if (typeof fn === "function" && fn[TOOL]) {
|
|
553
|
+
const opts = fn[TOOL];
|
|
554
|
+
found.set(
|
|
555
|
+
key,
|
|
556
|
+
defineTool(opts.name ?? key, opts, (input, ctx) => instance[key](input, ctx))
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const staticChain = [];
|
|
562
|
+
for (let c = cls; c && c !== Function.prototype; c = Object.getPrototypeOf(c)) staticChain.unshift(c);
|
|
563
|
+
for (const c of staticChain) {
|
|
564
|
+
if (!Object.hasOwn(c, "tools") || !c.tools || typeof c.tools !== "object" || Array.isArray(c.tools))
|
|
565
|
+
continue;
|
|
566
|
+
for (const [key, spec] of Object.entries(c.tools)) {
|
|
567
|
+
const opts = isToolDecorator(spec) ? spec[TOOL_OPTIONS] : spec && typeof spec === "object" && typeof spec.description === "string" ? spec : void 0;
|
|
568
|
+
if (!opts) continue;
|
|
569
|
+
if (typeof opts.execute === "function") {
|
|
570
|
+
found.set(key, defineTool(opts.name ?? key, opts, opts.execute));
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (typeof instance[key] !== "function") {
|
|
574
|
+
throw new ImproperlyConfigured2(`${c.name}.tools.${key}: the class has no method named '${key}'.`);
|
|
575
|
+
}
|
|
576
|
+
found.set(
|
|
577
|
+
key,
|
|
578
|
+
defineTool(opts.name ?? key, opts, (input, ctx) => instance[key](input, ctx))
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return [...found.values()];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/tools/types.ts
|
|
586
|
+
var Toolkit = class {
|
|
587
|
+
};
|
|
588
|
+
function isToolkit(value) {
|
|
589
|
+
return !!value && typeof value.getTools === "function";
|
|
590
|
+
}
|
|
591
|
+
function isAgentTool(value) {
|
|
592
|
+
return !!value && typeof value === "object" && typeof value.name === "string" && typeof value.execute === "function" && typeof value.inputSchema === "object";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/agent.ts
|
|
596
|
+
function deriveName(cls) {
|
|
597
|
+
const base = cls.name.replace(/Agent$/, "") || cls.name || "agent";
|
|
598
|
+
return base.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9_-]+/g, "-").toLowerCase();
|
|
599
|
+
}
|
|
600
|
+
function errorText(error) {
|
|
601
|
+
if (error instanceof Error) return error.message;
|
|
602
|
+
return typeof error === "string" ? error : JSON.stringify(error);
|
|
603
|
+
}
|
|
604
|
+
function toolCallsFrom(content) {
|
|
605
|
+
const calls = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const part of content) {
|
|
607
|
+
if (part.type === "tool-call") {
|
|
608
|
+
calls.set(part.toolCallId, { toolCallId: part.toolCallId, toolName: part.toolName, input: part.input });
|
|
609
|
+
} else if (part.type === "tool-result" || part.type === "tool-error") {
|
|
610
|
+
const rec = calls.get(part.toolCallId) ?? {
|
|
611
|
+
toolCallId: part.toolCallId,
|
|
612
|
+
toolName: part.toolName,
|
|
613
|
+
input: part.input
|
|
614
|
+
};
|
|
615
|
+
if (part.type === "tool-result") rec.output = part.output;
|
|
616
|
+
else rec.error = errorText(part.error);
|
|
617
|
+
calls.set(part.toolCallId, rec);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return [...calls.values()];
|
|
621
|
+
}
|
|
622
|
+
var Agent = class {
|
|
623
|
+
/** Name used for conversations, URLs and MCP (`ask_<name>`); default: class name minus `Agent`, kebab-case. */
|
|
624
|
+
static agentName;
|
|
625
|
+
/** Shown to MCP clients for the `ask_<name>` tool. */
|
|
626
|
+
static description;
|
|
627
|
+
/** Tools implemented by methods of the same name, without decorators: `{ refund: tool({...}) }`. */
|
|
628
|
+
static tools;
|
|
629
|
+
/** `'provider:model-id'` or an AI SDK `LanguageModel` (default `AGENTS.DEFAULT_MODEL`). */
|
|
630
|
+
model;
|
|
631
|
+
/** System instructions, or a function of the run context. */
|
|
632
|
+
instructions;
|
|
633
|
+
/** Toolkits and tools (plus `@tool` methods and `static tools`). */
|
|
634
|
+
tools;
|
|
635
|
+
/** REST permission classes checked for HTTP / MCP access (default `AGENTS.PERMISSION_CLASSES`). */
|
|
636
|
+
permissionClasses;
|
|
637
|
+
/** Maximum model steps (tool round trips) per run (default `AGENTS.MAX_STEPS`, 10). */
|
|
638
|
+
maxSteps;
|
|
639
|
+
temperature;
|
|
640
|
+
maxOutputTokens;
|
|
641
|
+
/** Provider-specific options passed to the AI SDK (`{ anthropic: { thinking: … } }`). */
|
|
642
|
+
providerOptions;
|
|
643
|
+
/** Use conversation memory when the `agents` app is installed (default true). */
|
|
644
|
+
memory = true;
|
|
645
|
+
/** Set by `AgentRouter.register(name, …)`. */
|
|
646
|
+
agentName;
|
|
647
|
+
/** `new this().run(input, opts)`. */
|
|
648
|
+
static run(input, opts) {
|
|
649
|
+
return new this().run(input, opts);
|
|
650
|
+
}
|
|
651
|
+
getName() {
|
|
652
|
+
const cls = this.constructor;
|
|
653
|
+
return this.agentName ?? (Object.hasOwn(cls, "agentName") ? cls.agentName : void 0) ?? deriveName(cls);
|
|
654
|
+
}
|
|
655
|
+
getDescription() {
|
|
656
|
+
const cls = this.constructor;
|
|
657
|
+
return cls.description ?? `Ask the ${this.getName()} agent. It answers in natural language.`;
|
|
658
|
+
}
|
|
659
|
+
getPermissionClasses() {
|
|
660
|
+
return this.permissionClasses ?? defaultPermissionClasses();
|
|
661
|
+
}
|
|
662
|
+
/** System instructions for a run (override for dynamic instructions). */
|
|
663
|
+
async getInstructions(ctx) {
|
|
664
|
+
const i = this.instructions;
|
|
665
|
+
return typeof i === "function" ? await i.call(this, ctx) : i;
|
|
666
|
+
}
|
|
667
|
+
/** All tools of this agent, normalized (toolkits expanded, declared methods bound). */
|
|
668
|
+
getTools() {
|
|
669
|
+
const out = [];
|
|
670
|
+
const add = (entry, key) => {
|
|
671
|
+
if (isToolDecorator(entry)) out.push(standaloneTool(entry, key));
|
|
672
|
+
else if (isAgentTool(entry)) out.push(key && !entry.name ? { ...entry, name: key } : entry);
|
|
673
|
+
else if (isToolkit(entry)) out.push(...entry.getTools());
|
|
674
|
+
else
|
|
675
|
+
throw new ImproperlyConfigured3(
|
|
676
|
+
`${this.constructor.name}.tools: unsupported entry ${key ? `'${key}'` : String(entry)} (use toolkits, tool({...}) or AgentTool objects).`
|
|
677
|
+
);
|
|
678
|
+
};
|
|
679
|
+
const tools = this.tools;
|
|
680
|
+
if (Array.isArray(tools)) for (const t of tools) add(t);
|
|
681
|
+
else if (tools && typeof tools === "object") for (const [k, t] of Object.entries(tools)) add(t, k);
|
|
682
|
+
out.push(...collectDeclaredTools(this));
|
|
683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
684
|
+
for (const t of out) {
|
|
685
|
+
if (seen.has(t.name))
|
|
686
|
+
throw new ImproperlyConfigured3(`${this.constructor.name} has two tools named '${t.name}'.`);
|
|
687
|
+
seen.add(t.name);
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
/** Check `permissionClasses` for `user` (`NotAuthenticated` / `PermissionDenied` from `@ghuri/rest`). */
|
|
692
|
+
async checkPermissions(user, request) {
|
|
693
|
+
const req = request ?? { user, auth: null, method: "POST", successfulAuthenticator: null };
|
|
694
|
+
for (const P of this.getPermissionClasses()) {
|
|
695
|
+
const p = new P();
|
|
696
|
+
if (!await p.hasPermission(req, this)) {
|
|
697
|
+
if (!user.isAuthenticated) throw new NotAuthenticated();
|
|
698
|
+
throw new PermissionDenied(p.message ?? null, p.code ?? null);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/** AI SDK tool set for a run: validation, `onToolCall`, context and error normalization included. */
|
|
703
|
+
buildToolSet(ctx, tools = this.getTools()) {
|
|
704
|
+
const ghuri = tryGetContext6();
|
|
705
|
+
const set = {};
|
|
706
|
+
for (const t of tools) {
|
|
707
|
+
set[t.name] = aiTool({
|
|
708
|
+
description: t.description,
|
|
709
|
+
inputSchema: jsonSchema(t.inputSchema, {
|
|
710
|
+
validate: (value) => {
|
|
711
|
+
try {
|
|
712
|
+
return { success: true, value: t.parse ? t.parse(value) : value };
|
|
713
|
+
} catch (error) {
|
|
714
|
+
return { success: false, error: toToolError(error, t.name) };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}),
|
|
718
|
+
execute: (input, opts) => inContext(ghuri, async () => {
|
|
719
|
+
const toolCtx = {
|
|
720
|
+
user: ctx.user,
|
|
721
|
+
agent: this,
|
|
722
|
+
conversationId: ctx.conversationId,
|
|
723
|
+
toolCallId: opts.toolCallId,
|
|
724
|
+
signal: opts.abortSignal ?? ctx.signal,
|
|
725
|
+
request: ctx.request
|
|
726
|
+
};
|
|
727
|
+
try {
|
|
728
|
+
await this.onToolCall?.({ toolName: t.name, toolCallId: opts.toolCallId, input }, ctx);
|
|
729
|
+
return await t.execute(input, toolCtx);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
throw toToolError(err, t.name);
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return set;
|
|
737
|
+
}
|
|
738
|
+
async prepare(input, opts) {
|
|
739
|
+
const user = opts.user ?? anonymousUser2;
|
|
740
|
+
if (opts.checkPermissions) await this.checkPermissions(user, opts.request);
|
|
741
|
+
const model = await resolveModel(this.model);
|
|
742
|
+
const agentName = this.getName();
|
|
743
|
+
const useMemory = opts.memory ?? (this.memory !== false && memoryAvailable());
|
|
744
|
+
let conversation;
|
|
745
|
+
let history = [];
|
|
746
|
+
if (opts.conversationId) {
|
|
747
|
+
if (!memoryAvailable()) {
|
|
748
|
+
throw new ImproperlyConfigured3(
|
|
749
|
+
"conversationId requires the agents app: add AgentsConfig to INSTALLED_APPS."
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
conversation = await getConversation(agentName, opts.conversationId, user);
|
|
753
|
+
history = await loadMessages(conversation);
|
|
754
|
+
} else if (useMemory) {
|
|
755
|
+
conversation = await createConversation(agentName, user, { title: opts.title ?? titleFrom(input) });
|
|
756
|
+
}
|
|
757
|
+
const newMessages = typeof input === "string" ? [{ role: "user", content: input }] : [...input];
|
|
758
|
+
const ctx = {
|
|
759
|
+
agent: this,
|
|
760
|
+
agentName,
|
|
761
|
+
user,
|
|
762
|
+
input,
|
|
763
|
+
conversation,
|
|
764
|
+
conversationId: conversation ? String(conversation.pk) : void 0,
|
|
765
|
+
messages: [...history, ...newMessages],
|
|
766
|
+
newMessages,
|
|
767
|
+
signal: opts.signal,
|
|
768
|
+
request: opts.request,
|
|
769
|
+
extra: opts.context ?? {}
|
|
770
|
+
};
|
|
771
|
+
await this.beforeRun?.(ctx);
|
|
772
|
+
const instructions = await this.getInstructions(ctx);
|
|
773
|
+
return { ctx, model, instructions, tools: this.buildToolSet(ctx), ghuri: tryGetContext6() };
|
|
774
|
+
}
|
|
775
|
+
callSettings(p) {
|
|
776
|
+
return {
|
|
777
|
+
model: p.model,
|
|
778
|
+
...p.instructions ? { instructions: p.instructions } : {},
|
|
779
|
+
messages: p.ctx.messages,
|
|
780
|
+
tools: p.tools,
|
|
781
|
+
stopWhen: stepCountIs(this.maxSteps ?? agentsSettings().MAX_STEPS ?? DEFAULT_MAX_STEPS),
|
|
782
|
+
...this.temperature !== void 0 ? { temperature: this.temperature } : {},
|
|
783
|
+
...this.maxOutputTokens !== void 0 ? { maxOutputTokens: this.maxOutputTokens } : {},
|
|
784
|
+
...this.providerOptions ? { providerOptions: this.providerOptions } : {},
|
|
785
|
+
...p.ctx.signal ? { abortSignal: p.ctx.signal } : {}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
async finish(p, r) {
|
|
789
|
+
const messages = [...p.ctx.newMessages, ...r.responseMessages];
|
|
790
|
+
if (p.ctx.conversation) await saveMessages(p.ctx.conversation, messages);
|
|
791
|
+
const result = {
|
|
792
|
+
text: r.text,
|
|
793
|
+
messages,
|
|
794
|
+
toolCalls: toolCallsFrom(r.content),
|
|
795
|
+
usage: r.usage,
|
|
796
|
+
finishReason: r.finishReason,
|
|
797
|
+
steps: r.steps,
|
|
798
|
+
conversationId: p.ctx.conversationId
|
|
799
|
+
};
|
|
800
|
+
await this.afterRun?.(result, p.ctx);
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Run the agent to completion (tool loop included). Tool failures (permission denied, validation, not
|
|
805
|
+
* found) are returned to the model as tool errors; the run itself only throws for model / config errors.
|
|
806
|
+
*/
|
|
807
|
+
async run(input, opts = {}) {
|
|
808
|
+
const p = await this.prepare(input, opts);
|
|
809
|
+
const res = await generateText(this.callSettings(p));
|
|
810
|
+
return this.finish(p, {
|
|
811
|
+
text: res.text,
|
|
812
|
+
responseMessages: res.responseMessages,
|
|
813
|
+
content: res.content,
|
|
814
|
+
usage: res.usage,
|
|
815
|
+
finishReason: res.finishReason,
|
|
816
|
+
steps: res.steps.length
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Run in the background through a Cloudflare Queue (`AGENTS.QUEUE_BINDING`); a consumer registered with
|
|
821
|
+
* `AgentRouter.consumeQueue()` performs the run. Returns the conversation id to poll.
|
|
822
|
+
*/
|
|
823
|
+
async enqueue(input, opts = {}) {
|
|
824
|
+
return enqueueRun(this, input, opts);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Stream a run. Use `toUIMessageStreamResponse()` for AI SDK UI (`useChat`) / SSE clients, or `textStream`.
|
|
828
|
+
* The conversation is saved when the stream finishes; `await stream.done` for the final result.
|
|
829
|
+
*/
|
|
830
|
+
async stream(input, opts = {}) {
|
|
831
|
+
const p = await this.prepare(input, opts);
|
|
832
|
+
const result = streamText(this.callSettings(p));
|
|
833
|
+
const done = (async () => {
|
|
834
|
+
const [text, responseMessages, content, usage, finishReason, steps] = await Promise.all([
|
|
835
|
+
result.text,
|
|
836
|
+
result.responseMessages,
|
|
837
|
+
result.content,
|
|
838
|
+
result.usage,
|
|
839
|
+
result.finishReason,
|
|
840
|
+
result.steps
|
|
841
|
+
]);
|
|
842
|
+
return inContext(
|
|
843
|
+
p.ghuri,
|
|
844
|
+
() => this.finish(p, {
|
|
845
|
+
text,
|
|
846
|
+
responseMessages,
|
|
847
|
+
content,
|
|
848
|
+
usage,
|
|
849
|
+
finishReason,
|
|
850
|
+
steps: steps.length
|
|
851
|
+
})
|
|
852
|
+
);
|
|
853
|
+
})();
|
|
854
|
+
done.catch(() => {
|
|
855
|
+
});
|
|
856
|
+
p.ghuri?.executionCtx?.waitUntil(done.catch(() => {
|
|
857
|
+
}));
|
|
858
|
+
return new AgentStream(result, done, p.ctx.conversationId);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
var AgentStream = class {
|
|
862
|
+
constructor(result, done, conversationId) {
|
|
863
|
+
this.result = result;
|
|
864
|
+
this.done = done;
|
|
865
|
+
this.conversationId = conversationId;
|
|
866
|
+
}
|
|
867
|
+
result;
|
|
868
|
+
done;
|
|
869
|
+
conversationId;
|
|
870
|
+
/** Plain text deltas. */
|
|
871
|
+
get textStream() {
|
|
872
|
+
return this.result.textStream;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* AI SDK UI message stream chunks (`start`, `text-delta`, `tool-input-available`, `tool-output-available`,
|
|
876
|
+
* `finish`…). The `start` chunk carries `messageMetadata: { conversationId }`. The stream closes only after
|
|
877
|
+
* the conversation was saved.
|
|
878
|
+
*/
|
|
879
|
+
toUIMessageStream() {
|
|
880
|
+
const conversationId = this.conversationId;
|
|
881
|
+
const done = this.done;
|
|
882
|
+
const stream = this.result.toUIMessageStream({
|
|
883
|
+
messageMetadata: ({ part }) => part.type === "start" && conversationId ? { conversationId } : void 0,
|
|
884
|
+
onError: (error) => errorText(error)
|
|
885
|
+
});
|
|
886
|
+
return stream.pipeThrough(
|
|
887
|
+
new TransformStream({
|
|
888
|
+
async flush(controller) {
|
|
889
|
+
try {
|
|
890
|
+
await done;
|
|
891
|
+
} catch (err) {
|
|
892
|
+
controller.enqueue({ type: "error", errorText: errorText(err) });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
/** An SSE `Response` in the AI SDK UI message stream protocol (header `x-conversation-id`). */
|
|
899
|
+
toUIMessageStreamResponse(init = {}) {
|
|
900
|
+
return createUIMessageStreamResponse({
|
|
901
|
+
status: init.status,
|
|
902
|
+
headers: {
|
|
903
|
+
...this.conversationId ? { "x-conversation-id": this.conversationId } : {},
|
|
904
|
+
...init.headers ?? {}
|
|
905
|
+
},
|
|
906
|
+
stream: this.toUIMessageStream()
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// src/apps.ts
|
|
912
|
+
import { AppConfig, tryGetContext as tryGetContext7 } from "@ghuri/core";
|
|
913
|
+
import { Migration as Migration2, migrationsFromModule } from "@ghuri/orm/migrations";
|
|
914
|
+
|
|
915
|
+
// src/migrations/index.ts
|
|
916
|
+
var migrations_exports = {};
|
|
917
|
+
__export(migrations_exports, {
|
|
918
|
+
m0001_initial: () => initial_default,
|
|
919
|
+
migrations: () => migrations
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// src/migrations/0001_initial.ts
|
|
923
|
+
import { Migration, operations as ops } from "@ghuri/orm/migrations";
|
|
924
|
+
var initial_default = new Migration({
|
|
925
|
+
initial: true,
|
|
926
|
+
dependencies: [["auth", "0001_initial"]],
|
|
927
|
+
operations: [
|
|
928
|
+
ops.CreateModel({
|
|
929
|
+
name: "Conversation",
|
|
930
|
+
fields: {
|
|
931
|
+
id: {
|
|
932
|
+
type: "UUIDField",
|
|
933
|
+
primaryKey: true,
|
|
934
|
+
default: { callable: "newConversationId" },
|
|
935
|
+
editable: false
|
|
936
|
+
},
|
|
937
|
+
user: {
|
|
938
|
+
type: "ForeignKey",
|
|
939
|
+
to: "auth.User",
|
|
940
|
+
onDelete: "CASCADE",
|
|
941
|
+
null: true,
|
|
942
|
+
blank: true,
|
|
943
|
+
dbIndex: true,
|
|
944
|
+
relatedName: "agentConversations"
|
|
945
|
+
},
|
|
946
|
+
agent: { type: "CharField", maxLength: 100, dbIndex: true },
|
|
947
|
+
title: { type: "CharField", maxLength: 200, blank: true, default: "" },
|
|
948
|
+
metadata: { type: "JSONField", blank: true, default: { callable: "emptyMetadata" } },
|
|
949
|
+
createdAt: { type: "DateTimeField", autoNowAdd: true },
|
|
950
|
+
updatedAt: { type: "DateTimeField", autoNow: true }
|
|
951
|
+
},
|
|
952
|
+
options: { ordering: ["-updatedAt"], verboseName: "conversation" }
|
|
953
|
+
}),
|
|
954
|
+
ops.CreateModel({
|
|
955
|
+
name: "Message",
|
|
956
|
+
fields: {
|
|
957
|
+
id: { type: "BigAutoField", primaryKey: true },
|
|
958
|
+
conversation: {
|
|
959
|
+
type: "ForeignKey",
|
|
960
|
+
to: "agents.Conversation",
|
|
961
|
+
onDelete: "CASCADE",
|
|
962
|
+
dbIndex: true,
|
|
963
|
+
relatedName: "messages"
|
|
964
|
+
},
|
|
965
|
+
role: {
|
|
966
|
+
type: "CharField",
|
|
967
|
+
maxLength: 20,
|
|
968
|
+
choices: [
|
|
969
|
+
["system", "system"],
|
|
970
|
+
["user", "user"],
|
|
971
|
+
["assistant", "assistant"],
|
|
972
|
+
["tool", "tool"]
|
|
973
|
+
]
|
|
974
|
+
},
|
|
975
|
+
content: { type: "JSONField", null: true },
|
|
976
|
+
createdAt: { type: "DateTimeField", autoNowAdd: true }
|
|
977
|
+
},
|
|
978
|
+
options: { ordering: ["id"], verboseName: "message" }
|
|
979
|
+
})
|
|
980
|
+
]
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// src/migrations/index.ts
|
|
984
|
+
var migrations = {
|
|
985
|
+
"0001_initial": initial_default
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// src/apps.ts
|
|
989
|
+
function migrationsForUserModel() {
|
|
990
|
+
const label = tryGetContext7()?.settings.AUTH_USER_MODEL;
|
|
991
|
+
if (!label || label.toLowerCase() === "auth.user") return migrations_exports;
|
|
992
|
+
const userApp = label.split(".")[0];
|
|
993
|
+
const out = {};
|
|
994
|
+
for (const [name, m] of migrationsFromModule(migrations_exports) ?? []) {
|
|
995
|
+
const operations = m.operations.map((op) => {
|
|
996
|
+
if (!op.fields) return op;
|
|
997
|
+
const fields = Object.fromEntries(
|
|
998
|
+
Object.entries(op.fields).map(([k, f]) => {
|
|
999
|
+
const to = f?.options?.to ?? f?.to;
|
|
1000
|
+
if (typeof to !== "string" || to.toLowerCase() !== "auth.user") return [k, f];
|
|
1001
|
+
return [k, f.options ? { ...f, options: { ...f.options, to: label } } : { ...f, to: label }];
|
|
1002
|
+
})
|
|
1003
|
+
);
|
|
1004
|
+
return Object.assign(Object.create(Object.getPrototypeOf(op)), op, { fields });
|
|
1005
|
+
});
|
|
1006
|
+
const dependencies = m.dependencies.filter((d) => d[0] !== "auth");
|
|
1007
|
+
if (!dependencies.some((d) => d[0] === userApp)) dependencies.push([userApp, "__first__"]);
|
|
1008
|
+
const adapted = new Migration2({ dependencies, operations, initial: m.initial });
|
|
1009
|
+
adapted.name = m.name || name;
|
|
1010
|
+
out[adapted.name] = adapted;
|
|
1011
|
+
}
|
|
1012
|
+
return { migrations: out };
|
|
1013
|
+
}
|
|
1014
|
+
function appPath() {
|
|
1015
|
+
const meta = import.meta;
|
|
1016
|
+
if (!meta.dirname || !/[\\/]src$/.test(meta.dirname) || !/\.[cm]?ts$/.test(meta.filename ?? ""))
|
|
1017
|
+
return void 0;
|
|
1018
|
+
return meta.dirname;
|
|
1019
|
+
}
|
|
1020
|
+
var AgentsConfig = class extends AppConfig {
|
|
1021
|
+
name = "@ghuri/agents";
|
|
1022
|
+
label = "agents";
|
|
1023
|
+
verboseName = "Agents";
|
|
1024
|
+
path = appPath();
|
|
1025
|
+
migrations = migrationsForUserModel();
|
|
1026
|
+
models = { Conversation, Message };
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// src/http.ts
|
|
1030
|
+
import { ImproperlyConfigured as ImproperlyConfigured5, path as path2, tryGetContext as tryGetContext9 } from "@ghuri/core";
|
|
1031
|
+
import {
|
|
1032
|
+
APIException as APIException2,
|
|
1033
|
+
APIResponse as APIResponse2,
|
|
1034
|
+
APIView as APIView2,
|
|
1035
|
+
JSONRenderer as JSONRenderer2,
|
|
1036
|
+
NotAuthenticated as NotAuthenticated2,
|
|
1037
|
+
NotFound,
|
|
1038
|
+
ValidationError as RestValidationError
|
|
1039
|
+
} from "@ghuri/rest";
|
|
1040
|
+
|
|
1041
|
+
// src/mcp.ts
|
|
1042
|
+
import { getHost, ImproperlyConfigured as ImproperlyConfigured4, path, tryGetContext as tryGetContext8 } from "@ghuri/core";
|
|
1043
|
+
import {
|
|
1044
|
+
APIResponse,
|
|
1045
|
+
APIView,
|
|
1046
|
+
JSONRenderer
|
|
1047
|
+
} from "@ghuri/rest";
|
|
1048
|
+
var MCP_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1049
|
+
var MCP_LATEST_PROTOCOL_VERSION = MCP_PROTOCOL_VERSIONS[0];
|
|
1050
|
+
var JSONRPC = {
|
|
1051
|
+
PARSE_ERROR: -32700,
|
|
1052
|
+
INVALID_REQUEST: -32600,
|
|
1053
|
+
METHOD_NOT_FOUND: -32601,
|
|
1054
|
+
INVALID_PARAMS: -32602,
|
|
1055
|
+
INTERNAL_ERROR: -32603,
|
|
1056
|
+
/** Server-defined: authentication / authorization failures. */
|
|
1057
|
+
UNAUTHORIZED: -32001
|
|
1058
|
+
};
|
|
1059
|
+
function isAgentClass(source) {
|
|
1060
|
+
return source === Agent || source.prototype instanceof Agent;
|
|
1061
|
+
}
|
|
1062
|
+
function agentFactory(source) {
|
|
1063
|
+
return isAgentClass(source) ? () => new source() : source;
|
|
1064
|
+
}
|
|
1065
|
+
function isPlainObject(v) {
|
|
1066
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1067
|
+
}
|
|
1068
|
+
function errorResponse(id, code, message, data) {
|
|
1069
|
+
return { jsonrpc: "2.0", id, error: { code, message, ...data !== void 0 ? { data } : {} } };
|
|
1070
|
+
}
|
|
1071
|
+
function asJson(value) {
|
|
1072
|
+
return value === void 0 ? null : JSON.parse(JSON.stringify(value));
|
|
1073
|
+
}
|
|
1074
|
+
var McpServer = class {
|
|
1075
|
+
opts;
|
|
1076
|
+
constructor(opts = {}) {
|
|
1077
|
+
this.opts = opts;
|
|
1078
|
+
}
|
|
1079
|
+
/** All exposed tools (toolkits expanded, agents as `ask_<name>`). */
|
|
1080
|
+
getTools() {
|
|
1081
|
+
const out = [];
|
|
1082
|
+
const add = (entry, key) => {
|
|
1083
|
+
if (isToolDecorator(entry)) out.push(standaloneTool(entry, key));
|
|
1084
|
+
else if (isAgentTool(entry)) out.push(entry);
|
|
1085
|
+
else if (isToolkit(entry)) out.push(...entry.getTools());
|
|
1086
|
+
else throw new TypeError(`MCP tools: unsupported entry ${key ? `'${key}'` : String(entry)}.`);
|
|
1087
|
+
};
|
|
1088
|
+
const tools = this.opts.tools;
|
|
1089
|
+
if (Array.isArray(tools)) for (const t of tools) add(t);
|
|
1090
|
+
else if (tools) for (const [k, t] of Object.entries(tools)) add(t, k);
|
|
1091
|
+
for (const [name, factory] of Object.entries(this.opts.agents ?? {})) out.push(agentTool(name, factory));
|
|
1092
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1093
|
+
for (const t of out) {
|
|
1094
|
+
if (seen.has(t.name)) throw new ImproperlyConfigured4(`MCP server has two tools named '${t.name}'.`);
|
|
1095
|
+
seen.add(t.name);
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
/** `tools/list` entries. */
|
|
1100
|
+
listTools() {
|
|
1101
|
+
return this.getTools().map((t) => ({
|
|
1102
|
+
name: t.name,
|
|
1103
|
+
...t.annotations?.title ? { title: t.annotations.title } : {},
|
|
1104
|
+
description: t.description,
|
|
1105
|
+
inputSchema: t.inputSchema,
|
|
1106
|
+
...t.outputSchema ? { outputSchema: t.outputSchema } : {},
|
|
1107
|
+
...t.annotations ? { annotations: t.annotations } : {}
|
|
1108
|
+
}));
|
|
1109
|
+
}
|
|
1110
|
+
/** `tools/call`: run a tool as `ctx.user`. Tool failures are results with `isError: true`. */
|
|
1111
|
+
async callTool(name, args, ctx) {
|
|
1112
|
+
const tool2 = this.getTools().find((t) => t.name === name);
|
|
1113
|
+
if (!tool2) throw new McpError(JSONRPC.INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
1114
|
+
try {
|
|
1115
|
+
const out = await runTool(tool2, args, { user: ctx.user, request: ctx.request, signal: ctx.signal });
|
|
1116
|
+
if (typeof out === "string") return { content: [{ type: "text", text: out }], isError: false };
|
|
1117
|
+
const json = asJson(out);
|
|
1118
|
+
return {
|
|
1119
|
+
content: [{ type: "text", text: JSON.stringify(json) }],
|
|
1120
|
+
structuredContent: isPlainObject(json) ? json : { result: json },
|
|
1121
|
+
isError: false
|
|
1122
|
+
};
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
const e = err instanceof ToolError ? err : new ToolError(String(err));
|
|
1125
|
+
const detail = e.detail !== void 0 ? `
|
|
1126
|
+
${JSON.stringify(e.detail)}` : "";
|
|
1127
|
+
return { content: [{ type: "text", text: `${e.message}${detail}` }], isError: true };
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** `initialize` result. */
|
|
1131
|
+
initializeResult(params) {
|
|
1132
|
+
const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
|
|
1133
|
+
const protocolVersion = requested && MCP_PROTOCOL_VERSIONS.includes(requested) ? requested : MCP_LATEST_PROTOCOL_VERSION;
|
|
1134
|
+
return {
|
|
1135
|
+
protocolVersion,
|
|
1136
|
+
capabilities: { tools: { listChanged: false } },
|
|
1137
|
+
serverInfo: {
|
|
1138
|
+
name: this.opts.name ?? "ghuri",
|
|
1139
|
+
version: this.opts.version ?? "0.1.0",
|
|
1140
|
+
...this.opts.title ? { title: this.opts.title } : {}
|
|
1141
|
+
},
|
|
1142
|
+
...this.opts.instructions ? { instructions: this.opts.instructions } : {}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
/** Handle one JSON-RPC message; `null` for notifications and client responses (nothing to send). */
|
|
1146
|
+
async handleMessage(message, ctx) {
|
|
1147
|
+
if (!isPlainObject(message) || message.jsonrpc !== "2.0") {
|
|
1148
|
+
const id2 = isPlainObject(message) && isValidId(message.id) ? message.id : null;
|
|
1149
|
+
return errorResponse(id2, JSONRPC.INVALID_REQUEST, "Invalid Request");
|
|
1150
|
+
}
|
|
1151
|
+
const hasId = "id" in message && message.id !== void 0;
|
|
1152
|
+
if (typeof message.method !== "string") {
|
|
1153
|
+
if (("result" in message || "error" in message) && hasId) return null;
|
|
1154
|
+
return errorResponse(
|
|
1155
|
+
isValidId(message.id) ? message.id : null,
|
|
1156
|
+
JSONRPC.INVALID_REQUEST,
|
|
1157
|
+
"Invalid Request"
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
if (!hasId) return null;
|
|
1161
|
+
if (!isValidId(message.id) || message.id === null)
|
|
1162
|
+
return errorResponse(null, JSONRPC.INVALID_REQUEST, "Invalid Request: id must be a string or number");
|
|
1163
|
+
const id = message.id;
|
|
1164
|
+
const params = message.params === void 0 ? {} : message.params;
|
|
1165
|
+
if (!isPlainObject(params)) return errorResponse(id, JSONRPC.INVALID_PARAMS, "params must be an object");
|
|
1166
|
+
try {
|
|
1167
|
+
const result = await this.dispatch(message.method, params, ctx);
|
|
1168
|
+
return { jsonrpc: "2.0", id, result };
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
if (err instanceof McpError) return errorResponse(id, err.code, err.message, err.data);
|
|
1171
|
+
console.error("[ghuri.agents] MCP request failed:", err);
|
|
1172
|
+
const debug = !!tryGetContext8()?.settings.DEBUG;
|
|
1173
|
+
return errorResponse(
|
|
1174
|
+
id,
|
|
1175
|
+
JSONRPC.INTERNAL_ERROR,
|
|
1176
|
+
debug && err instanceof Error ? err.message : "Internal error"
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async dispatch(method, params, ctx) {
|
|
1181
|
+
switch (method) {
|
|
1182
|
+
case "initialize":
|
|
1183
|
+
return this.initializeResult(params);
|
|
1184
|
+
case "ping":
|
|
1185
|
+
return {};
|
|
1186
|
+
case "tools/list":
|
|
1187
|
+
return { tools: this.listTools() };
|
|
1188
|
+
case "tools/call": {
|
|
1189
|
+
if (typeof params.name !== "string")
|
|
1190
|
+
throw new McpError(JSONRPC.INVALID_PARAMS, "tools/call requires a tool name");
|
|
1191
|
+
return this.callTool(params.name, params.arguments ?? {}, ctx);
|
|
1192
|
+
}
|
|
1193
|
+
case "resources/list":
|
|
1194
|
+
return { resources: [] };
|
|
1195
|
+
case "resources/templates/list":
|
|
1196
|
+
return { resourceTemplates: [] };
|
|
1197
|
+
case "prompts/list":
|
|
1198
|
+
return { prompts: [] };
|
|
1199
|
+
case "logging/setLevel":
|
|
1200
|
+
return {};
|
|
1201
|
+
default:
|
|
1202
|
+
throw new McpError(JSONRPC.METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/** Handle a parsed POST body (single message or batch). Returns the HTTP status and JSON body (or none). */
|
|
1206
|
+
async handleBody(body, ctx) {
|
|
1207
|
+
if (Array.isArray(body)) {
|
|
1208
|
+
if (!body.length)
|
|
1209
|
+
return { status: 400, body: errorResponse(null, JSONRPC.INVALID_REQUEST, "Empty batch") };
|
|
1210
|
+
const responses = [];
|
|
1211
|
+
for (const m of body) {
|
|
1212
|
+
const r2 = await this.handleMessage(m, ctx);
|
|
1213
|
+
if (r2) responses.push(r2);
|
|
1214
|
+
}
|
|
1215
|
+
return responses.length ? { status: 200, body: responses } : { status: 202 };
|
|
1216
|
+
}
|
|
1217
|
+
const r = await this.handleMessage(body, ctx);
|
|
1218
|
+
if (!r) return { status: 202 };
|
|
1219
|
+
return { status: 200, body: r };
|
|
1220
|
+
}
|
|
1221
|
+
/** The Streamable HTTP endpoint view. */
|
|
1222
|
+
asView() {
|
|
1223
|
+
return McpView.asView({ mcpServer: this });
|
|
1224
|
+
}
|
|
1225
|
+
/** URL patterns: mount with `path('mcp/', server.urls)`. */
|
|
1226
|
+
get urls() {
|
|
1227
|
+
return [path("", this.asView(), { name: "mcp" })];
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
var McpError = class extends Error {
|
|
1231
|
+
constructor(code, message, data) {
|
|
1232
|
+
super(message);
|
|
1233
|
+
this.code = code;
|
|
1234
|
+
this.data = data;
|
|
1235
|
+
}
|
|
1236
|
+
code;
|
|
1237
|
+
data;
|
|
1238
|
+
};
|
|
1239
|
+
function isValidId(id) {
|
|
1240
|
+
return id === null || typeof id === "string" || typeof id === "number" && Number.isFinite(id);
|
|
1241
|
+
}
|
|
1242
|
+
function agentTool(name, source) {
|
|
1243
|
+
const factory = agentFactory(source);
|
|
1244
|
+
const probe = factory();
|
|
1245
|
+
return {
|
|
1246
|
+
name: sanitizeToolName(`ask_${name}`),
|
|
1247
|
+
description: probe.getDescription(),
|
|
1248
|
+
inputSchema: {
|
|
1249
|
+
type: "object",
|
|
1250
|
+
properties: {
|
|
1251
|
+
message: { type: "string", description: "Your message to the agent." },
|
|
1252
|
+
conversationId: {
|
|
1253
|
+
type: "string",
|
|
1254
|
+
description: "Continue a previous conversation (the conversationId returned by an earlier call)."
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
required: ["message"]
|
|
1258
|
+
},
|
|
1259
|
+
annotations: { title: `Ask ${name}`, readOnlyHint: false, openWorldHint: false },
|
|
1260
|
+
parse(input) {
|
|
1261
|
+
if (!isPlainObject(input) || typeof input.message !== "string" || !input.message.trim())
|
|
1262
|
+
throw new ToolError("'message' is required.", { status: 400 });
|
|
1263
|
+
if (input.conversationId !== void 0 && typeof input.conversationId !== "string")
|
|
1264
|
+
throw new ToolError("'conversationId' must be a string.", { status: 400 });
|
|
1265
|
+
return input;
|
|
1266
|
+
},
|
|
1267
|
+
async execute(input, ctx) {
|
|
1268
|
+
const agent = factory();
|
|
1269
|
+
await agent.checkPermissions(ctx.user, ctx.request);
|
|
1270
|
+
const r = await agent.run(input.message, {
|
|
1271
|
+
user: ctx.user,
|
|
1272
|
+
conversationId: input.conversationId ?? null,
|
|
1273
|
+
request: ctx.request,
|
|
1274
|
+
signal: ctx.signal
|
|
1275
|
+
});
|
|
1276
|
+
return { text: r.text, ...r.conversationId ? { conversationId: r.conversationId } : {} };
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function sameOrigin(request, origin) {
|
|
1281
|
+
let url;
|
|
1282
|
+
try {
|
|
1283
|
+
url = new URL(origin);
|
|
1284
|
+
} catch {
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
const host = getHost(request.c);
|
|
1288
|
+
if (url.host === host) return true;
|
|
1289
|
+
const trusted = tryGetContext8()?.settings.CSRF_TRUSTED_ORIGINS ?? [];
|
|
1290
|
+
return trusted.some(
|
|
1291
|
+
(t) => t === origin || t.includes("*") && new RegExp(`^${t.replace(/[.]/g, "\\.").replace("*", "[^/]+")}$`).test(origin)
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
var McpView = class extends APIView {
|
|
1295
|
+
rendererClasses = [JSONRenderer];
|
|
1296
|
+
getAuthenticators() {
|
|
1297
|
+
const classes = this.mcpServer.opts.authenticationClasses ?? agentsSettings().AUTHENTICATION_CLASSES;
|
|
1298
|
+
return classes ? classes.map((A) => new A()) : super.getAuthenticators();
|
|
1299
|
+
}
|
|
1300
|
+
getPermissions() {
|
|
1301
|
+
return (this.mcpServer.opts.permissionClasses ?? defaultPermissionClasses()).map((P) => new P());
|
|
1302
|
+
}
|
|
1303
|
+
performContentNegotiation(request, _force = false) {
|
|
1304
|
+
return super.performContentNegotiation(request, true);
|
|
1305
|
+
}
|
|
1306
|
+
/** Authentication / permission failures keep their HTTP status (401 + WWW-Authenticate) with a JSON-RPC body. */
|
|
1307
|
+
async handleException(exc) {
|
|
1308
|
+
const res = await super.handleException(exc);
|
|
1309
|
+
const detail = res.data?.detail;
|
|
1310
|
+
res.data = errorResponse(
|
|
1311
|
+
null,
|
|
1312
|
+
res.status === 401 || res.status === 403 ? JSONRPC.UNAUTHORIZED : JSONRPC.INTERNAL_ERROR,
|
|
1313
|
+
detail !== void 0 && detail !== null ? String(detail) : "Request failed"
|
|
1314
|
+
);
|
|
1315
|
+
return res;
|
|
1316
|
+
}
|
|
1317
|
+
async post(request) {
|
|
1318
|
+
const origin = request.header("origin");
|
|
1319
|
+
if (origin && !this.mcpServer.opts.allowAnyOrigin && !sameOrigin(request, origin)) {
|
|
1320
|
+
return new APIResponse(errorResponse(null, JSONRPC.INVALID_REQUEST, "Origin not allowed"), {
|
|
1321
|
+
status: 403
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
const version = request.header("mcp-protocol-version");
|
|
1325
|
+
if (version && !MCP_PROTOCOL_VERSIONS.includes(version)) {
|
|
1326
|
+
return new APIResponse(
|
|
1327
|
+
errorResponse(null, JSONRPC.INVALID_REQUEST, `Unsupported MCP-Protocol-Version: ${version}`),
|
|
1328
|
+
{ status: 400 }
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
let body;
|
|
1332
|
+
try {
|
|
1333
|
+
body = request.data;
|
|
1334
|
+
} catch {
|
|
1335
|
+
return new APIResponse(errorResponse(null, JSONRPC.PARSE_ERROR, "Parse error"), { status: 400 });
|
|
1336
|
+
}
|
|
1337
|
+
if (body === void 0 || isPlainObject(body) && !Object.keys(body).length) {
|
|
1338
|
+
return new APIResponse(errorResponse(null, JSONRPC.INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
1339
|
+
}
|
|
1340
|
+
const { status, body: out } = await this.mcpServer.handleBody(body, {
|
|
1341
|
+
user: request.user,
|
|
1342
|
+
request,
|
|
1343
|
+
signal: request.raw.signal
|
|
1344
|
+
});
|
|
1345
|
+
if (out === void 0) return new Response(null, { status });
|
|
1346
|
+
return new APIResponse(out, { status });
|
|
1347
|
+
}
|
|
1348
|
+
async get() {
|
|
1349
|
+
return new Response(null, { status: 405, headers: { Allow: "POST" } });
|
|
1350
|
+
}
|
|
1351
|
+
async delete() {
|
|
1352
|
+
return new Response(null, { status: 405, headers: { Allow: "POST" } });
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/http.ts
|
|
1357
|
+
var ModelUnavailable = class extends APIException2 {
|
|
1358
|
+
static statusCode = 503;
|
|
1359
|
+
static defaultDetail = "The agent's language model is not available.";
|
|
1360
|
+
static defaultCode = "model_unavailable";
|
|
1361
|
+
};
|
|
1362
|
+
function agentAuthenticationClasses() {
|
|
1363
|
+
return agentsSettings().AUTHENTICATION_CLASSES;
|
|
1364
|
+
}
|
|
1365
|
+
var AgentBaseView = class extends APIView2 {
|
|
1366
|
+
_agent;
|
|
1367
|
+
rendererClasses = [JSONRenderer2];
|
|
1368
|
+
/** The agent instance serving this request. */
|
|
1369
|
+
get agent() {
|
|
1370
|
+
this._agent ??= this.agentRouter.create(this.agentName);
|
|
1371
|
+
return this._agent;
|
|
1372
|
+
}
|
|
1373
|
+
getAuthenticators() {
|
|
1374
|
+
const classes = this.authenticationClasses ?? agentAuthenticationClasses();
|
|
1375
|
+
return classes ? classes.map((A) => new A()) : super.getAuthenticators();
|
|
1376
|
+
}
|
|
1377
|
+
getPermissions() {
|
|
1378
|
+
if (this.permissionClasses) return super.getPermissions();
|
|
1379
|
+
return this.agent.getPermissionClasses().map((P) => new P());
|
|
1380
|
+
}
|
|
1381
|
+
// Never 406: SSE clients send `Accept: text/event-stream`; errors are rendered as JSON.
|
|
1382
|
+
performContentNegotiation(request, _force = false) {
|
|
1383
|
+
return super.performContentNegotiation(request, true);
|
|
1384
|
+
}
|
|
1385
|
+
requireMemory() {
|
|
1386
|
+
if (!memoryAvailable()) throw new NotFound("Conversation memory is not enabled (install AgentsConfig).");
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
function wantsStream(request) {
|
|
1390
|
+
const q = request.queryParams.get("stream");
|
|
1391
|
+
if (q !== null) return ["1", "true", "yes"].includes(q.toLowerCase());
|
|
1392
|
+
return (request.header("accept") ?? "").includes("text/event-stream");
|
|
1393
|
+
}
|
|
1394
|
+
function lastUserText(messages) {
|
|
1395
|
+
if (!Array.isArray(messages)) return void 0;
|
|
1396
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1397
|
+
const m = messages[i];
|
|
1398
|
+
if (m?.role !== "user") continue;
|
|
1399
|
+
if (typeof m.content === "string") return m.content;
|
|
1400
|
+
if (Array.isArray(m.parts))
|
|
1401
|
+
return m.parts.filter((p) => p?.type === "text").map((p) => p.text).join("");
|
|
1402
|
+
}
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
var AgentChatView = class extends AgentBaseView {
|
|
1406
|
+
async post(request) {
|
|
1407
|
+
const body = request.data ?? {};
|
|
1408
|
+
const message = typeof body.message === "string" ? body.message : lastUserText(body.messages);
|
|
1409
|
+
if (typeof message !== "string" || !message.trim())
|
|
1410
|
+
throw new RestValidationError({ message: ["This field is required."] });
|
|
1411
|
+
const conversationId = body.conversationId ?? null;
|
|
1412
|
+
if (conversationId !== null && typeof conversationId !== "string")
|
|
1413
|
+
throw new RestValidationError({ conversationId: ["Must be a string."] });
|
|
1414
|
+
const input = message;
|
|
1415
|
+
const opts = { user: request.user, conversationId, request };
|
|
1416
|
+
try {
|
|
1417
|
+
if (wantsStream(request)) {
|
|
1418
|
+
const stream = await this.agent.stream(input, opts);
|
|
1419
|
+
return stream.toUIMessageStreamResponse();
|
|
1420
|
+
}
|
|
1421
|
+
const r = await this.agent.run(input, opts);
|
|
1422
|
+
return new APIResponse2({
|
|
1423
|
+
conversationId: r.conversationId ?? null,
|
|
1424
|
+
text: r.text,
|
|
1425
|
+
toolCalls: r.toolCalls,
|
|
1426
|
+
usage: r.usage,
|
|
1427
|
+
finishReason: r.finishReason
|
|
1428
|
+
});
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (err instanceof ConversationNotFound) throw new NotFound(err.message);
|
|
1431
|
+
if (err instanceof ModelResolutionError)
|
|
1432
|
+
throw new ModelUnavailable(tryGetContext9()?.settings.DEBUG ? err.message : void 0);
|
|
1433
|
+
throw err;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
function conversationJSON(c) {
|
|
1438
|
+
return {
|
|
1439
|
+
id: String(c.pk),
|
|
1440
|
+
agent: c.agent,
|
|
1441
|
+
title: c.title,
|
|
1442
|
+
metadata: c.metadata ?? {},
|
|
1443
|
+
createdAt: c.createdAt,
|
|
1444
|
+
updatedAt: c.updatedAt
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
var ConversationListView = class extends AgentBaseView {
|
|
1448
|
+
async get(request) {
|
|
1449
|
+
this.requireMemory();
|
|
1450
|
+
if (!request.user.isAuthenticated) throw new NotAuthenticated2();
|
|
1451
|
+
const limit = Math.min(Math.max(Number(request.query.limit) || 50, 1), 200);
|
|
1452
|
+
const offset = Math.max(Number(request.query.offset) || 0, 0);
|
|
1453
|
+
const qs = Conversation.objects.filter({ agent: this.agentName, user: request.user.pk });
|
|
1454
|
+
const [count, rows] = await Promise.all([
|
|
1455
|
+
qs.count(),
|
|
1456
|
+
qs.orderBy("-updatedAt").slice(offset, offset + limit)
|
|
1457
|
+
]);
|
|
1458
|
+
return new APIResponse2({ count, results: rows.map(conversationJSON) });
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
var ConversationDetailView = class extends AgentBaseView {
|
|
1462
|
+
async conversation(request, id) {
|
|
1463
|
+
this.requireMemory();
|
|
1464
|
+
if (!request.user.isAuthenticated) throw new NotAuthenticated2();
|
|
1465
|
+
try {
|
|
1466
|
+
return await getConversation(this.agentName, id, request.user);
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
if (err instanceof ConversationNotFound) throw new NotFound(err.message);
|
|
1469
|
+
throw err;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
async get(request, kwargs) {
|
|
1473
|
+
const conv = await this.conversation(request, String(kwargs.id));
|
|
1474
|
+
const rows = await Message.objects.filter({ conversation: String(conv.pk) }).orderBy("id");
|
|
1475
|
+
return new APIResponse2({
|
|
1476
|
+
...conversationJSON(conv),
|
|
1477
|
+
messages: rows.map((m) => ({ id: m.pk, role: m.role, content: m.content, createdAt: m.createdAt }))
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
async delete(request, kwargs) {
|
|
1481
|
+
const conv = await this.conversation(request, String(kwargs.id));
|
|
1482
|
+
await conv.delete();
|
|
1483
|
+
return new APIResponse2(null, { status: 204 });
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
var AgentRouter = class {
|
|
1487
|
+
registry = /* @__PURE__ */ new Map();
|
|
1488
|
+
opts;
|
|
1489
|
+
_urls;
|
|
1490
|
+
constructor(opts = {}) {
|
|
1491
|
+
this.opts = opts;
|
|
1492
|
+
}
|
|
1493
|
+
/** Register an agent class under `name` (URL segment, conversation `agent` value, MCP `ask_<name>`). */
|
|
1494
|
+
register(name, agentClass) {
|
|
1495
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
1496
|
+
throw new ImproperlyConfigured5(`Agent name '${name}' may only contain letters, digits, '_' and '-'.`);
|
|
1497
|
+
if (this.registry.has(name))
|
|
1498
|
+
throw new ImproperlyConfigured5(`An agent named '${name}' is already registered.`);
|
|
1499
|
+
this.registry.set(name, agentClass);
|
|
1500
|
+
this._urls = void 0;
|
|
1501
|
+
return this;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Consume queued runs (`agent.enqueue()`) from the Cloudflare Queue named `queue` (default: every queue).
|
|
1505
|
+
* Call it once, e.g. from an AppConfig's `ready()`. Returns an unregister function.
|
|
1506
|
+
*/
|
|
1507
|
+
consumeQueue(queue = "*") {
|
|
1508
|
+
return consumeAgentQueue(queue, (name) => this.create(name));
|
|
1509
|
+
}
|
|
1510
|
+
/** A fresh agent instance for `name`. */
|
|
1511
|
+
create(name) {
|
|
1512
|
+
const cls = this.registry.get(name);
|
|
1513
|
+
if (!cls) throw new ImproperlyConfigured5(`No agent registered as '${name}'.`);
|
|
1514
|
+
const agent = new cls();
|
|
1515
|
+
agent.agentName = name;
|
|
1516
|
+
return agent;
|
|
1517
|
+
}
|
|
1518
|
+
/** Registered agent names. */
|
|
1519
|
+
get names() {
|
|
1520
|
+
return [...this.registry.keys()];
|
|
1521
|
+
}
|
|
1522
|
+
getUrls() {
|
|
1523
|
+
const prefix = this.opts.urlNamePrefix ?? "agents";
|
|
1524
|
+
const out = [];
|
|
1525
|
+
for (const name of this.registry.keys()) {
|
|
1526
|
+
const init = { agentRouter: this, agentName: name };
|
|
1527
|
+
const chat = AgentChatView.asView(init);
|
|
1528
|
+
out.push(
|
|
1529
|
+
path2(`${name}/chat/`, chat, { name: `${prefix}-${name}-chat` }),
|
|
1530
|
+
path2(`${name}/chat`, chat),
|
|
1531
|
+
path2(`${name}/conversations/`, ConversationListView.asView(init), {
|
|
1532
|
+
name: `${prefix}-${name}-conversations`
|
|
1533
|
+
}),
|
|
1534
|
+
path2(`${name}/conversations/<uuid:id>/`, ConversationDetailView.asView(init), {
|
|
1535
|
+
name: `${prefix}-${name}-conversation`
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
return out;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* A Streamable HTTP MCP endpoint exposing `tools` plus the registered agents as `ask_<name>` tools
|
|
1543
|
+
* (`agents: false` to leave them out, or a list of names). Mount it with `path('mcp/', agents.mcp())`.
|
|
1544
|
+
*/
|
|
1545
|
+
mcp(opts = {}) {
|
|
1546
|
+
return this.mcpServer(opts).urls;
|
|
1547
|
+
}
|
|
1548
|
+
/** The `McpServer` behind `mcp()` (for tests / custom mounting). */
|
|
1549
|
+
mcpServer(opts = {}) {
|
|
1550
|
+
const { agents = true, ...rest } = opts;
|
|
1551
|
+
const names = agents === true ? this.names : agents === false ? [] : agents;
|
|
1552
|
+
const factories = {};
|
|
1553
|
+
for (const name of names) factories[name] = () => this.create(name);
|
|
1554
|
+
return new McpServer({ ...rest, agents: factories });
|
|
1555
|
+
}
|
|
1556
|
+
/** URL patterns to `include()`. */
|
|
1557
|
+
get urls() {
|
|
1558
|
+
this._urls ??= this.getUrls();
|
|
1559
|
+
return this._urls;
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
// src/tools/model.ts
|
|
1564
|
+
import {
|
|
1565
|
+
FieldFilter,
|
|
1566
|
+
ModelSerializer,
|
|
1567
|
+
ModelViewSet,
|
|
1568
|
+
OrderingFilter,
|
|
1569
|
+
PageNumberPagination,
|
|
1570
|
+
SearchFilter
|
|
1571
|
+
} from "@ghuri/rest";
|
|
1572
|
+
|
|
1573
|
+
// src/tools/viewset.ts
|
|
1574
|
+
import { ImproperlyConfigured as ImproperlyConfigured6 } from "@ghuri/core";
|
|
1575
|
+
import { RouterRegistration, SimpleRouter } from "@ghuri/rest";
|
|
1576
|
+
var ACTION_ALIASES = {
|
|
1577
|
+
delete: "destroy",
|
|
1578
|
+
partial_update: "partialUpdate",
|
|
1579
|
+
"partial-update": "partialUpdate"
|
|
1580
|
+
};
|
|
1581
|
+
var STANDARD = /* @__PURE__ */ new Set(["list", "create", "retrieve", "update", "partialUpdate", "destroy"]);
|
|
1582
|
+
function viewsetPrefix(viewset) {
|
|
1583
|
+
let model;
|
|
1584
|
+
try {
|
|
1585
|
+
const inst = new viewset();
|
|
1586
|
+
model = inst.queryset?.model ?? viewset.queryset?.model;
|
|
1587
|
+
} catch {
|
|
1588
|
+
model = void 0;
|
|
1589
|
+
}
|
|
1590
|
+
if (model?._meta)
|
|
1591
|
+
return sanitizeToolName(String(model._meta.verboseNamePlural).replace(/\s+/g, "_").toLowerCase());
|
|
1592
|
+
return sanitizeToolName(snakeCase(String(viewset.name).replace(/ViewSet$|View$/, "")));
|
|
1593
|
+
}
|
|
1594
|
+
var ViewSetTools = class extends Toolkit {
|
|
1595
|
+
entry;
|
|
1596
|
+
opts;
|
|
1597
|
+
prefix;
|
|
1598
|
+
specs;
|
|
1599
|
+
constructor(target, opts = {}) {
|
|
1600
|
+
super();
|
|
1601
|
+
this.opts = opts;
|
|
1602
|
+
if (target instanceof RouterRegistration) {
|
|
1603
|
+
this.entry = target;
|
|
1604
|
+
this.prefix = sanitizeToolName(opts.prefix ?? (target.prefix.replace(/\//g, "_") || target.basename));
|
|
1605
|
+
} else if (typeof target === "function") {
|
|
1606
|
+
this.prefix = sanitizeToolName(opts.prefix ?? viewsetPrefix(target));
|
|
1607
|
+
const router = new SimpleRouter();
|
|
1608
|
+
this.entry = router.register(this.prefix, target, opts.basename ? { basename: opts.basename } : {});
|
|
1609
|
+
} else {
|
|
1610
|
+
throw new TypeError("ViewSetTools expects a viewset class or a router registry entry.");
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/** The (action, tool suffix) pairs to expose. */
|
|
1614
|
+
actionSpecs() {
|
|
1615
|
+
if (this.specs) return this.specs;
|
|
1616
|
+
const available = this.entry.actions;
|
|
1617
|
+
const wanted = this.opts.actions ?? available;
|
|
1618
|
+
this.specs = wanted.map((name) => {
|
|
1619
|
+
const action = ACTION_ALIASES[name] ?? name;
|
|
1620
|
+
if (!available.includes(action)) {
|
|
1621
|
+
throw new ImproperlyConfigured6(
|
|
1622
|
+
`${this.entry.viewset.name} has no routed action '${name}' (available: ${available.join(", ")}).`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
return { action, suffix: snakeCase(name), description: this.opts.descriptions?.[name] };
|
|
1626
|
+
});
|
|
1627
|
+
return this.specs;
|
|
1628
|
+
}
|
|
1629
|
+
getTools() {
|
|
1630
|
+
return this.actionSpecs().map((spec) => this.buildTool(spec));
|
|
1631
|
+
}
|
|
1632
|
+
get model() {
|
|
1633
|
+
return this.entry.model;
|
|
1634
|
+
}
|
|
1635
|
+
modelNames() {
|
|
1636
|
+
const meta = this.model?._meta;
|
|
1637
|
+
return {
|
|
1638
|
+
one: meta ? String(meta.verboseName) : this.entry.basename,
|
|
1639
|
+
many: meta ? String(meta.verboseNamePlural) : this.prefix
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
lookup() {
|
|
1643
|
+
const view = this.entry.viewFor("retrieve");
|
|
1644
|
+
const field = view.lookupField ?? "pk";
|
|
1645
|
+
const kwarg = view.lookupUrlKwarg ?? field;
|
|
1646
|
+
const param = kwarg === "pk" ? "id" : kwarg;
|
|
1647
|
+
let schema = { type: ["integer", "string"] };
|
|
1648
|
+
const meta = this.model?._meta;
|
|
1649
|
+
const modelField = meta && (field === "pk" ? meta.pk : meta.getForwardField?.(field));
|
|
1650
|
+
if (modelField && typeof modelField.jsonSchema === "function") {
|
|
1651
|
+
try {
|
|
1652
|
+
const s = modelField.jsonSchema();
|
|
1653
|
+
schema = { type: s.type ?? ["integer", "string"], ...s.format ? { format: s.format } : {} };
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const { one } = this.modelNames();
|
|
1658
|
+
return { kwarg, param, schema: { ...schema, description: `Identifier (${field}) of the ${one}.` } };
|
|
1659
|
+
}
|
|
1660
|
+
describe(spec, endpoint) {
|
|
1661
|
+
if (spec.description) return spec.description;
|
|
1662
|
+
const { one, many } = this.modelNames();
|
|
1663
|
+
switch (spec.action) {
|
|
1664
|
+
case "list":
|
|
1665
|
+
return `List ${many}. Supports the filters, search, ordering and pagination parameters below; returns the matching ${many}.`;
|
|
1666
|
+
case "retrieve":
|
|
1667
|
+
return `Get one ${one} by id.`;
|
|
1668
|
+
case "create":
|
|
1669
|
+
return `Create a ${one}. Returns the created ${one}.`;
|
|
1670
|
+
case "update":
|
|
1671
|
+
return `Replace a ${one} (all writable fields required). Returns the updated ${one}.`;
|
|
1672
|
+
case "partialUpdate":
|
|
1673
|
+
return `Update some fields of a ${one}; omitted fields are left unchanged. Returns the updated ${one}.`;
|
|
1674
|
+
case "destroy":
|
|
1675
|
+
return `Delete a ${one} by id. This cannot be undone.`;
|
|
1676
|
+
}
|
|
1677
|
+
const extra = this.entry.extraActions.find((a) => a.name === spec.action);
|
|
1678
|
+
const desc = endpoint.description ?? extra?.kwargs.description;
|
|
1679
|
+
if (desc) return desc;
|
|
1680
|
+
return endpoint.detail ? `Run the '${spec.action}' action on one ${one}.` : `Run the '${spec.action}' action on ${many}.`;
|
|
1681
|
+
}
|
|
1682
|
+
buildTool(spec) {
|
|
1683
|
+
const endpoints = this.entry.endpoints.filter((e) => e.action === spec.action);
|
|
1684
|
+
const endpoint = endpoints[0];
|
|
1685
|
+
const methods = [...new Set(endpoints.map((e) => e.method))];
|
|
1686
|
+
const extra = !STANDARD.has(spec.action);
|
|
1687
|
+
const properties = {};
|
|
1688
|
+
const required = [];
|
|
1689
|
+
const lookup = endpoint.detail ? this.lookup() : void 0;
|
|
1690
|
+
if (lookup) {
|
|
1691
|
+
properties[lookup.param] = lookup.schema;
|
|
1692
|
+
required.push(lookup.param);
|
|
1693
|
+
}
|
|
1694
|
+
const hasBodyMethod = methods.some((m) => ["POST", "PUT", "PATCH"].includes(m));
|
|
1695
|
+
let bodyKeys = [];
|
|
1696
|
+
let nestData = false;
|
|
1697
|
+
let queryKeys = [];
|
|
1698
|
+
let freeQuery = false;
|
|
1699
|
+
if (hasBodyMethod) {
|
|
1700
|
+
const kwargs = this.entry.extraActions.find((a) => a.name === spec.action)?.kwargs ?? {};
|
|
1701
|
+
const useSerializer = !extra || kwargs.serializerClass;
|
|
1702
|
+
const schema = useSerializer ? this.entry.jsonSchema(spec.action, { mode: "input", partial: spec.action === "partialUpdate" }) : void 0;
|
|
1703
|
+
if (schema) {
|
|
1704
|
+
const body = cleanInputSchema(schema);
|
|
1705
|
+
const props = body.properties;
|
|
1706
|
+
nestData = !!lookup && lookup.param in props;
|
|
1707
|
+
if (nestData) {
|
|
1708
|
+
properties.data = { ...body, description: "Field values." };
|
|
1709
|
+
if (body.required?.length) required.push("data");
|
|
1710
|
+
} else {
|
|
1711
|
+
Object.assign(properties, props);
|
|
1712
|
+
for (const r of body.required ?? []) required.push(r);
|
|
1713
|
+
bodyKeys = Object.keys(props);
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
nestData = true;
|
|
1717
|
+
properties.data = {
|
|
1718
|
+
type: "object",
|
|
1719
|
+
description: "Request body (JSON object), if the action takes one."
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (spec.action === "list") {
|
|
1724
|
+
const params = this.listParameters();
|
|
1725
|
+
const { properties: qp, required: qr } = parametersToProperties(params);
|
|
1726
|
+
for (const [k, v] of Object.entries(qp)) if (!(k in properties)) properties[k] = v;
|
|
1727
|
+
required.push(...qr.filter((r) => !required.includes(r)));
|
|
1728
|
+
queryKeys = Object.keys(qp);
|
|
1729
|
+
} else if (methods.includes("GET") && extra) {
|
|
1730
|
+
freeQuery = true;
|
|
1731
|
+
properties.query = { type: "object", description: "Optional query string parameters." };
|
|
1732
|
+
}
|
|
1733
|
+
if (methods.length > 1) {
|
|
1734
|
+
properties.method = { type: "string", enum: methods, description: "HTTP method of the action." };
|
|
1735
|
+
}
|
|
1736
|
+
const inputSchema = { type: "object", properties };
|
|
1737
|
+
if (required.length) inputSchema.required = required;
|
|
1738
|
+
const annotations = {
|
|
1739
|
+
title: `${this.prefix} ${spec.suffix}`.replace(/_/g, " "),
|
|
1740
|
+
readOnlyHint: methods.every((m) => m === "GET" || m === "HEAD"),
|
|
1741
|
+
destructiveHint: spec.action === "destroy" || methods.includes("DELETE"),
|
|
1742
|
+
idempotentHint: methods.every((m) => ["GET", "HEAD", "PUT", "DELETE"].includes(m)),
|
|
1743
|
+
openWorldHint: false
|
|
1744
|
+
};
|
|
1745
|
+
const entry = this.entry;
|
|
1746
|
+
return {
|
|
1747
|
+
name: sanitizeToolName(`${this.prefix}_${spec.suffix}`),
|
|
1748
|
+
description: this.describe(spec, endpoint),
|
|
1749
|
+
inputSchema,
|
|
1750
|
+
annotations,
|
|
1751
|
+
parse: (value) => {
|
|
1752
|
+
if (value === void 0 || value === null) return {};
|
|
1753
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
1754
|
+
throw new ToolError("Arguments must be an object.");
|
|
1755
|
+
const missing = required.filter((r) => value[r] === void 0);
|
|
1756
|
+
if (lookup && missing.includes(lookup.param))
|
|
1757
|
+
throw new ToolError(`Missing required argument '${lookup.param}'.`, { status: 400 });
|
|
1758
|
+
return value;
|
|
1759
|
+
},
|
|
1760
|
+
execute: async (input, ctx) => {
|
|
1761
|
+
const args = { ...input ?? {} };
|
|
1762
|
+
const kwargs = {};
|
|
1763
|
+
if (lookup) {
|
|
1764
|
+
kwargs[lookup.kwarg] = args[lookup.param];
|
|
1765
|
+
delete args[lookup.param];
|
|
1766
|
+
}
|
|
1767
|
+
const method = typeof args.method === "string" ? args.method.toUpperCase() : void 0;
|
|
1768
|
+
delete args.method;
|
|
1769
|
+
let query;
|
|
1770
|
+
let data;
|
|
1771
|
+
if (spec.action === "list") {
|
|
1772
|
+
query = {};
|
|
1773
|
+
for (const k of queryKeys) if (args[k] !== void 0 && args[k] !== null) query[k] = args[k];
|
|
1774
|
+
} else if (freeQuery && args.query && typeof args.query === "object") {
|
|
1775
|
+
query = args.query;
|
|
1776
|
+
delete args.query;
|
|
1777
|
+
}
|
|
1778
|
+
if (hasBodyMethod && method !== "GET") {
|
|
1779
|
+
if (nestData) data = args.data ?? {};
|
|
1780
|
+
else {
|
|
1781
|
+
data = {};
|
|
1782
|
+
for (const k of bodyKeys) if (k in args) data[k] = args[k];
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const user = ctx.user?.isAuthenticated ? ctx.user : null;
|
|
1786
|
+
const res = await entry.invoke(spec.action, { user, kwargs, query, data, method });
|
|
1787
|
+
return toolResult(res.status, res.data);
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
/** OpenAPI query parameters of the list action (filter backends + paginator). */
|
|
1792
|
+
listParameters() {
|
|
1793
|
+
const view = this.entry.viewFor("list");
|
|
1794
|
+
const out = [];
|
|
1795
|
+
const backends = typeof view.getFilterBackends === "function" ? view.getFilterBackends() : view.filterBackends ?? [];
|
|
1796
|
+
for (const B of backends) {
|
|
1797
|
+
const b = new B();
|
|
1798
|
+
if (typeof b.getSchemaOperationParameters === "function")
|
|
1799
|
+
out.push(...b.getSchemaOperationParameters(view));
|
|
1800
|
+
}
|
|
1801
|
+
const paginator = view.paginator;
|
|
1802
|
+
if (paginator && typeof paginator.getSchemaOperationParameters === "function")
|
|
1803
|
+
out.push(...paginator.getSchemaOperationParameters(view));
|
|
1804
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1805
|
+
return out.filter((p) => {
|
|
1806
|
+
const name = String(p.name);
|
|
1807
|
+
if (seen.has(name)) return false;
|
|
1808
|
+
seen.add(name);
|
|
1809
|
+
return true;
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
function toolResult(status, data) {
|
|
1814
|
+
if (status >= 400) throw new ToolError(errorMessage(status, data), { status, detail: data });
|
|
1815
|
+
if (status === 204 || data === null || data === "") return { success: true, status };
|
|
1816
|
+
return data;
|
|
1817
|
+
}
|
|
1818
|
+
function errorMessage(status, data) {
|
|
1819
|
+
const detail = data && typeof data === "object" && typeof data.detail === "string" ? data.detail : typeof data === "string" && data ? data : status === 400 ? `Validation failed: ${JSON.stringify(data)}` : void 0;
|
|
1820
|
+
return `${detail ?? "Request failed."} (HTTP ${status})`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/tools/model.ts
|
|
1824
|
+
var ACTIONS = {
|
|
1825
|
+
list: "list",
|
|
1826
|
+
retrieve: "retrieve",
|
|
1827
|
+
create: "create",
|
|
1828
|
+
update: "partialUpdate",
|
|
1829
|
+
delete: "destroy"
|
|
1830
|
+
};
|
|
1831
|
+
function defaultSerializer(model) {
|
|
1832
|
+
const cls = class extends ModelSerializer {
|
|
1833
|
+
static Meta = { model, fields: "__all__" };
|
|
1834
|
+
};
|
|
1835
|
+
Object.defineProperty(cls, "name", { value: `${model._meta.objectName}Serializer` });
|
|
1836
|
+
return cls;
|
|
1837
|
+
}
|
|
1838
|
+
function buildViewSet(model, opts) {
|
|
1839
|
+
const serializer = opts.serializer ?? defaultSerializer(model);
|
|
1840
|
+
const backends = [];
|
|
1841
|
+
if (opts.filterFields) backends.push(FieldFilter);
|
|
1842
|
+
if (opts.searchFields?.length) backends.push(SearchFilter);
|
|
1843
|
+
if (opts.orderingFields || opts.ordering) backends.push(OrderingFilter);
|
|
1844
|
+
const pageSize = opts.pageSize ?? 20;
|
|
1845
|
+
const maxPageSize = opts.maxPageSize ?? Math.max(100, pageSize);
|
|
1846
|
+
const Paginator = class AgentPagination extends PageNumberPagination {
|
|
1847
|
+
pageSize = pageSize;
|
|
1848
|
+
pageSizeQueryParam = "pageSize";
|
|
1849
|
+
maxPageSize = maxPageSize;
|
|
1850
|
+
};
|
|
1851
|
+
const ViewSet = class extends ModelViewSet {
|
|
1852
|
+
queryset = model;
|
|
1853
|
+
serializerClass = serializer;
|
|
1854
|
+
filterBackends = backends;
|
|
1855
|
+
paginationClass = Paginator;
|
|
1856
|
+
searchFields = opts.searchFields;
|
|
1857
|
+
orderingFields = opts.orderingFields;
|
|
1858
|
+
ordering = opts.ordering;
|
|
1859
|
+
filtersetFields = opts.filterFields;
|
|
1860
|
+
constructor() {
|
|
1861
|
+
super();
|
|
1862
|
+
if (opts.permissionClasses) this.permissionClasses = opts.permissionClasses;
|
|
1863
|
+
}
|
|
1864
|
+
getQueryset() {
|
|
1865
|
+
const qs = super.getQueryset();
|
|
1866
|
+
return opts.filter ? opts.filter(qs, this.request.user) : qs;
|
|
1867
|
+
}
|
|
1868
|
+
async performCreate(serializer2) {
|
|
1869
|
+
await serializer2.save(opts.saveKwargs ? await opts.saveKwargs(this.request.user, "create") : {});
|
|
1870
|
+
}
|
|
1871
|
+
async performUpdate(serializer2) {
|
|
1872
|
+
await serializer2.save(opts.saveKwargs ? await opts.saveKwargs(this.request.user, "update") : {});
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
Object.defineProperty(ViewSet, "name", { value: `${model._meta.objectName}ToolsViewSet` });
|
|
1876
|
+
return ViewSet;
|
|
1877
|
+
}
|
|
1878
|
+
var ModelTools = class extends ViewSetTools {
|
|
1879
|
+
modelClass;
|
|
1880
|
+
modelOptions;
|
|
1881
|
+
constructor(model, opts = {}) {
|
|
1882
|
+
if (!model?._meta) throw new TypeError("ModelTools expects a model class.");
|
|
1883
|
+
const prefix = opts.prefix ?? sanitizeToolName(String(model._meta.verboseNamePlural).replace(/\s+/g, "_").toLowerCase());
|
|
1884
|
+
super(buildViewSet(model, opts), { prefix, basename: model._meta.modelName });
|
|
1885
|
+
this.modelClass = model;
|
|
1886
|
+
this.modelOptions = opts;
|
|
1887
|
+
}
|
|
1888
|
+
actionSpecs() {
|
|
1889
|
+
if (!this.specs) {
|
|
1890
|
+
const actions = this.modelOptions.actions ?? ["list", "retrieve"];
|
|
1891
|
+
this.specs = actions.map((a) => {
|
|
1892
|
+
const action = ACTIONS[a];
|
|
1893
|
+
if (!action)
|
|
1894
|
+
throw new TypeError(`ModelTools: unknown action '${a}' (use ${Object.keys(ACTIONS).join(", ")}).`);
|
|
1895
|
+
return { action, suffix: a, description: this.modelOptions.descriptions?.[a] };
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return this.specs;
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
// src/tools/router.ts
|
|
1903
|
+
function matches(entry, spec) {
|
|
1904
|
+
if (typeof spec === "function") return spec(entry);
|
|
1905
|
+
return spec.includes(entry.prefix) || spec.includes(entry.basename);
|
|
1906
|
+
}
|
|
1907
|
+
var RouterTools = class extends Toolkit {
|
|
1908
|
+
constructor(router, opts = {}) {
|
|
1909
|
+
super();
|
|
1910
|
+
this.router = router;
|
|
1911
|
+
this.opts = opts;
|
|
1912
|
+
}
|
|
1913
|
+
router;
|
|
1914
|
+
opts;
|
|
1915
|
+
/** The `ViewSetTools` of each included registration. */
|
|
1916
|
+
toolkits() {
|
|
1917
|
+
const out = [];
|
|
1918
|
+
for (const entry of this.router.registry) {
|
|
1919
|
+
if (this.opts.include && !matches(entry, this.opts.include)) continue;
|
|
1920
|
+
if (this.opts.exclude && matches(entry, this.opts.exclude)) continue;
|
|
1921
|
+
let actions = this.opts.actions?.[entry.prefix] ?? this.opts.actions?.[entry.basename];
|
|
1922
|
+
if (this.opts.readOnly) {
|
|
1923
|
+
const safe = new Set(entry.endpoints.filter((e) => e.method === "GET").map((e) => e.action));
|
|
1924
|
+
actions = (actions ?? entry.actions).filter((a) => safe.has(a));
|
|
1925
|
+
}
|
|
1926
|
+
if (actions && !actions.length) continue;
|
|
1927
|
+
out.push(new ViewSetTools(entry, actions ? { actions } : {}));
|
|
1928
|
+
}
|
|
1929
|
+
return out;
|
|
1930
|
+
}
|
|
1931
|
+
getTools() {
|
|
1932
|
+
const excluded = Array.isArray(this.opts.exclude) ? new Set(this.opts.exclude) : void 0;
|
|
1933
|
+
return this.toolkits().flatMap((t) => t.getTools()).filter((t) => !excluded?.has(t.name));
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
export {
|
|
1937
|
+
AGENT_JOB_TYPE,
|
|
1938
|
+
Agent,
|
|
1939
|
+
AgentChatView,
|
|
1940
|
+
AgentError,
|
|
1941
|
+
AgentRouter,
|
|
1942
|
+
AgentStream,
|
|
1943
|
+
AgentsConfig,
|
|
1944
|
+
Conversation,
|
|
1945
|
+
ConversationDetailView,
|
|
1946
|
+
ConversationListView,
|
|
1947
|
+
ConversationNotFound,
|
|
1948
|
+
JSONRPC,
|
|
1949
|
+
MCP_LATEST_PROTOCOL_VERSION,
|
|
1950
|
+
MCP_PROTOCOL_VERSIONS,
|
|
1951
|
+
MESSAGE_ROLES,
|
|
1952
|
+
McpError,
|
|
1953
|
+
McpServer,
|
|
1954
|
+
McpView,
|
|
1955
|
+
Message,
|
|
1956
|
+
ModelResolutionError,
|
|
1957
|
+
ModelTools,
|
|
1958
|
+
ModelUnavailable,
|
|
1959
|
+
RouterTools,
|
|
1960
|
+
TOOL,
|
|
1961
|
+
TOOL_OPTIONS,
|
|
1962
|
+
ToolError,
|
|
1963
|
+
ToolInputError,
|
|
1964
|
+
Toolkit,
|
|
1965
|
+
UnknownToolError,
|
|
1966
|
+
ViewSetTools,
|
|
1967
|
+
agentAuthenticationClasses,
|
|
1968
|
+
agentTool,
|
|
1969
|
+
agentsSettings,
|
|
1970
|
+
collectDeclaredTools,
|
|
1971
|
+
consumeAgentQueue,
|
|
1972
|
+
createConversation,
|
|
1973
|
+
defineTool,
|
|
1974
|
+
enqueueRun,
|
|
1975
|
+
getConversation,
|
|
1976
|
+
getProviders,
|
|
1977
|
+
isAgentTool,
|
|
1978
|
+
isLanguageModel,
|
|
1979
|
+
isToolDecorator,
|
|
1980
|
+
isToolkit,
|
|
1981
|
+
loadJobUser,
|
|
1982
|
+
loadMessages,
|
|
1983
|
+
memoryAvailable,
|
|
1984
|
+
registerProvider,
|
|
1985
|
+
resolveModel,
|
|
1986
|
+
runTool,
|
|
1987
|
+
sanitizeToolName,
|
|
1988
|
+
saveMessages,
|
|
1989
|
+
standaloneTool,
|
|
1990
|
+
toJSONSchema,
|
|
1991
|
+
toToolError,
|
|
1992
|
+
tool,
|
|
1993
|
+
unregisterProvider
|
|
1994
|
+
};
|
|
1995
|
+
//# sourceMappingURL=index.js.map
|