Configuration over code: why score files beat Python graphs

Agent systems aren't general programs. They're a small set of repeating patterns. When you write them as code, the configuration disappears. When you write them as config, the system shape is the file.

Chihab
Building Tutti AI · · 7 min read

The first big decision in Tutti was that agents are configured, not coded. Here's why.

A typical multi-agent setup in LangGraph looks like a Python program. You define nodes, define edges, wire them with imperative code, run the graph. It's powerful — you can express any topology — and it's also a dense piece of code that nobody on your team will read on a Tuesday morning.

The trick is that agent systems aren't general programs. They're a small set of repeating patterns: a handful of agents, some delegation, some tools, some guardrails, some budgets. The interesting thing isn't the control flow. It's the configuration — which agents exist, what voices they have, what they're allowed to do.

When you write that in code, the configuration disappears into the program. You have to read the whole graph to understand the system. PR reviews become trace-by-trace exercises. New team members spend a week figuring out what a working baseline looks like.

When you write it as configuration, the system shape is the file. You can read it linearly, diff it across a PR, hand it to a non-engineer for review, and computer-validate it before it ever runs.

What a Tutti score file looks like

```ts import { defineScore, AnthropicProvider } from '@tuttiai/core' import { GitHubVoice } from '@tuttiai/github' import { FilesystemVoice } from '@tuttiai/filesystem'

export default defineScore({ provider: new AnthropicProvider(), agents: { reviewer: { name: 'reviewer', model: 'claude-sonnet-4-6', system_prompt: 'You review pull requests. Read the diff and the affected files, then post a structured review.', voices: [new GitHubVoice(), new FilesystemVoice()], permissions: ['network', 'filesystem'], }, }, }) ```

Twenty lines, three imports, one `defineScore` call. Every important decision — model, system prompt, voices, permissions — is in plain sight. Every value is type-checked at edit time, not at run time.

It's still TypeScript. That's the point.

A common pushback is that "configuration" smells like YAML, with all the brittleness that implies. Score files aren't that. They're TypeScript objects validated by Zod schemas. You get IDE autocomplete on every field, compile-time errors when you reference a missing agent, real imports for voices and providers (no string-based dependency injection), and loops, conditionals, or `process.env` access when you actually need them.

If your config needs a small piece of code, you write it. The point isn't to ban code; it's to make the common case — "what does this system look like?" — boringly readable.

What we give up

Honest about trade-offs: you can't express arbitrary control flow in a score file. If you need a multi-step workflow with conditional branches, dynamic spawning, or stateful loops, you write that on top of the runtime — using `AgentRouter` for delegation, or imperatively calling `runtime.run()` from your application code.

Score files are the right abstraction for the 90% of agent-system work that's repetitive. For the other 10%, the runtime is a regular TypeScript API and you reach for it directly.

Why it matters at 3am

Three months from now, when something is paging your on-call, the engineer who picks up the page doesn't want to debug a Python graph. They want to read the score file, see exactly what the agent is allowed to do, and ship a PR that flips one flag. Configuration makes that possible. Code rarely does.

Tags #score #config #design
Older post
Why I'm building Tutti AI
6 min · Company
Newer post
Voices: the plugin model agent frameworks need
6 min · Engineering

Start conducting.

One install. Your first agent running in 60 seconds. No signup. No telemetry.