npm.io
0.6.0 • Published 5d ago

@mp-lb/agents-framework

Licence
MIT
Version
0.6.0
Deps
1
Size
145 kB
Vulns
0
Weekly
1.5K

@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.

Keywords