In framework-first apps, "calling another agent" usually means calling ordinary TypeScript. Put the reusable work in a function, import it from the route or worker that owns the trigger, and use Agentuity clients inside that function when it needs service state.
See Agents for model-backed workflows and Background Work for moving slower work out of request handlers.
Shared Function
npm install @agentuity/keyvalue zodimport { KeyValueClient } from '@agentuity/keyvalue';
import { z } from 'zod';
const inputSchema = z.object({
userId: z.string(),
message: z.string(),
});
const outputSchema = z.object({
userId: z.string(),
previousMessage: z.string().nullable(),
message: z.string(),
});
type EnrichOutput = z.infer<typeof outputSchema>;
const kv = new KeyValueClient();
export async function enrichMessage(body: unknown): Promise<EnrichOutput> {
const input = inputSchema.parse(body);
const previous = await kv.get<string>('messages', input.userId);
const output = {
userId: input.userId,
previousMessage: previous.exists ? previous.data : null,
message: input.message,
} satisfies EnrichOutput;
await kv.set('messages', input.userId, input.message, {
ttl: 60 * 60 * 24 * 30,
});
return outputSchema.parse(output);
}The function owns validation and service calls. Any server entry point can call it without recreating a separate app registry.
Framework Route
import { enrichMessage } from '@/workflows/enrich-message';
export async function POST(request: Request): Promise<Response> {
const result = await enrichMessage(await request.json());
return Response.json(result);
}Use the same function from a server action, queue consumer, cron job, or script when that trigger should run the same work.
Hono Route
Use @agentuity/hono when you want Hono context variables for shared service clients.
npm install @agentuity/hono honoimport { Hono } from 'hono';
import { agentuity } from '@agentuity/hono';
import { enrichMessage } from './workflows/enrich-message';
import type { Logger } from '@agentuity/hono';
interface Variables {
readonly logger: Logger;
}
const app = new Hono<{ Variables: Variables }>();
app.use('*', agentuity());
app.post('/messages', async (c) => {
const result = await enrichMessage(await c.req.json());
c.var.logger.info('message enriched', { userId: result.userId });
return c.json(result);
});
export default app;Direct clients are still the portable default. Hono middleware is useful when a route should receive clients and telemetry through c.var.*.
Parallel Calls
Use Promise.all when independent workflows can run at the same time.
import { enrichMessage } from './enrich-message';
import { scoreMessage } from './score-message';
import { routeMessage } from './route-message';
export async function processMessage(body: unknown): Promise<{
readonly enriched: Awaited<ReturnType<typeof enrichMessage>>;
readonly score: Awaited<ReturnType<typeof scoreMessage>>;
readonly route: Awaited<ReturnType<typeof routeMessage>>;
}> {
const [enriched, score, route] = await Promise.all([
enrichMessage(body),
scoreMessage(body),
routeMessage(body),
]);
return { enriched, score, route };
}Keep sequential await when the second call depends on the first result.
Typed Outputs
Export function return types from the implementation instead of duplicating output shapes in callers.
import { enrichMessage } from './enrich-message';
export type EnrichedMessage = Awaited<ReturnType<typeof enrichMessage>>;
export function formatEnrichedMessage(message: EnrichedMessage): string {
return `${message.userId}: ${message.message}`;
}Migration Note
If an older app still imports agents from @agentuity/runtime and calls .run(), treat that code as migration context. New v3 code should use framework routes, shared functions, and standalone service clients.