@mp-lb/agents-framework
@mp-lb/agents-framework
Backend-resolved agent definitions and runtimes for MAP Lab apps.
Code owns the agent contract: agent ids, instruction prompt keys, input prompt keys, required variables, tools, runtime context, and inspectable metadata. The configured backend owns prompt storage, LLM calls, tracing, and prompt/run metadata.
const agents = createAgentRuntime({
app: "instantagent",
environment: "local",
backend: langfuseBackend({
llm: openAICompatibleLlm({
apiKey: process.env.OPENROUTER_API_KEY,
baseUrl: "https://openrouter.ai/api/v1",
model: "openai/gpt-5.5",
}),
}),
agents: [conciergeAgent],
});
await agents.boot();
const result = await agents.run("concierge", {
context: {
PRODUCT_NAME: "InstantAgent",
userMessage: "Can you check this lease?",
},
});Use prompt(...) for backend-resolved instruction and input templates:
export const conciergeAgent = defineAgent({
id: "concierge",
name: "Concierge",
instructions: [
prompt("instantagent/concierge-system", {
variables: ["PRODUCT_NAME"],
}),
],
input: prompt("instantagent/concierge-input", {
variables: ["userMessage"],
}),
tools: [lookupPolicy],
});Tools
Use defineTool(...) for tools the model can call. A tool definition includes
its Zod input schema and its executable handler, so the framework only advertises
tools it knows how to run:
const lookupPolicy = defineTool({
name: "lookup_policy",
description: "Look up policy text by id",
inputSchema: z.object({ id: z.string() }),
describe: ({ input, output, phase }) => {
if (phase === "started") return `Looking up policy ${input.id}`;
if (phase === "failed") return `Could not look up policy ${input?.id}`;
return `Found policy ${output.id}`;
},
handler: async (input, { context }) => {
return policyStore.forTenant(String(context.tenantId)).lookup(input.id);
},
});Handlers receive schema-validated input. If model output does not match the
schema, the framework emits a structured tool.failed event instead of passing
raw JSON into application code.
If a tool is deliberately unavailable in a runtime path, make that explicit:
const fileDocument = defineTool({
name: "file_doc",
description: "File a generated document",
inputSchema: z.object({ documentId: z.string() }),
handler: notImplementedToolHandler("file_doc"),
});ToolNotImplemented is treated like any other tool failure: it is emitted
through onEvent and captured by tracing/error handling.
await agents.run("concierge", {
context: { PRODUCT_NAME: "InstantAgent", tenantId: "tenant_1" },
input: "Check this lease",
onEvent: (event) => {
agentTelemetry.record(event);
},
});The event stream includes tool.started, tool.succeeded, tool.failed,
tool.missing_handler, model.retry, model.failure,
agent.no_submit_tool_call, and agent.degraded_completion.
Tool lifecycle events include event.narration, using the tool's deterministic
describe callback when present and a generic fallback otherwise.
Use describe for UI progress copy that should not depend on model-generated
text:
const requestSignature = defineTool({
name: "request_signature",
description: "Create and email a contract signing request.",
inputSchema: z.object({
contractId: z.string(),
recipientEmail: z.email(),
}),
describe: ({ error, input, output, phase }) => {
if (phase === "started") {
return `Sending contract to ${input.recipientEmail}`;
}
if (phase === "failed") {
return `Could not send contract to ${input?.recipientEmail}: ${String(error)}`;
}
return `Contract sent to ${output.recipientEmail ?? input.recipientEmail}`;
},
handler: async (input) => {
return signatureService.request(input);
},
});Migrating Existing Tools
defineAgentTool(...) remains available for older call sites. During migration,
you can either add handler/execute to the tool definition or keep passing a
per-run toolHandlers map:
const lookupPolicy = defineAgentTool({
name: "lookup_policy",
description: "Look up policy text by id",
inputSchema: z.object({ id: z.string() }),
});
await agents.run("concierge", {
input: "Check this lease",
toolHandlers: {
lookup_policy: ({ toolCall, context }) => {
const input = toolCall.input as { id: string };
return policyStore.forTenant(String(context.tenantId)).lookup(input.id);
},
},
});Before every model call, the framework validates that each advertised non-submit
tool has either a definition-level handler or a legacy toolHandlers entry.
Missing handlers emit tool.missing_handler and fail before the model sees the
tool. Submit tools are framework-handled because their validated input is the
agent's final result.