@jsm-mit/agent-platform-canister-package
v0.4.0
Published
Wrapper TypeScript package for the agent-platform-canister canister.
Readme
Agent Platform Canister Package
A TypeScript library for interacting with the agent-platform-canister canister
on the Internet Computer — agent configs (base fields + freeform custom JSON)
and batched chat history for the agent platform. The wrapper is the only
sanctioned way to talk to the canister: it hides HttpAgent/idlFactory
wiring, unwraps the canister's Framework.Result<T> into "resolves with T
or throws a known Error", and converts candid opt/variant/Principal
shapes into idiomatic TypeScript.
Part of workspace-agent-platform; the canister itself lives in the sibling
../agent-platform-canister. Concept/architecture design lives in
~/workspace-jsm-company/jsm-ideas/agent-platform/.
Installation
npm install @jsm-mit/agent-platform-canister-packageUsage
Initialization
import {
AgentConfigsActor,
ChatHistoryActor,
AdminActor,
} from "@jsm-mit/agent-platform-canister-package";
const canisterId = "your-canister-id";
const agentConfigsActor = new AgentConfigsActor(canisterId, identity); // identity optional
const chatHistoryActor = new ChatHistoryActor(canisterId, identity);
const adminActor = new AdminActor(canisterId, identity);Permissions model
Everything is admin-gated. A fresh canister has no admins — the first caller
of adminActor.registerAsAdminAsyncUnsafe() claims the role (e.g.
agents-village's own service identity); further admins via
addAdminAsyncUnsafe.
Agent configs
const config = await agentConfigsActor.upsertAgentConfigAsyncUnsafe({
id: "kasia",
name: "Kasia",
persona: "Jesteś Kasią, sprzedawczynią mebli...",
disclosesAsAi: false,
// tools defaults to [] when omitted (a tool-less agent); customConfigJson too is
// an optional escape hatch, JSON-encoded.
tools: [
{ tool: "check_availability", paramsJson: '{"serviceId":"sultana"}' },
],
});
// Same call again with the same id updates in place — createdAt is preserved.
const fetched = await agentConfigsActor.getAgentConfigAsyncUnsafe("kasia");
const all = await agentConfigsActor.listAgentConfigsAsyncUnsafe(500);
await agentConfigsActor.deleteAgentConfigAsyncUnsafe("kasia");Chat history — batched, mixed conversations/agents per call
A single flush from agents-village can mix entries from many conversations
and agents — that's the whole point of flushing a buffer as one call rather
than one call per message (AGENT-PLATFORM decision 2026-08-18). Any entry
naming an unknown agentId refuses the whole batch.
await chatHistoryActor.appendMessageBatchAsyncUnsafe([
{
conversationId: "conv-1",
agentId: "kasia",
message: { role: "user", content: "hi", timestamp: 0n },
},
{
conversationId: "conv-2",
agentId: "lilly",
message: { role: "user", content: "hey", timestamp: 0n },
},
]);
const history = await chatHistoryActor.getConversationHistoryAsyncUnsafe(
"conv-1",
100,
);Config change notifications (0.3.0)
The canister can report every real create, change or delete of an agent config
as one task on a rabbit-motoko channel, so agents-village reloads only that
agent. commonId is the agent id, the payload is
{"agentId":"<id>","change":"upserted"|"deleted"}. Writing identical values
and refused writes report nothing; a failed report is only logged on the
canister, the write stands. The agent-platform canister must be allowed to add
tasks on the channel in rabbit. Admin only.
await adminActor.setConfigChangeNotificationsAsyncUnsafe({
rabbitCanisterId: "6aigz-vyaaa-aaaag-ayjqa-cai",
channel: "agent-config-changes",
});
const settings = await adminActor.getConfigChangeNotificationsAsyncUnsafe(); // or null
await adminActor.setConfigChangeNotificationsAsyncUnsafe(null); // switch offAgent placement: village and tags (0.4.0)
Every agent lives in a village — the agents-village process that runs it,
"Development" unless placed elsewhere — and wears tags, labels the
dashboard filters by (the canister attaches no meaning to them). Both come back
on every AgentConfigView. On upsert they are optional: left out, a create lands
in "Development" with no tags and an update keeps what is stored, so a caller
that never heard of villages cannot move an agent. setAgentTagsAsyncUnsafe
replaces the tags alone (the creating app and admins); a different set counts as
a config change and is reported like any other.
const placed = await agentConfigsActor.upsertAgentConfigAsyncUnsafe({
id: "kasia",
name: "Kasia",
persona: "...",
disclosesAsAi: true,
village: "SultanaProd",
tags: ["salon", "vip"],
});
const retagged = await agentConfigsActor.setAgentTagsAsyncUnsafe("kasia", [
"vip",
]);
const mine = (await agentConfigsActor.listAgentConfigsAsyncUnsafe(500)).filter(
(c) => c.village === "Development",
);Village: 1–50 chars, letters, digits, - and _, starting with a letter or
digit. Tag: 1–30 chars, lowercase letters, digits and -, starting with a
letter or digit; at most 20 per agent, no duplicates — InvalidData otherwise.
Error model
Every ...AsyncUnsafe method throws one of exactly three errors — switch on
error.message, details are always on error.cause:
CanisterError— the canister returned a business#err;cause = { errorKey, errorMessage, logs }(e.g.errorKey: "NotAuthorized","NotFound","InvalidData").CriticalCanisterError— transport failure or a canister trap;cause = { logs, rawError }.CallRefusedAtInspectionStage— the canister'sinspectgate refused the call before execution (oversized payload or anonymous caller).
API
AgentConfigsActor—upsertAgentConfigAsyncUnsafe,setAgentPersonaAsyncUnsafe,setAgentTagsAsyncUnsafe,getAgentConfigAsyncUnsafe,listAgentConfigsAsyncUnsafe,deleteAgentConfigAsyncUnsafe.ChatHistoryActor—appendMessageBatchAsyncUnsafe,getConversationHistoryAsyncUnsafe.AdminActor— admin allowlist and diagnostics:registerAsAdminAsyncUnsafe,addAdminAsyncUnsafe,removeAdminAsyncUnsafe,getAdminsAsyncUnsafe,setLoggingEnabledAsyncUnsafe,whoAmIAsyncUnsafe,getLogsAsyncUnsafe,clearLogsAsyncUnsafe,setConfigChangeNotificationsAsyncUnsafe,getConfigChangeNotificationsAsyncUnsafe(ConfigChangeNotificationsSettings | null).- Configs/messages are returned as
AgentConfigView/ChatMessageView(opts flattened to optionals, the message role variant as a"user" | "assistant" | "tool"string union); raw candid types remain available from.and./declarations/*exports.
Development
See CLAUDE.md for the canister-change workflow (declarations sync, wrapper
updates). The scripts below deploy to mainnet and spend cycles — the user runs
them; Claude never does.
npm run test-suite— reinstalls the current canister build intoshort-living, the short-lived test canister ofmotoko-crafting-table, with a 3-minute lease (no prompt), then runstests/test-suite.tswithCANISTER_IDset toshort-living. No.env: every identity is generated per run. Tests never touchbackend5.npm run test-smoke— the same, withtests/test-smoke.ts: one write and one read per domain (~20 seconds).publish-publicruns it too.npm run redeploy -- [slot] [reinstall|upgrade] [--force] [--wipe]— deploys the canister build without tests through../agent-platform-canister'scraft:upgrade/craft:deploy. Defaults: slotbackend5(the live agents of agents-village), mode upgrade (state kept). A reinstall of any slot butshort-livingneeds--wipeand prints the restore steps (registerAsAdmin,addAdminthe village,addAppSultana core,setConfigChangeNotifications).npm run publish-public— bump the version by hand (uncommitted), then run it frommaster. It checks both repos are on a cleanmasterequal toorigin/masterand the version is unpublished, reinstalls the canister master intoshort-living(lease 2 minutes) and refuses unless the candidshort-livingserves equals the committeddeclarations/and the smoke test passes against that deploy. Then it stamps the canister commit intopackage.json, builds, publishes and commits "Version bump to ".
Requirements
- Node.js 18+
- TypeScript 5+
License
ISC
