@anteros/core 0.0.40 → 0.0.42

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.
Files changed (130) hide show
  1. package/database/audit.ts +143 -0
  2. package/database/collection.ts +6 -4
  3. package/database/deleteLog.ts +98 -0
  4. package/database/file.ts +7 -6
  5. package/database/mongodbadapter.ts +420 -185
  6. package/database/replication.ts +858 -0
  7. package/database/schema.ts +140 -125
  8. package/database/tenant.ts +4 -0
  9. package/database/ttl.ts +68 -0
  10. package/database/vars.ts +405 -0
  11. package/database/workflow.ts +466 -240
  12. package/index.ts +16 -0
  13. package/lib/agent.ts +1327 -0
  14. package/lib/agentMemory.ts +586 -0
  15. package/lib/agents.ts +157 -0
  16. package/lib/ai.ts +0 -0
  17. package/lib/api.ts +246 -0
  18. package/lib/attachment.ts +351 -0
  19. package/lib/audit.ts +271 -0
  20. package/lib/auditResult.ts +133 -0
  21. package/lib/define.ts +62 -1
  22. package/lib/endpoints.ts +80 -0
  23. package/lib/files.ts +66 -12
  24. package/lib/jsonSchema.ts +183 -0
  25. package/lib/lifecycle.ts +147 -0
  26. package/lib/load.ts +17 -0
  27. package/lib/mcp.ts +3 -2
  28. package/lib/messages.ts +299 -0
  29. package/lib/providers.ts +867 -0
  30. package/lib/redact.ts +117 -0
  31. package/lib/routes.ts +14 -1
  32. package/lib/scriptWorker.ts +4 -1
  33. package/lib/scripts.ts +6 -1
  34. package/lib/sockets.ts +3 -1
  35. package/lib/workflow.ts +209 -37
  36. package/lib/zip.ts +109 -0
  37. package/package.json +1 -1
  38. package/server/access.ts +96 -0
  39. package/server/agents.ts +451 -0
  40. package/server/api.ts +121 -95
  41. package/server/boot.ts +196 -53
  42. package/server/flags.ts +136 -0
  43. package/server/hono.ts +9 -4
  44. package/server/mcp.ts +17 -63
  45. package/server/routes.ts +19 -24
  46. package/tests/agent-memory.test.ts +617 -0
  47. package/tests/agent-working-memory.test.ts +402 -0
  48. package/tests/agent.test.ts +1239 -0
  49. package/tests/agents-api.test.ts +593 -0
  50. package/tests/api.test.ts +51 -0
  51. package/tests/attachment.test.ts +241 -0
  52. package/tests/audit-file.test.ts +296 -0
  53. package/tests/audit-operations.test.ts +184 -0
  54. package/tests/audit-redact.test.ts +259 -0
  55. package/tests/audit-results.test.ts +188 -0
  56. package/tests/audit-retention-config.test.ts +76 -0
  57. package/tests/audit-run-action.test.ts +120 -0
  58. package/tests/audit.test.ts +320 -0
  59. package/tests/db-cache.test.ts +7 -7
  60. package/tests/endpoints.test.ts +80 -0
  61. package/tests/file-collection-replication.test.ts +98 -0
  62. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  63. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  64. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  65. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  66. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  67. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  68. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  69. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  70. package/tests/fixtures/attachment/data.csv +2 -0
  71. package/tests/fixtures/fake-provider.ts +128 -0
  72. package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
  73. package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
  74. package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
  75. package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
  76. package/tests/fixtures/repl-only/app.ts +26 -0
  77. package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
  78. package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
  79. package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
  80. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  81. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  82. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  83. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  84. package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
  85. package/tests/fixtures/vars-tenant/vars/private.var.ts +9 -0
  86. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  87. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  88. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  89. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  90. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  91. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  92. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  93. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  94. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  95. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  96. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  97. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  98. package/tests/hono.test.ts +6 -3
  99. package/tests/lifecycle.test.ts +96 -0
  100. package/tests/locks.test.ts +120 -0
  101. package/tests/mcp.test.ts +11 -11
  102. package/tests/messages.test.ts +258 -0
  103. package/tests/replication-control.test.ts +128 -0
  104. package/tests/replication-initial-sync.test.ts +86 -0
  105. package/tests/replication-meta.test.ts +243 -0
  106. package/tests/replication-only.test.ts +358 -0
  107. package/tests/replication.test.ts +188 -0
  108. package/tests/upload.test.ts +141 -8
  109. package/tests/vars-api.test.ts +123 -0
  110. package/tests/vars-replication.test.ts +96 -0
  111. package/tests/vars.test.ts +216 -0
  112. package/tests/workflow-engine.test.ts +340 -0
  113. package/tests/workflow-retention.test.ts +103 -0
  114. package/tests/workflow.test.ts +186 -0
  115. package/types/agent.d.ts +837 -0
  116. package/types/collection.d.ts +3 -0
  117. package/types/config.d.ts +80 -0
  118. package/types/file.d.ts +11 -8
  119. package/types/hook.d.ts +6 -0
  120. package/types/lifecycle.d.ts +77 -0
  121. package/types/mcp.d.ts +11 -1
  122. package/types/replication.d.ts +228 -0
  123. package/types/route.d.ts +7 -1
  124. package/types/scripts.d.ts +6 -0
  125. package/types/service.d.ts +7 -1
  126. package/types/tenant.d.ts +36 -0
  127. package/types/vars.d.ts +147 -0
  128. package/types/websocket.d.ts +6 -0
  129. package/types/workflow.d.ts +50 -3
  130. package/utils/func.ts +67 -52
