Skip to content

client.agents

A Programmable Agent is a server-side “virtual user” backed by the edge AI runtime. App editors give it a persona (a system prompt), a model, named prompt “skills”, and triggers that decide when it speaks; members then chat with it inside a Space. The agent reads and replies to messages entirely on the accelerator — your client only manages it.

An agent must read message plaintext to function, so a Space it’s enabled on is no longer server-blind — the app’s own infrastructure (the keeper) reads those messages. This is opt-in per Space: every Space the agent isn’t enabled on stays end-to-end-encrypted and blind. The agent appears in the roster flagged as an agent so members can see it can read the conversation.

// List the app's agents.
client.agents.list(appId, opts?): Promise<AgentConfig[]>
// Read one agent (config + its published identity keys).
client.agents.get(appId, agentId, opts?): Promise<AgentProvisioned>
// Create an agent (owner/editor; paid tier).
client.agents.create(appId, input, opts?): Promise<AgentProvisioned>
// Update an agent's editor-settable fields.
client.agents.update(appId, agentId, patch, opts?): Promise<{ config: AgentConfig }>
// Delete an agent (also drops its signing identity + transcripts).
client.agents.delete(appId, agentId, opts?): Promise<{ deleted: boolean }>
// Enable / disable the agent on a specific Space (per-Space opt-in).
client.agents.enable(appId, agentId, spaceId, opts?): Promise<{ config: AgentConfig }>
client.agents.disable(appId, agentId, spaceId, opts?): Promise<{ config: AgentConfig }>

opts is { space?: string } — a non-owner editor passes the app’s management Space id for delegated access; the owner can omit it.

// 1. Create an agent (only an app owner / editor on a paid plan can).
const { config } = await client.agents.create(appId, {
handle: "@assistant",
displayName: "Assistant",
model: "meta/llama-3.1-8b-instruct",
systemPrompt: "You are a concise, friendly teammate in this Space.",
triggers: [{ type: "mention" }], // reply when @-mentioned
});
// 2. Turn it on for the channel you want it in.
await client.agents.enable(appId, config.agentId, channelSpaceId);
// 3. In that channel, a member @-mentions it — the agent replies in the Space.
interface AgentConfig {
agentId: string;
handle: string; // the @-mention target, e.g. "@assistant"
displayName: string;
model: string; // a model id from the catalog (see below)
systemPrompt: string; // the agent's persona / instructions
skills: AgentSkill[]; // named, invokable prompts
triggers: AgentTrigger[];
enabledSpaces: string[]; // Spaces it's active on (per-Space opt-in)
caps: { dailyTokenBudget: number }; // hard daily cost circuit-breaker
tools?: AgentToolsConfig; // optional tool-use grant (see below)
createdAt: number;
updatedAt: number;
}
interface AgentSkill { name: string; prompt: string }
interface AgentTrigger {
type: "mention" | "keyword" | "regex" | "always";
pattern?: string; // required for "keyword" / "regex"
skill?: string; // run this named skill on a match
}

AgentCreateInput is the editor-settable subset (handle, displayName, systemPrompt, and optional model, skills, triggers, caps, tools); AgentUpdateInput is a partial of it.

interface AgentProvisioned {
config: AgentConfig;
memberId: string; // the agent's Space member id (__agent__:<agentId>)
ecdhPub: string | null; // identity keys (the agent signs its messages)
ecdsaPub: string | null;
}

Triggers decide when the agent runs, which bounds its cost:

  • mention — fires when the agent’s handle appears in a message. The direct “talk to the agent” path.
  • keyword — a case-insensitive substring match on pattern.
  • regex — a case-insensitive pattern match.
  • always — every message (pair with a tight caps.dailyTokenBudget).

The first matching trigger wins; its skill (if set) is composed with the system prompt for that reply.

By default an agent’s only output is a chat message. Grant it tools and it can act on your app through a server-side function-calling loop — query and write its database, call its functions, and resolve its channels — then summarize what it did back in the Space.

interface AgentToolsConfig {
enabled: boolean;
db: {
mode: "off" | "read" | "write"; // read = query/get; write = + insert/update/delete
tables: string[]; // table allowlist
};
functions: string[]; // function-name allowlist
channels: boolean; // may resolve the app's public channels
maxIterations: number; // max tool-call rounds per reply (1–6)
}

Pass tools on create/update. The exact columns, function parameters, and the closed list of callable tools are injected into the agent at runtime, so it can’t invent tables or tools.

Rather than hand-writing the prompt and the allowlist, you can declare your app’s agent-facing surface in code and eject both. Annotate a plain class with @MuhkooAgent (identity + behavioral guidance) and its members with @MuhkooSpace, @MuhkooDB, and @MuhkooFunction, then call ejectAgentPrompt and ejectAgentTools:

import {
MuhkooAgent, MuhkooSpace, MuhkooDB, MuhkooFunction,
ejectAgentPrompt, ejectAgentTools,
} from "@muhkoo/connect";
@MuhkooAgent({
name: "Chat Assistant",
purpose: "A helpful assistant inside a real-time team chat.",
guidance: ["Keep replies short.", "Only chime in when relevant."],
})
class ChatAssistant {
@MuhkooSpace({ description: "Main team discussion." }) general!: string;
@MuhkooDB({ access: "read", description: "Chat message history." }) messages!: unknown;
@MuhkooFunction({ description: "Open a support ticket." }) openTicket!: () => void;
}
await client.agents.create(appId, {
handle: "@assistant",
displayName: "Assistant",
model: "openai/gpt-oss-120b", // a function-calling model
systemPrompt: ejectAgentPrompt(ChatAssistant),
tools: ejectAgentTools(ChatAssistant),
});

The ejected prompt carries the semantic layer — what the app is, how to behave, and what each channel, table, and function is for. The runtime supplies the authoritative schema and tool list separately, so the prompt never restates columns and can’t drift from your deployed app. The member name is the default channel/table/function name; override it with name/table.

Since 0.6.0-alpha.1, the ejected prompt also includes a “How to respond” section that compels the agent to finish its turn with a short, plain-language reply after using tools — and never to end with only tool calls, an empty message, or a recitation of its tool list. Without it, some models (notably gpt-oss-*) run their tool loop and then go silent: you see the work happen but get no reply. Toolless agents get the “always reply” rule without the tool-specific lines.

Each agent has a hard caps.dailyTokenBudget circuit breaker. Inference is metered to your account as the ai_inference axis, billed in neurons (a compute unit computed per call from the chosen model’s input/output rates) so every model’s true cost is reflected.

Pick any model from the edge model catalog (e.g. meta/llama-3.1-8b-instruct, meta/llama-3.3-70b-instruct-fp8-fast, mistralai/mistral-small-3.1-24b-instruct). Muhkoo exposes the full catalog with per-model pricing at GET /api/apps/agent-models.