Building a Voice
Create and publish your own Tutti voice package
A voice is an npm package that exports a class implementing the Voice interface. This guide walks through building one from scratch.
Project structure
my-voice/
├── src/
│ ├── index.ts # Voice class + tool exports
│ └── tools/
│ ├── my-tool.ts # Individual tool
│ └── other-tool.ts
├── tests/
│ └── my-voice.test.ts
├── package.json
├── tsconfig.json
└── tsup.config.ts
Step 1: Define a tool
Each tool has a name, description, Zod parameter schema, and an async execute function.
import { z } from "zod";
import type { Tool } from "@tuttiai/types";
const parameters = z.object({
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
});
export const weatherTool: Tool<z.infer<typeof parameters>> = {
name: "get_weather",
description: "Get the current weather for a city",
parameters,
execute: async (input) => {
const response = await fetch(
`https://api.weather.example/v1?city=${encodeURIComponent(input.city)}&units=${input.units}`,
);
const data = await response.json();
return {
content: `Weather in ${input.city}: ${data.temp}° ${input.units}, ${data.condition}`,
};
},
};
:::tip
Return errors as { content: "Error message", is_error: true } instead of throwing. This sends the error back to the LLM so it can try a different approach.
:::
Step 2: Create the voice class
import type { Permission, Voice } from "@tuttiai/types";
import { weatherTool } from "./tools/weather.js";
export class WeatherVoice implements Voice {
name = "weather";
description = "Get weather data for any city";
required_permissions: Permission[] = ["network"];
tools = [weatherTool];
}
export { weatherTool };
Step 3: Configure the package
{
"name": "@yourscope/weather-voice",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"dependencies": {
"@tuttiai/types": "*",
"zod": "3.25.76"
}
}
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
});
Step 4: Write tests
import { describe, it, expect } from "vitest";
import { WeatherVoice } from "../src/index.js";
describe("WeatherVoice", () => {
it("implements Voice with the correct name and permissions", () => {
const voice = new WeatherVoice();
expect(voice.name).toBe("weather");
expect(voice.required_permissions).toEqual(["network"]);
expect(voice.tools.length).toBeGreaterThan(0);
});
});
Step 5: Use it
import { AnthropicProvider, defineScore } from "@tuttiai/core";
import { WeatherVoice } from "@yourscope/weather-voice";
export default defineScore({
provider: new AnthropicProvider(),
agents: {
assistant: {
name: "Weather Assistant",
system_prompt: "You help users check the weather.",
voices: [new WeatherVoice()],
permissions: ["network"],
},
},
});
Step 6: Publish
npm publish --access public
Once published, anyone can install it:
npx tutti-ai add @yourscope/weather-voice
Voice lifecycle hooks
Voices can optionally implement setup() and teardown():
export class BrowserVoice implements Voice {
name = "browser";
required_permissions: Permission[] = ["browser"];
tools = [/* ... */];
async setup(context: VoiceContext) {
// Called before first use — launch browser, connect to DB, etc.
}
async teardown() {
// Called on cleanup — close browser, disconnect, etc.
}
}