@@ -0,0 +1,96 @@
1
+ import { AppError, fn } from "../lib/error";
2
+ import * as func from "../utils/func";
3
+ import type { useRest } from "../database/rest";
4
+
5
+ /**
6
+ * Shared HTTP plumbing between the collection/vars/service API (`server/api.ts`)
7
+ * and the agents API (`server/agents.ts`).
8
+ *
9
+ * Extracted rather than duplicated: the access rule semantics (wildcard, boolean
10
+ * vs function, token requirement) must stay identical everywhere — a divergence
11
+ * here is a hole.
12
+ */
13
+
14
+ function getAccessToken(c: any) {
15
+ const t = c.get('token');
16
+ return {
17
+ value: (t?.value ?? null) as string | null,
18
+ decoded: (t?.decoded ?? null) as Record<string, unknown> | null,
19
+ provided: (t?.provided ?? false) as boolean,
20
+ expired: (t?.expired ?? false) as boolean,
21
+ };
22
+ }
23
+
24
+ function errorResponse(c: any, err: any) {
25
+ const isAppError = err instanceof AppError;
26
+ const status = isAppError ? Number(err.status) : 500;
27
+ return c.json({
28
+ message: isAppError ? err.message : 'Internal server error',
29
+ code: isAppError ? err.code : 'INTERNAL_SERVER_ERROR',
30
+ meta: isAppError ? err.meta : undefined,
31
+ }, status);
32
+ }
33
+
34
+ /**
35
+ * Evaluate an `api.access` rule — **no rule = denied** (secure by default).
36
+ *
37
+ * - `true` → allowed without a token (public)
38
+ * - `false` → denied
39
+ * - function → **requires a valid, non-expired token**, then the rule decides
40
+ *
41
+ * `extra` is merged into the rule context (agents pass the agent, the action and
42
+ * the input so a rule can decide on the payload it is about to run).
43
+ */
44
+ async function evaluateAccess(
45
+ access: { [key: string]: boolean | ((ctx: any) => boolean | Promise<boolean>) | undefined } | undefined,
46
+ operation: string,
47
+ rest: InstanceType<typeof useRest>,
48
+ label: string,
49
+ c: any,
50
+ extra: Record<string, any> = {},
51
+ ): Promise<void> {
52
+ if (!access) {
53
+ throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
54
+ }
55
+
56
+ const hasWildcard = access['*'] !== undefined;
57
+ const hasSpecific = access[operation] !== undefined;
58
+
59
+ if (!hasWildcard && !hasSpecific) {
60
+ throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
61
+ }
62
+
63
+ const rule = hasSpecific ? access[operation] : access['*'];
64
+ if (rule === undefined) return;
65
+
66
+ // Boolean `true` → allow without requiring a token
67
+ if (typeof rule === 'boolean') {
68
+ if (!rule) {
69
+ throw new AppError(`${label} not allowed`, { status: 401, code: 'ACCESS_DENIED' });
70
+ }
71
+ return;
72
+ }
73
+
74
+ // Function → requires a valid token
75
+ const accessToken = getAccessToken(c);
76
+
77
+ if (accessToken.expired) {
78
+ throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
79
+ }
80
+ if (!accessToken.value) {
81
+ throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
82
+ }
83
+
84
+ const allowed = await (rule as Function)({
85
+ rest,
86
+ error: fn.error,
87
+ jwt: func.jwt,
88
+ token: accessToken,
89
+ ...extra,
90
+ });
91
+ if (!allowed) {
92
+ throw new AppError(`${label} not allowed`, { status: 401, code: 'ACCESS_DENIED' });
93
+ }
94
+ }
95
+
96
+ export { getAccessToken, errorResponse, evaluateAccess };
@@ -0,0 +1,451 @@
1
+ import type { Hono } from "hono";
2
+ import { cfg } from "./config";
3
+ import { AppError } from "../lib/error";
4
+ import { useRest } from "../database/rest";
5
+ import { createAgents } from "../lib/agents";
6
+ import { getTenant } from "../database/tenant";
7
+ import { requestCtxStorage } from "../lib/asyncContextStorage";
8
+ import { evaluateAccess, errorResponse } from "./access";
9
+ import { patterns } from "../lib/endpoints";
10
+ import type { HonoVariables } from "./env";
11
+ import type { Agent } from "../lib/agent";
12
+ import type {
13
+ AgentCallOptions,
14
+ AgentGenerateResult,
15
+ AgentInput,
16
+ AgentMessageInput,
17
+ } from "../types/agent";
18
+
19
+ /**
20
+ * Agents HTTP API — `POST /api/:tenant_id/agents/:agent/:action`.
21
+ *
22
+ * Same shape as the services API (`/api/:tenant_id/services/:service/:action`) and
23
+ * the same access model (`api.access`, **no rules = denied**).
24
+ *
25
+ * ```ts
26
+ * // define.Agent({ api: { access: { generate: (ctx) => !!ctx.token.value } } })
27
+ * POST /api/v1/agents/support/generate { "input": "Where is my order?" }
28
+ * POST /api/v1/agents/support/stream { "input": "…", "thread": "user-42" } → SSE
29
+ * ```
30
+ *
31
+ * A client can only send **what it is allowed to choose**: the input, a memory
32
+ * thread, injected turns and a *lower* `maxSteps` / `lastMessages`. The provider
33
+ * (model, endpoint, key), the options and the structured-output schema stay
34
+ * server-side — changing the model or the endpoint of a declared agent is a
35
+ * deployment decision, not a request one.
36
+ */
37
+
38
+ const AGENT_PREFIX = patterns.agent;
39
+
40
+ /** What an HTTP call may carry. Nothing else is read. */
41
+ type AgentRequestBody = {
42
+ /** A string (one user turn), one message, or a whole conversation. */
43
+ input?: string | AgentMessageInput | AgentMessageInput[];
44
+ /**
45
+ * Turns **injected** into the run — after the thread, before the `input`. They
46
+ * are sent to the model *and* saved with the rest of the conversation.
47
+ */
48
+ messages?: AgentMessageInput | AgentMessageInput[];
49
+ /** Memory thread. */
50
+ thread?: string;
51
+ /** @deprecated Alias of `thread`. */
52
+ threadId?: string;
53
+ /** Opaque caller identity, forwarded to the instructions function. */
54
+ resource?: string;
55
+ /** @deprecated Alias of `resource`. */
56
+ resourceId?: string;
57
+ /** Lower the run's step ceiling (never raise it). */
58
+ maxSteps?: number;
59
+ /** Lower the replayed-history window (never raise it past the declaration). */
60
+ lastMessages?: number;
61
+ /** `threads` — how many threads to return (default 20). */
62
+ limit?: number;
63
+ /**
64
+ * Files and images sent with the run — base64, a `File`/`Blob` never crosses the
65
+ * wire as such. `{ name: 'invoice.pdf', data: '<base64>' }`.
66
+ */
67
+ files?: Array<{
68
+ /** File name — its extension decides how it is read (`report.pdf`, `data.csv`, `photo.png`). */
69
+ name: string;
70
+ mimeType?: string;
71
+ /** Base64 (default) or UTF-8 text with `encoding: 'utf8'`. */
72
+ data: string;
73
+ encoding?: "base64" | "utf8";
74
+ }>;
75
+ /** Read the thread without writing the run back. */
76
+ readOnly?: boolean;
77
+ /** Mastra-style shorthand — `memory: { thread, resource, readOnly }`. */
78
+ memory?: {
79
+ thread?: string;
80
+ /** @deprecated Alias of `thread`. */
81
+ threadId?: string;
82
+ resource?: string;
83
+ /** @deprecated Alias of `resource`. */
84
+ resourceId?: string;
85
+ readOnly?: boolean;
86
+ };
87
+ };
88
+
89
+ /** `memory.thread` first, then the flat `thread` (and its `threadId` alias). */
90
+ function resolveThread(body: AgentRequestBody | undefined): string | undefined {
91
+ const thread = body?.memory?.thread ?? body?.memory?.threadId ?? body?.thread ?? body?.threadId;
92
+ return typeof thread === 'string' && thread.trim() ? thread : undefined;
93
+ }
94
+
95
+ /** `memory.resource` first, then the flat `resource` (and its `resourceId` alias). */
96
+ function resolveResource(body: AgentRequestBody | undefined): string | undefined {
97
+ const resource = body?.memory?.resource ?? body?.memory?.resourceId ?? body?.resource ?? body?.resourceId;
98
+ return typeof resource === 'string' && resource.trim() ? resource : undefined;
99
+ }
100
+
101
+ /** Cap on the prompt kept in the audit trail. */
102
+ const AUDIT_INPUT_MAX = 2000;
103
+
104
+ /**
105
+ * The input of a run — a string, **one** message, or a whole conversation. The
106
+ * shape itself (roles, content parts, tool parts) is validated by
107
+ * `normalizeMessages`, shared with the in-process runtime: the two cannot drift.
108
+ */
109
+ function normalizeInput(body: AgentRequestBody | undefined): AgentInput {
110
+ const input = body?.input;
111
+ if (typeof input === "string") {
112
+ if (!input.trim()) throw new AppError('`input` is required', { status: 400, code: 'INPUT_REQUIRED' });
113
+ return input;
114
+ }
115
+ if (Array.isArray(input)) {
116
+ if (!input.length) throw new AppError('`input` is required', { status: 400, code: 'INPUT_REQUIRED' });
117
+ return input as AgentInput;
118
+ }
119
+ if (input && typeof input === "object" && typeof (input as any).role === "string") {
120
+ return input as AgentInput;
121
+ }
122
+ throw new AppError('`input` is required (a string, a message or an array of messages)', {
123
+ status: 400, code: 'INPUT_REQUIRED',
124
+ });
125
+ }
126
+
127
+ function requireThread(body: AgentRequestBody | undefined): string {
128
+ const thread = resolveThread(body);
129
+ if (!thread) {
130
+ throw new AppError('`thread` is required (or `memory.thread`)', { status: 400, code: 'THREAD_ID_REQUIRED' });
131
+ }
132
+ return thread;
133
+ }
134
+
135
+ /** A client may lower the step ceiling, never raise it — the cost is the tenant's. */
136
+ function resolveMaxSteps(requested: unknown, agent: Agent): number {
137
+ const declared = agent.getConfig().maxSteps ?? 5;
138
+ const asked = Number(requested);
139
+ if (!Number.isFinite(asked) || asked <= 0) return declared;
140
+ return Math.max(1, Math.min(Math.floor(asked), declared));
141
+ }
142
+
143
+ /**
144
+ * The history window of a request: a client may shrink the prompt, never grow it
145
+ * past what the agent declares (with no declaration, the store cap is the only
146
+ * bound that matters — and it is already applied on read).
147
+ */
148
+ function resolveLastMessages(requested: unknown, agent: Agent): number | undefined {
149
+ const declared = agent.getConfig().lastMessages;
150
+ const asked = Number(requested);
151
+ if (!Number.isFinite(asked) || asked <= 0) return declared;
152
+ if (!declared) return Math.floor(asked);
153
+ return Math.max(1, Math.min(Math.floor(asked), declared));
154
+ }
155
+
156
+ /** The answer's envelope — the conversation itself is available through `history`. */
157
+ function presentResult(result: AgentGenerateResult) {
158
+ return {
159
+ text: result.text,
160
+ // Same envelope as `generate()` in-process: the reasoning travels with the run
161
+ ...(result.reasoning ? { reasoning: result.reasoning } : {}),
162
+ toolCalls: result.toolCalls,
163
+ toolResults: result.toolResults,
164
+ steps: result.steps,
165
+ usage: result.usage,
166
+ finishReason: result.finishReason,
167
+ };
168
+ }
169
+
170
+ /** The prompt is a parameter — capped, and redacted like any other payload. */
171
+ function auditInput(input: AgentInput | undefined) {
172
+ if (typeof input === 'string') {
173
+ return input.length > AUDIT_INPUT_MAX
174
+ ? { text: input.slice(0, AUDIT_INPUT_MAX), length: input.length, truncated: true }
175
+ : { text: input };
176
+ }
177
+ return { messages: Array.isArray(input) ? input.length : 1 };
178
+ }
179
+
180
+ type AuditOptions = {
181
+ rest: InstanceType<typeof useRest>;
182
+ agentId: string;
183
+ action: string;
184
+ input?: AgentInput;
185
+ thread?: string;
186
+ result?: AgentGenerateResult;
187
+ error?: { message: string; code?: string };
188
+ started: number;
189
+ };
190
+
191
+ /** One audit entry per call, at the same place as `_vars_:<ns>` — `_agents_:<id>`. */
192
+ async function auditAgent({
193
+ rest, agentId, action, input, thread, result, error, started,
194
+ }: AuditOptions): Promise<void> {
195
+ await rest.audit.log({
196
+ action: `agent.${action}`,
197
+ collection: `_agents_:${agentId}`,
198
+ input: { agent: agentId, thread, ...auditInput(input) },
199
+ // Never the answer, never the tool arguments: the envelope only.
200
+ result: result
201
+ ? {
202
+ finishReason: result.finishReason,
203
+ usage: result.usage,
204
+ toolCalls: result.toolCalls.map((call) => ({ name: call.name })),
205
+ steps: result.steps,
206
+ }
207
+ : undefined,
208
+ error,
209
+ duration: Date.now() - started,
210
+ }).catch(() => {});
211
+ }
212
+
213
+ /** The streamed answer is one SSE event per runtime chunk, then a `done` event. */
214
+ function streamResponse(
215
+ c: any,
216
+ agent: Agent,
217
+ input: AgentInput,
218
+ callOptions: AgentCallOptions,
219
+ audit: Omit<AuditOptions, 'input' | 'thread' | 'result' | 'error'>,
220
+ ): Response {
221
+ const encoder = new TextEncoder();
222
+ const stream = agent.stream(input, callOptions);
223
+
224
+ const body = new ReadableStream<Uint8Array>({
225
+ async start(controller) {
226
+ const send = (payload: any) => {
227
+ try {
228
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
229
+ } catch {
230
+ // The client is gone — the run is already aborted by its signal.
231
+ }
232
+ };
233
+
234
+ try {
235
+ for await (const chunk of stream.fullStream) send(chunk);
236
+
237
+ const result = {
238
+ text: await stream.text,
239
+ object: await stream.object,
240
+ toolCalls: await stream.toolCalls,
241
+ toolResults: await stream.toolResults,
242
+ steps: await stream.steps,
243
+ usage: await stream.usage,
244
+ finishReason: await stream.finishReason,
245
+ messages: [],
246
+ } as AgentGenerateResult;
247
+
248
+ send({
249
+ type: 'done',
250
+ result: presentResult(result),
251
+ ...(result.object !== undefined ? { object: result.object } : {}),
252
+ });
253
+ await auditAgent({ ...audit, result });
254
+ } catch (err: any) {
255
+ send({
256
+ type: 'error',
257
+ message: err?.message ?? 'Agent stream failed',
258
+ code: err?.code ?? 'INTERNAL_AGENT_ERROR',
259
+ });
260
+ await auditAgent({
261
+ ...audit,
262
+ error: { message: err?.message, code: err?.code || 'INTERNAL_AGENT_ERROR' },
263
+ });
264
+ } finally {
265
+ try { controller.close(); } catch { /* already closed */ }
266
+ }
267
+ },
268
+ cancel() {
269
+ // Client disconnected mid-run: stop paying for tokens.
270
+ stream.abort('client disconnected');
271
+ },
272
+ });
273
+
274
+ return new Response(body, {
275
+ headers: {
276
+ 'Content-Type': 'text/event-stream; charset=utf-8',
277
+ 'Cache-Control': 'no-cache, no-transform',
278
+ 'Connection': 'keep-alive',
279
+ // nginx: never buffer an event stream
280
+ 'X-Accel-Buffering': 'no',
281
+ },
282
+ });
283
+ }
284
+
285
+ function initializeAgents(app: Hono<{ Variables: HonoVariables }>) {
286
+ app.post(AGENT_PREFIX, async (c) => {
287
+ const started = Date.now();
288
+ let rest: InstanceType<typeof useRest> | undefined;
289
+ let tenant_id = '';
290
+ let agentId = '';
291
+ let action = '';
292
+
293
+ try {
294
+ const contentType = c.req.header('Content-Type');
295
+ let body: AgentRequestBody | undefined;
296
+ if (contentType?.includes('application/json')) {
297
+ // `info` legitimately carries no body: an empty one is `{}`, a
298
+ // malformed one is an error (never silently ignored).
299
+ const raw = await c.req.text();
300
+ if (raw.trim()) {
301
+ try {
302
+ body = JSON.parse(raw);
303
+ } catch {
304
+ throw new AppError('Invalid JSON body', { status: 400, code: 'INVALID_JSON_BODY' });
305
+ }
306
+ }
307
+ }
308
+
309
+ const params = c.req.param() as { tenant_id: string; agent: string; action: string };
310
+ tenant_id = params.tenant_id;
311
+ agentId = params.agent;
312
+ action = params.action;
313
+
314
+ if (!tenant_id) throw new AppError('Tenant ID is required', { status: 400, code: 'TENANT_ID_REQUIRED' });
315
+ if (!agentId) throw new AppError('Agent is required', { status: 400, code: 'AGENT_REQUIRED' });
316
+ if (!action) throw new AppError('Action is required', { status: 400, code: 'ACTION_REQUIRED' });
317
+ if (!getTenant(tenant_id)) throw new AppError('Tenant not found', { status: 400, code: 'TENANT_NOT_FOUND' });
318
+
319
+ requestCtxStorage.set('tenant_id', tenant_id);
320
+
321
+ // Only agents declared with `define.Agent` are reachable, and their
322
+ // `api.access` gates every action (no rules = denied).
323
+ rest = new useRest({ internal: false, tenant_id });
324
+ const agent = createAgents(tenant_id, rest).get(agentId);
325
+ if (!agent) throw new AppError(`Agent '${agentId}' not found`, { status: 400, code: 'AGENT_NOT_FOUND' });
326
+
327
+ const api = agent.getConfig().api;
328
+ const needsInput = action === 'generate' || action === 'stream' || action === 'object';
329
+ const input = needsInput ? normalizeInput(body) : undefined;
330
+ // `memory: { thread, resource }` or the flat `thread` / `resource`
331
+ const thread = resolveThread(body);
332
+ const resource = resolveResource(body);
333
+ const readOnly = body?.readOnly ?? body?.memory?.readOnly ?? false;
334
+
335
+ await evaluateAccess(api?.access as any, action, rest, `agents:${agentId}`, c, {
336
+ agent,
337
+ action,
338
+ body,
339
+ input,
340
+ thread,
341
+ // The deprecated alias, so a rule written against it keeps working
342
+ threadId: thread,
343
+ });
344
+
345
+ const callOptions: AgentCallOptions = {
346
+ thread,
347
+ resource,
348
+ maxSteps: resolveMaxSteps(body?.maxSteps, agent),
349
+ lastMessages: resolveLastMessages(body?.lastMessages, agent),
350
+ // Injected turns: the same normalization as the input, so a client can
351
+ // seed context without building one big `input` array
352
+ ...(body?.messages ? { messages: body.messages as AgentInput } : {}),
353
+ // Stop the run when the client disconnects (no tokens past the wire)
354
+ signal: c.req.raw.signal,
355
+ ...(body?.files?.length ? { files: body.files } : {}),
356
+ ...(readOnly ? { memory: { readOnly: true } } : {}),
357
+ };
358
+
359
+ if (action === 'stream') {
360
+ return streamResponse(c, agent, input as AgentInput, callOptions, {
361
+ rest, agentId, action, started,
362
+ });
363
+ }
364
+
365
+ let result: AgentGenerateResult | undefined;
366
+ let response: any;
367
+
368
+ switch (action) {
369
+ case 'info':
370
+ // No model call, no token spent
371
+ response = { agent: agent.toJSON() };
372
+ break;
373
+
374
+ case 'history': {
375
+ if (!agent.getMemory()) {
376
+ throw new AppError(`Agent '${agentId}' declares no memory`, { status: 400, code: 'AGENT_NO_MEMORY' });
377
+ }
378
+ // The same `resource` the run used: it namespaces the thread
379
+ response = {
380
+ messages: await agent.getMessages(requireThread(body), { resource }),
381
+ };
382
+ break;
383
+ }
384
+
385
+ case 'threads': {
386
+ if (!agent.getMemory()) {
387
+ throw new AppError(`Agent '${agentId}' declares no memory`, { status: 400, code: 'AGENT_NO_MEMORY' });
388
+ }
389
+ if (!agent.getMemory()?.list) {
390
+ throw new AppError(
391
+ `The memory of agent '${agentId}' does not list threads`,
392
+ { status: 400, code: 'AGENT_MEMORY_NO_LIST' },
393
+ );
394
+ }
395
+ response = { threads: await agent.listThreads({ resource, limit: body?.limit }) };
396
+ break;
397
+ }
398
+
399
+ case 'clear': {
400
+ if (!agent.getMemory()) {
401
+ throw new AppError(`Agent '${agentId}' declares no memory`, { status: 400, code: 'AGENT_NO_MEMORY' });
402
+ }
403
+ await agent.clearMessages(requireThread(body), { resource });
404
+ response = { ok: true };
405
+ break;
406
+ }
407
+
408
+ case 'generate': {
409
+ const generated = await agent.generate(input as AgentInput, callOptions);
410
+ result = generated;
411
+ response = presentResult(generated);
412
+ break;
413
+ }
414
+
415
+ case 'object': {
416
+ const structuredOutput = api?.object;
417
+ if (!structuredOutput?.schema) {
418
+ throw new AppError(
419
+ `Agent '${agentId}' declares no \`api.object\` schema`,
420
+ { status: 400, code: 'AGENT_NO_OBJECT_SCHEMA' },
421
+ );
422
+ }
423
+ const generated = await agent.generate(input as AgentInput, { ...callOptions, structuredOutput });
424
+ result = generated;
425
+ response = { ...presentResult(generated), object: generated.object };
426
+ break;
427
+ }
428
+
429
+ default:
430
+ throw new AppError(`Action '${action}' not found`, { status: 400, code: 'ACTION_NOT_FOUND' });
431
+ }
432
+
433
+ await auditAgent({ rest, agentId, action, input, thread, result, started });
434
+ return c.json(response);
435
+ } catch (err: any) {
436
+ if (cfg?.debug) console.error(err);
437
+ if (rest && tenant_id && agentId) {
438
+ await rest.audit.log({
439
+ action: `agent.${action || 'unknown'}`,
440
+ collection: `_agents_:${agentId}`,
441
+ input: { agent: agentId },
442
+ error: { message: err?.message, code: err?.code || 'INTERNAL_AGENT_ERROR' },
443
+ duration: Date.now() - started,
444
+ }).catch(() => {});
445
+ }
446
+ return errorResponse(c, err);
447
+ }
448
+ });
449
+ }
450
+
451
+ export { initializeAgents, AGENT_PREFIX };