Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

Call Barry from a bag

Use this when a bag’s code needs Barry itself: a tool that records an event, a script that lists an identity’s sessions, a service that starts an action. The Barry object from @barry-rocks/sdk answers for identities, sessions, bags, actions, events, schedules, and services, with the same calls in every process. The example is pulse, a bag that holds two tools plus a report script.

  • Create a bag.
  • Know which process your code runs in. A tool runs inside Barry’s tool host, and a service or a script runs on its own. The difference decides how you connect.

Barry’s tool host installs every subject Barry answers for, so a tool calls Barry directly, with no setup. The host names no caller, so these calls act as the installation’s owner, with full access. To act with a session’s access instead, call through Barry.as({ kind: "session", identity, sessionId }), which needs the session’s identity as well as its ID.

import { Barry } from "@barry-rocks/sdk";
import { z } from "zod";
export const announce = Barry.Tool({
namespace: "pulse",
name: "announce",
access: "write",
description: "Record an event that shows up in Barry's event feed.",
schema: {
title: z.string().min(1).max(500).describe("One line on what happened"),
body: z.string().optional().describe("Details"),
},
handler: async ({ title, body }, context) => {
const event = await Barry.recordEvent({
type: "notification",
severity: "info",
title,
...(body ? { body } : {}),
...(context?.sessionId ? { session: context.sessionId } : {}),
});
return { id: event.id, createdAt: event.createdAt };
},
});
export const scheduleHealth = Barry.Tool({
namespace: "pulse",
name: "schedule_health",
access: "read",
description: "Say whether the scheduler is alive and which schedules keep failing.",
schema: {},
handler: async () => {
const [scheduler, schedules] = await Promise.all([Barry.scheduler(), Barry.schedules()]);
return {
scheduler: scheduler.status,
failing: schedules.filter((schedule) => schedule.consecutiveFailures > 0).map((schedule) => schedule.id),
};
},
});

An event’s type is progress, notification, task_finished, or system_alert, and its severity is info, warn, error, or success.

Any other process connects first. Barry.connect points every later call at one Barry over HTTP, and localConnection from @barry-rocks/sdk/services finds this machine’s Barry and its instance secret. Barry.at returns a separate Barry for another installation, with its own address and credential, and leaves the default connection alone.

import { Barry, isBarryError } from "@barry-rocks/sdk";
import { localConnection } from "@barry-rocks/sdk/services";
/** This machine's Barry, or another one when BARRY_REMOTE_URL names it. */
async function connect(): Promise<Barry> {
const remote = process.env.BARRY_REMOTE_URL;
if (remote) return Barry.at({ baseUrl: remote, secret: process.env.BARRY_REMOTE_SECRET });
Barry.connect(await localConnection());
return Barry;
}
const identity = process.argv[2];
if (!identity) {
console.error("usage: report.ts <identity>");
process.exit(2);
}
const barry = await connect();
try {
for await (const session of barry.identity(identity).sessions({ active: true })) {
console.log(`${session.id} ${session.status} ${session.name}`);
}
const unread = await barry.events({ unread: true });
console.log(`${unread.length} unread event(s)`);
} catch (error) {
if (!isBarryError(error)) throw error;
console.error(`${error.reason}: ${error.message}${error.hint ? `\n${error.hint}` : ""}`);
process.exitCode = 1;
}

A process that neither hosts Barry nor connects gets a BarryError with the reason unavailable on its first call. The bag declares the script under scripts:, so Barry can run it on demand:

name: pulse
description: Reports on this Barry, its sessions, events, and schedules, by calling Barry from bag code.
scripts:
report:
description: Print an identity's active sessions and the unread event count.
entry: src/report.ts

Barry runs a declared script as barry <bag> <script> [args...], so this one runs as barry pulse report <identity>. It starts the script in the bag’s directory, passes the words after the script’s name as arguments, and hands the script’s exit code back as its own. A bag’s command group appears only for a bag listed under bags: in cli.yaml, in the settings directory that barry config show prints, so add pulse to that list first.

Barry runs a TypeScript entry through tsx’s loader, which it finds in the bag’s own node_modules, so the bag lists tsx beside the SDK:

{
"name": "pulse-bag",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run"
},
"dependencies": {
"@barry-rocks/sdk": "link:../barry/sdk",
"tsx": "4.23.12",
"zod": "4.4.3"
},
"devDependencies": {
"vitest": "4.1.0"
}
}

Naming a thing returns a handle, such as Barry.identity("bonanza") or Barry.schedule("standup/sweep"). A handle makes no request until you call a method on it, and a misspelled name fails on that first call.

A method that returns many items returns a collection. Await it to get every item as an array, or iterate it with for await to fetch one page at a time:

  • Barry.identities(), and an identity’s sessions(query)
  • a session’s messages(query)
  • Barry.events(query)

The rest of what Barry answers, by subject:

Subject Calls
Identities identity(ref) with get, update, traits, grants, tools, session(id), and start(input); createIdentity(input)
Sessions A session handle’s get, send(text), events() for live events, changes, end, and archive
Bags bags(), and bag(name) with tools, store(name), and job(name).run(inputs)
Actions actions(), and action(name) with run(inputs) and runs()
Events events(query) and recordEvent(input)
Schedules schedules(), scheduler(), schedule(id).runs(), scheduleOnce(input), oneOffs(query), and oneOff(id).cancel()
Services services(), and service(key) with get, start, stop, and restart

scheduleOnce runs an action once at dueAt, with optional inputs, identity, traits, bounds, from 1 to 5 attempts, and an idempotencyKey.

Some calls work only in a process that hosts Barry, among them an identity’s or a session’s grants, a session’s live events, a bag’s tools, and listing closed sessions. Over HTTP, those fail with the reason unavailable and a hint.

Every refusal is a BarryError. Its reason is one of invalid, forbidden, not_found, conflict, or unavailable; its message says what happened; and its hint, when present, says what to do. Test for it with isBarryError, which also recognizes an error from another copy of the SDK bundled into a different bag.

To call the tools from the terminal, install, pack, and grant the bag as Create a bag describes, and run:

Terminal
barry install ~/repos/pulse
bonanza pack pulse
bonanza identity traits add pulse
bonanza tool pulse announce "Deployed the docs"
bonanza events list

The event list shows Deployed the docs as a notification. To run the script against this machine’s Barry, run barry pulse report <identity> with your identity’s name: it prints each active session’s ID, status, and name, then the unread event count. Run as an identity’s command, such as bonanza pulse report <identity>, a script needs that identity’s unbounded Bash grant.

  • A call fails with unavailable and says nothing answers. The process doesn’t host Barry and never connected. Call Barry.connect first.
  • A call fails with not_found. A handle names an identity, a session, or a schedule that doesn’t exist. Check the name.
  • A call fails with forbidden. The caller may not do it. A call through Barry.as with a session caller has only that session’s identity’s access, and a paired device has only its grant.
  • Barry answers the script with Unknown command. The bag isn’t listed in cli.yaml. Add pulse under bags:.
  • For anything else, see Troubleshooting.