Appearance
Custom Tools
When the built-in providers don't cover something, register your own in TypeScript. A provider is one async function. Every tool declared with that provider name calls it.
1. Register a provider
ts
import { HaloRuntime, HaloError, parse, type ToolProvider } from "halo";
const jira: ToolProvider = async (call, { fetch }) => {
// call.tool the declaration (name, provider, args)
// call.method e.g. "create_issue"
// call.positional positional args (defaults to [flowing value])
// call.named named args
// call.config(k) a declaration arg by name or index, with Env.* resolved
// call.viaAgent true when called as Agent.task(tool: Jira, ...)
// call.signal abort signal (timeouts, cancellation)
if (call.method !== "create_issue") throw new HaloError(`Jira has no method "${call.method}"`);
const res = await fetch(`https://${call.config("site")}/rest/api/2/issue`, {
method: "POST",
headers: { Authorization: `Bearer ${call.config(0)}`, "Content-Type": "application/json" },
body: JSON.stringify({
fields: { project: { key: call.named.project }, summary: call.named.title, issuetype: { name: "Task" } },
}),
signal: call.signal,
});
return res.json(); // becomes the stage's value
};
const runtime = new HaloRuntime().registerToolProvider("Jira", jira).load(parse(source));The second argument gives the provider access to the runtime's fetch (useful in tests), the local vector store, and a pooled Bun.SQL connection.
2. Declare and call it
halo
tool Tickets : Jira(Env.JIRA_TOKEN, site: "acme.atlassian.net")
agent PM [openai/gpt-4o-mini]
equips Tickets
pipeline FileTicket(spec: String) -> Result:
spec
|> PM.title("Write a one-line ticket title for this spec.") as title
|> Tickets.create_issue(project: "ENG", title: context.data.title)halo validate warns that Jira is an unknown provider (it knows built-ins and installed tool packages). Pass { toolProviders: ["Jira"] } to validate()/compile() to silence the warning in your own tooling.
A tool runs when the pipeline calls it, either directly as above or through an agent with PM.create_issue(tool: Tickets, ...), which calls the tool first and gives the agent its result. For an agent to choose its own calls (PM.plan(tools: [Tickets]), see Agents with Tools), register the provider with a description of its methods:
ts
runtime.registerToolProvider("Jira", jira, {
description: "Jira issues",
methods: [
{
name: "create_issue",
description: "Create a Task in a project.",
params: { type: "object", properties: { project: { type: "string" }, title: { type: "string" } }, required: ["project", "title"] },
effect: "external",
},
],
});The model reads each method's description and params (JSON Schema). effect (read, write, or external) is what tool rules and halo plan go by. methods can also be an async function of ({ tool, config, signal }, services), for a provider that asks a server what it offers. Without this third argument, an agent call that offers the tool fails with TOOL_CONFIG.
To share a provider with other projects, publish it as a tool package.
System methods
For logic that isn't a tool, register a System method. If it returns a value other than undefined, that value replaces the flowing value.
ts
runtime.registerSystem("Slugify", ({ current }) => String(current).toLowerCase().replace(/\W+/g, "-"));System.Slugify() then works in pipelines. Pass systemMethods: ["Slugify"] to the validator. onSystemEvent receives System.Publish, Distribute, ResolveIncident, TriggerWebhook, and Notify.
Model providers
runtime.registerAdapter(adapter) adds a model provider. An adapter implements ProviderAdapter: id, envKeys, canHandle(modelId), getUrl, getHeaders, transformRequest, parseResponse, and optionally estimateCost. For an OpenAI-compatible API, extend the exported OpenAICompatibleAdapter:
ts
import { OpenAICompatibleAdapter } from "halo";
runtime.registerAdapter(new OpenAICompatibleAdapter({
id: "groq", prefixes: ["groq/"], baseUrl: "https://api.groq.com/openai/v1", envKeys: ["GROQ_API_KEY"],
}));Adapters registered later take precedence. If your model ids need a new prefix, the CLI's validator flags those agents, because it only knows the built-in adapters.