How to Test Your Agents on Real Tasks With Harbor - Xander Song, Arize AI
Read the talk
How to Test Your Agents on Real Tasks With Harbor
Xander Song explains how isolated environments, outcome-based verifiers and reference solutions make agent changes testable, then follows a Phoenix prompt change that reduced tool calls while preserving passing results.
From a talk by Xander Song
At a glance
Ideas worth remembering
A Harbor task combines instructions, a seeded environment and verifier scripts. Independent trials start fresh; a job collects their outcomes and optional trajectories.
Run an oracle solution before spending tokens on agents to check that the environment, starting data and verifiers work together.
Outcome-based verifiers tolerate implementation changes. Trajectory-aware checks are useful for tool selection and skill triggering, but create additional maintenance coupling.
The SQL prompt experiment increased aggregate SQL use and reduced tool calls and turns while preserving passing rewards. Task-level comparisons and traces explained where behavior changed; larger-data benefits were not measured.
Choose the evaluation unit explicitly. Phoenix tests agent-plus-product integrations against seeded data, not the customer application or its production ingestion path; realistic fixtures determine which failures those tests can expose.
Hold the task constant while the agent changes
Changing an agent’s prompt, tools or harness creates a practical problem: how do you know whether the new version improved anything? Xander Song, an engineer at Arize AI working on Phoenix, introduces Harbor as a way to answer that question with realistic tasks and observable results. He approaches it as a Harbor power user rather than a maintainer, with a goal he calls shipping on “evidence rather than vibes.”
Harbor gives an agent natural-language instructions inside a fresh sandbox containing relevant data. After the agent finishes, task-specific scripts called verifiers inspect what it left behind. A verifier produces a reward—for example, 0 or 1 for failure or success. The test can therefore ask whether a database changed correctly or a file was repaired, rather than whether the agent’s final message sounds convincing.
The useful comparison holds the task, starting data and success criteria constant while changing the agent implementation. Run both versions through the same setup, and the rewards provide a signal of improvement or regression. Collecting many tasks extends that signal across the capabilities you care about. Its value still depends on two choices: whether the tasks resemble real work, and whether the verifiers actually describe success.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Tasks define the work; trials start fresh
A Harbor task packages three distinct inputs:
- Instructions: A Markdown file contains the request the agent should receive.
- Environment: Typically described by a Dockerfile, this supplies the software and starting data. The data might be a SQLite file or a Git repository checked out at a particular commit; Harbor does not prescribe one data type.
- Verifiers: Scripts in a test directory inspect the environment after the agent runs and determine whether the intended effects occurred.
A collection of tasks is a dataset. For each task, Harbor can run a configurable number of trials, because an agent may behave differently on repeated attempts. Each trial gets a new container seeded with the task’s data, receives the instruction file and runs independently. The agent may edit source files, alter a database or make other changes. Once it stops, the verifier runs against that resulting environment and writes rewards to reward.txt.
Rewards tell you whether the run met the test. Optional ATIF trajectories help explain how it got there. ATIF stands for Agent Trajectory Interchange Format, Harbor’s JSON-based tracing specification for recording the agent’s internal steps. A job is the complete execution: tasks, their trials, verifier rewards and any collected trajectories.
Before paying for agent runs, you can substitute an oracle: a script that solves the task. Running a known solution through the environment and verifiers helps expose bad data, configuration problems and checks that reject a valid result. This separates debugging the evaluation setup from debugging the agent. An oracle is especially useful when a failed run could otherwise mean either “the agent failed” or “the test never worked.”
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A migration test grades the database it leaves behind
Consider Song’s hypothetical coding task: fix a failing migration. The instruction file asks for the repair in ordinary user language. The environment contains SQL, database definitions and source code for an up migration, with starting data arranged so that the migration fails. The agent under test could be a coding agent alone, one configured with skills, or one connected to a custom MCP server.
What would count as a repair? The verifier can run the up migration and require exit status 0, then inspect the resulting tables for the expected columns and indexes. These checks cover both execution and effect: a command that exits successfully is insufficient if it leaves the wrong schema. An optional oracle script supplies a literal repair, allowing the same checks to be exercised before the agent enters the picture.
The agent edits the broken migration file and stops. In the typical local Docker setup Song describes, the verifiers are mounted into the container only afterward; the agent cannot access the verifiers or reference solution during its attempt. That keeps the repair task separate from the machinery that grades it. The verifier then executes the migration and inspects the schema the agent’s changes actually produce.
This is the integration-testing analogy behind Harbor. The canonical verifier treats the agent as a black box: it cares about the final files, output and system state, not the precise sequence of tool calls. A different implementation can pass the same test. That makes it possible to “refactor under green”—replace the agent’s internals while retaining a meaningful check of its behavior.
The distinction came from maintenance pain on the Phoenix team. Tests coupled to particular tools broke when a tool changed even though the agent could still accomplish the task. Outcome-based tests support the team’s effort to consolidate reusable skills across CLI, MCP and server-side agent experiences. Harbor can inspect trajectories too, but making a particular execution path part of the test is a separate decision with a maintenance cost.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Independent trials can run in parallel
Fresh environments make the work horizontally scalable. One trial does not need another trial’s files or database changes, so a sandbox provider can run many at once. Song names Daytona, Modal and E2B as compatible examples; his team uses Daytona with about 200 concurrent workers. The point is to shorten the feedback time for a suite without making trials share state.
How should you read the task-by-agent-by-trial comparison? Song’s example has four tasks, two coding agents and three attempts per task-agent combination. Every attempt occupies a separate container with an identical starting state for that task. The agents can then take different paths. The comparison separates differences between agents from the variation across repeated attempts; it does not show one agent progressively repairing the same environment.
Those individual outcomes can be aggregated. In the example, pass@3 asks whether an agent solved a task in at least one of three independent attempts. Both agents pass the migration task under that criterion, while another task is passed by only one. Aggregating across the dataset gives a broader comparison, but the interpretation remains success within the allowed attempts—not a guarantee that the next single attempt will succeed.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Phoenix adds experiment history and inspectable traces
Phoenix maps Harbor’s evaluation objects into its own observability and experimentation model. Datasets remain datasets, jobs become experiments, trials are recorded, and verifier rewards become evaluations. This gives the team a place to compare runs and track whether the metrics it cares about move in the intended direction.
The integration also converts ATIF trajectories into OpenInference, semantic conventions built on OpenTelemetry. Those conventions describe AI operations in forms Phoenix can interpret. An LLM call and a tool call can receive specialized displays instead of appearing only as undifferentiated log records. The migration example’s sequence of agent steps can therefore be inspected as a trace.
Dataset versioning preserves what a task looked like for a particular job, while experiment views show rewards and other metrics across changes. Phoenix also exposes this data through skills, CLIs and an MCP server so agents can help investigate runs. None of that makes Phoenix a prerequisite: Song notes that Harbor has its own local open-source UI and a hosted service. Phoenix is an additional way to organize and inspect the evidence.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Put the application and its agent interfaces in the sandbox
Phoenix is both the tool used to inspect these experiments and the application being tested. In its ordinary architecture, an instrumented user application emits traces over OTLP, the OpenTelemetry Protocol. Phoenix ingests those traces into SQLite or Postgres and exposes services for traces, spans, sessions, datasets, experiments and evaluations through REST and GraphQL.
The team maintains three ways for agents to work with those services:
- Coding agent with a CLI: Commands invoke the REST or GraphQL APIs.
- Coding agent with MCP: An MCP server exposes access to those APIs.
- Server-side agent: The team’s own harness uses the MCP server and also has a Bash emulator.
Which routes reach the same application services? The diagram keeps the three alternatives separate while showing their shared backend. They are supported experiences to test, not three agents that must cooperate on every task.
Phoenix has a convenient testing property: the server runs on a single node. The team can package the running application, APIs and a SQLite file containing realistic data into Docker, replacing the browser with a test client. A task can then ask an agent to find hallucinated LLM calls from the last week of traces and add them to a dataset. This exercises a product workflow rather than merely asking the agent to edit a repository.
After the attempt, a verifier can check the result through a REST endpoint or query SQLite directly—for example, to confirm that the expected dataset record was written. The same application state can support different agent interfaces while the success check remains about the intended database change.
Three agent experiences reach shared Phoenix services
Read the diagram as text
- Coding agent + CLI. agent-experiences — Uses Phoenix commands to access application data.
- Coding agent + MCP. agent-experiences — Uses the configured Phoenix MCP server.
- Server-side agent. agent-experiences — Team-maintained harness with MCP access and a Bash emulator.
- Phoenix CLI. interfaces — Invokes REST or GraphQL.
- Phoenix MCP server. interfaces — Exposes tools backed by REST and GraphQL.
- REST / GraphQL → core services. application — Traces, spans, sessions, datasets, experiments and evaluations over SQL-backed data.
- Coding agent + CLI → Phoenix CLI: Runs commands.
- Coding agent + MCP → Phoenix MCP server: Calls exposed tools.
- Server-side agent → Phoenix MCP server: Uses MCP access.
- Phoenix CLI → REST / GraphQL → core services: Invokes REST or GraphQL.
- Phoenix MCP server → REST / GraphQL → core services: Invokes REST or GraphQL.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Harbor orchestrates from outside the environment
An audience question clarifies two important details. First, each trial tests only one selected agent configuration. Second, Harbor itself runs on a host machine, not inside the task’s Docker environment. That host might be a laptop or a GitHub Actions runner. The environment can run locally in Docker or remotely through a sandbox provider such as Daytona.
The host-side CLI coordinates execution through scripts and directory conventions. The setup must supply the expected scripts for starting work, running verifiers and running an oracle. Harbor starts the appropriate stages; the task environment supplies the application-specific behavior. This is why a sandbox can contain more than source files: Phoenix includes a running server, APIs, a CLI, data and skills available on the filesystem.
A distributed application makes the packaging problem harder. Song has not solved that case in his own work. He suggests exploring a stable interface around the agent’s data access or Harbor’s ability to start multiple Docker images. Neither is presented as a finished recipe. If the agent directly queries S3, replacing those calls with mocks can bring back the maintenance burden that realistic environments were meant to avoid.
Data volume is another choice the task must justify. The team generally uses cleaned subsets of existing data rather than its entire trace collection. Multiple tasks can reuse one SQLite file, but a task that needs only a subset need not carry everything. Conversely, testing behavior against a full database would require a large fixture. Song distinguishes that possible test from the smaller-data evaluations the team has actually run.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A SQL tool still needs a tool-selection policy
The concrete optimization begins with aggregate questions. Users wanted to know their busiest day over several months, how many traces arrived that day, or which models accounted for the most spending. Phoenix’s available REST routes and GraphQL resolvers did not directly answer all of those aggregates. Agents fetched pages of records and calculated the answer themselves. More data meant more pagination and more tool calls.
The team added an MCP tool that lets an agent write and execute a read-only SQL query against the database. That provided a more direct way to answer aggregate questions, but simply adding it did not reliably change behavior. An agent now had REST, GraphQL and SQL available and still sometimes chose an inefficient route. The missing piece was guidance about when to select the new capability.
What did the prompt change tell the agent to do differently? The earlier prompt mentioned GraphQL; the revised guidance directed aggregate work, such as counts and grouping, toward the SQL tool before GraphQL. This was a small policy change on top of an existing tool. Harbor supplied a way to test whether that policy changed actual tool use rather than merely sounding sensible.
The evaluation started with a semi-realistic SQLite file collected during the team’s own product development. Song asked Claude to examine domains including traces, spans, datasets and experiments, then brainstorm both aggregate and non-aggregate user questions and write oracle solutions. Those solutions could be REST calls or database queries. He spot-checked the synthetic questions, and the verifiers included SQL checks, REST calls and LLM-as-a-judge checks. Running them against the oracles helped debug the setup before evaluating the agent.
The changed prompt led to more frequent SQL use and a lower average tool-call count. That matched the intended direction: Song preferred aggressive SQL use to agents paginating through records for an aggregate. The fixture was small enough that pagination was limited, so he suspected the modest observed reductions understated the problem at larger volumes. That larger-volume effect remained an expectation, not a measured result of this experiment.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Keep correctness separate from the optimization target
The Phoenix demo opens the versioned dataset and a concrete aggregate task: identify the three most expensive models by total cost and report each cost. How is that question paired with an expected answer? Its reference answer comes from an oracle SQL query run against the fixture. This makes the result repeatable for that query and data, but its correctness still depends on the generated query expressing the intended question—a degree of trust Song explicitly places in Claude.
The experiment views then show jobs and metrics such as cost, run latency, reward, tool count and SQL use. Correctness rewards were already high before the prompt change: the system generally found the right answers, but used too many calls to get them. The experiment was an efficiency optimization, not a repair of an answer that had been consistently wrong.
A reward is not a universal score Harbor discovers for you. The task author writes the verifier, chooses what success means and gives the reward a name. In the binary examples, the script emits 0 or 1. The job view aggregates those task outcomes. Checking whether the expected database state exists and checking whether SQL was used answer different questions, even if both appear as evaluation metrics.
That prompts a useful objection from the audience: if verifiers are supposed to ignore the agent’s implementation, why inspect whether it used SQL? The distinction is the purpose of the test.
- Correctness regression: Inspect the answer or side effects. Let any implementation pass if it accomplishes the task.
- Targeted optimization: Inspect the trajectory when the behavior of interest is tool selection or tool-call count.
Trajectory-aware checks make the optimization visible, but they are less tolerant of future implementation changes. Song accepts that tradeoff for this particular investigation.
Phoenix also supports sharing and curating data, adding traces to datasets and loading externally stored data into its database. Those facilities can help a team maintain evaluations, but Song does not present a proven large-team operating model: this work is being done by him and a handful of colleagues.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Compare jobs, then compare the same task
The next view puts two jobs side by side: before and after the single prompt change, using the same model configuration. At the aggregate level, GraphQL calls decrease, SQL calls increase, total tool count decreases and agent turns decrease. Rewards stay the same, with the displayed tasks still passing. The explanation is concrete: SQL can perform work in one query that previously required multiple retrieval and aggregation steps.
Do individual tasks follow the aggregate pattern? The task table places expected answers beside each job’s actual answer and metrics. For the three-most-expensive-models question, tool count and turn count both fall—but SQL-call count falls too. That is a useful surprise: more SQL across the dataset does not mean every improved task must make more SQL calls. The task-level comparison prevents an average trend from becoming an invented explanation of every run.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Trace the change from GraphQL retrieval to SQL
Filtering helps narrow the comparison to consequential changes. Song tries a natural-language request for runs where the second experiment has a lower tool count than the first. A local model in the browser is meant to translate that request into a query DSL, but the attempt does not work as intended. He falls back to pasting a known-correct expression and filters the table, then examines an example that changed from no SQL use to SQL use. The fallback is part of the demonstration: the structured filter remains useful even when its language interface fails.
Where did the execution actually change? The corresponding traces reveal the distinction. Before the prompt change, the agent used Bash, whose emulator called GraphQL underneath. In the later job, the analogous operation issued a SQL query directly against the database. The ATIF-to-OpenInference conversion makes those steps inspectable. This is stronger explanatory evidence than a SQL-use counter alone: it identifies the old access path and the replacement in a particular task.
The team can also ask an agent to inspect a selected experiment and task. That fits its working practice: use the product, record the agent’s traces, collect useful data and enlist the agent to help investigate its own behavior. The traces provide concrete material for that investigation rather than requiring the agent to reconstruct what happened from memory.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Test skill outcomes and skill triggering deliberately
The team’s regression suite is still early, focused on happy paths for its most important skills across supported models and providers. Skills are useful units because they describe workflows: when a condition occurs, take the appropriate action. Song compares these tests to Playwright-style end-to-end tests, except that an agent and LLM calls sit inside the execution path.
Skill selection introduces a different failure. A useful skill may stop triggering when another skill’s front matter overlaps with its description. There are two ways to test that risk:
- Check the workflow outcome: Ignore which skill fired and verify that the task succeeded. This preserves flexibility when another route is equally valid.
- Check the trigger itself: Inspect the trajectory to see whether a particular question invoked the intended skill. This directly tests routing, even if the rest of the workflow is not exercised.
A one-turn trigger test may need little realistic data: ask the question and inspect whether the intended skill fired. Its cost is coupling. Rename the skill and the assertion may break even though the underlying capability still works. Song’s position is practical rather than doctrinaire: such tests can be useful, provided the suite treats them as focused implementation-aware checks rather than confusing them with end-to-end proof.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Evaluate the hybrid system—and invest in its starting data
The closing questions sharpen what the Phoenix evaluation includes. In production, an instrumented customer application sends traces through OTLP into the database. In these Harbor tests, that application and ingestion path are absent. The sandbox starts with an already-populated SQLite file. It assumes the upstream application has produced the data; it does not emulate that application or test the entire production ingestion path.
The unit under evaluation is a hybrid system. The Phoenix team does not modify Claude Code or Codex themselves. It modifies the MCP server, skills, CLI and APIs that help those agents interact with Phoenix. Packaging the supporting application and agent-facing infrastructure together allows the team to evaluate those combinations as a unit—the system its changes can actually improve or break.
Getting that setup working still requires care. Phoenix’s single-node architecture makes containerization comparatively easy; distributed systems face more work. Harbor’s reliance on scripts in particular locations also gives coding agents fewer automatic checks: Song notes that they cannot rely on a linter to establish that the setup follows all the execution conventions. Harbor’s own skills can help assemble the project correctly, but the script-driven setup remains something that must run successfully.
The data workflow is still fairly ad hoc. Song reuses an existing SQLite file when it fits the task; otherwise he asks Claude to remove unneeded material and upload the smaller file to cloud storage. His final emphasis is that obtaining realistic data is the hard part. The sandbox can only reveal problems represented by its starting conditions. Better fixtures make it more likely that an evaluation uncovers the failures users will actually encounter.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Resources
Related talks
- SWE-rebench: Lessons from Evaluating Coding Agents on Real Software Engineering Tasks — Ibragim Badertdinov, Nebius
Extends containerized agent evaluation to fresh GitHub tasks, including regression checks, harness differences, configuration drift and a concrete example of benchmark leakage.
- Task Fidelity Scaling Laws — Kobie Crawford, Snorkel AI
Develops the task-quality problem behind Harbor’s setup: distinguishing meaningful agent failures from failures caused by ambiguous instructions, weak rubrics or faulty evaluation tasks.
- Agentic Search vs Vector Search for Coding Agents: We Ran the Eval — Braintrust
Offers another worked evaluation where correctness and execution cost tell different stories, and trace inspection helps explain why an implementation uses more work to reach its result.
Read the complete timestamped transcript
- 0:12
I'll go ahead and get started. Uh, my name is Xander Song. I'm an engineer at Arize AI, where I work on Phoenix, our open source observability, experimentation, and evaluation platform. And today I'm going to introduce you to Harbor, which is an open source framework for evaluating agents on real tasks with real data in isolated sandboxed environments. The theme of today's talk is shipping based on evidence rather than vibes,
- 0:43
and my goal is to give you a conceptual overview of how Harbor works as a framework, and to share some of the ways in which my team is using Harbor to iterate more quickly and with more confidence on our agents, harnesses, skills, prompts, and tools. This talk is going to be a discussion rather than a interactive workshop, but my hope is that you walk away with a concrete sense of how Harbor works as a framework, and also a sense of how you might use it in your day-to-day work to make your
- 1:13
life and your agents' a little bit better. A disclaimer before I begin, I am not a maintainer of Harbor. Um, I do, however, consider myself to be an avid power user at this point.
- 1:28
So, so what is Harbor at a high level? Harbor is a framework that allows you to define realistic tasks in the form of natural language instructions to an agent. These are tasks that you would like your agent to be able to accomplish. Harbor feeds those tasks to your agent running in a fresh sandboxed environment with real data relevant to the given task. And then it enables you to define what are called verifiers. These verifiers
- 1:58
are specific to each task, and they just check that your agent produced the intended side effects inside of the environment. Verifiers produce rewards. Examples of rewards are binary scores, such as zero or one, or pass-fail. So this picture is, like, really, really simple, right? But I want to just take a moment to appreciate why it's powerful. Because if my task and my data are realistic,
- 2:29
and if I've done a good job of defining verifiers that accurately describe what constitutes success or failure on the given task, what that means is that I can hold my task and my verifier constant while I change the implementation of my agent. And by simply executing this pipeline from start to finish, I can quickly and efficiently gain a signal telling me whether or not the change I made to my agent produced an
- 2:58
improvement or a regression for this particular task. Um, and not only that, I can take it a step further. I can compile not just one task, but many tasks that cover the broad surface area of the capabilities of my agent. And this lets me close the loop here so that now I'm actually able to start iterating with a degree of confidence that I might not be able to, um, if I was shipping based on vibes alone.
- 3:30
So that's really the idea of Harbor in a nutshell.
- 3:35
Let's break it down a little bit further. I'm gonna talk about, like, the architectural details of how Harbor works. So on this slide here, I have the task that I was talking about on the previous slide. Each task specifies a set of instructions in the form of a markdown file. And once again, that is just a piece of natural language that you're going to feed to your agent.
- 3:58
The task also describes an environment in which the agent will run, and that typically takes the form of a Dockerfile. And that Dockerfile typically contains data that is relevant to your task at hand, and we'll give examples of this in just a minute. But the thing to realize here is that, um, Harbor is actually agnostic with respect to what kind of data you give it. So some examples could be, like, a SQLite file. A very common example for coding agents would be, like, a Git repository at a particular commit.
- 4:30
And Harbor also lets you define a set of verifiers inside of a test directory. And as I said, verifiers are just scripts. They're just scripts that run on your environment after your agent is done running that verify that the intended side effect took place.
- 4:46
And like I said, typically we don't have just one task, but we have many collected together in what is called a dataset. And for each task in the dataset, Harbor allows you to run a configurable number of trials. These trials are just independent executions of the task to account for the fact that LLMs are stochastic, agents are stochastic. They don't behave the same every time. So each trial is going to build a fresh environment in the form of a brand
- 5:16
new Docker container seeded with that data we were just talking about. Harbor is going to drop your agent of choice into this environment. It's going to feed it the instruction markdown file, and it's gonna let it do its thing. And of course, the agent under test is going to produce side effects in the environment usually. So if it's a coding agent, it's gonna make changes to source files. Um, it might make changes to a database if you put it in there. It could do any number of things, right? And after the agent finishes,
- 5:47
the verifiers are going to run on the environment. And again, a verifier is just a script that checks the environment. Those verifiers produce rewards, which get written to a reward.txt file.
- 6:03
And that, that's the most important thing if you take away nothing else. This is like the architecture of how Harbor works. There's another thing, which is kind of an additional optional thing you can do, which is you can configure your agents to emit so-called trajectories in the form of ATIF. ATIF stands for Agent Trajectory Interchange Format. ATIF is Harbor's specification for a JSON-based tracing format, which basically lets you peer into the internal details and the
- 6:33
internal steps that your agent took to produce the side effect on the system. So it's basically there for debugging purposes and for logs and, and recording and all of that.
- 6:46
This whole pipeline executing end to end, where we take all the tasks in the dataset, run a certain number of trials for those tasks, run our verifiers to produce rewards and collect trajectories, this whole thing all taken together is what Harbor calls a job.
- 7:02
And there's one more thing I wanna point out while I'm on this slide, which is you'll notice right here, um, I said Har- I said agent/oracle. Um, so Har-Harbor also has this notion of an oracle. You can drop not only an agent into the environment, but also an oracle. And an oracle is just a solution to your task. It's just a script that solves your task. And we'll talk about oracles more in a minute, but oracles turn out to be like this really useful thing because oracles basically let you work out all
- 7:32
the, the, the kinks in your pipeline before you ever throw an agent into the mix. It lets you make sure that your data is good, that your environment is configured properly, that your verifiers work as expected, and you get to do all that before you spend any money on tokens for an agent.
- 7:49
All right, so that is like the high level. Um, before I pause for questions, I will, I will just continue on for just a second to kind of illustrate it with a concrete example. So I'm gonna break down a concrete task. Let's suppose that our agent that we care about is a coding agent, um, or a coding agent with a particular set of skills, or a coding agent configured with like a custom MCP server that we're running, and let's suppose that the task is to fix a failing migration. So if that's the case,
- 8:19
the instruction markdown file would, you know, it would, it would be some set of instructions that you would pass to the coding agent as a user. So it might say, "Hey, I'm noticing that this migration is failing. Could you please try to fix it?" Right? And in the environment, the environment would contain perhaps an actual, um, SQL file. Maybe it would contain some DDL, um, and maybe it would contain some source code containing an up migration. And we've, you know, we've seeded the data so that the up migration fails. Um,
- 8:50
in addition, we'll have our tests, and again, the tests, they could be just scripts that run the up migration, that ensures that the up migration exit with status code zero, and then it, maybe it checks like some tables. Maybe it just checks the tables to make sure that the columns are there that we expect, the indexes are there that we expect, to make sure that the, um, migration has actually had the intended side effects on the system.
- 9:15
And you'll also notice that here optionally we have the solution, right? So the solution is just gonna be the literal solution, the literal s- script that you could run, um, that would fix the, the migration. And again, why would, why would you go through the trouble of doing this? Um, the reason again is it kind of lets you quickly and confidently build a little bit of a assurance that you've got your system set up, that your verifiers are correct. And you can-- It, it kind of lets you, um, vibe a little harder, I think, with the
- 9:45
system because you can gain a certain amount of confidence in your verifiers without necessarily, like, reviewing them line by line. At least that's my experience with it.
- 10:00
And so after the, the agent runs the task in the environment, um, it's gonna edit that broken migration file. It's gonna leave a couple modifications on the file system, and then the verifiers are gonna run. And typically, if you're running, um, Harbor in Docker, which is like the default local setting, those verifiers are gonna mount in the Docker container after the agent finishes, and that's to prevent the agent from
- 10:30
cheating. So the, the agent doesn't have access to the verifiers and the solutions, um, during the actual execution.
- 10:39
Those verifiers are gonna run the sort of checks that we talked about. Maybe it literally runs the up migration. Maybe it, um, you know, checks the tables, checks the indexes. And I think the thing to really take away from this slide is, um, the verifiers are agnostic of the implementation of your agent. They actually don't know anything about the agent at all. All they care about is what the agent leaves behind. So it is possible to, like, write some verifiers that are not like that, that peek into
- 11:09
the ATIF trajectory, that kind of look at the implementation or the execution of the agent and evaluate it that way, but that's not really the canonical kind of verifier. The verifiers, you know, canonically treat the agent as a little bit of a black box, and they're grading the agent solely based on its side effects and its output in the environment rather than the exact manner in which it produced those side effects and outcome. And there's kind of a natural parallel
- 11:39
here with integration tests because you can imagine that if you have some integration tests for an interface or an API, those integration tests, um, if they're really well-written, are hopefully agnostic of the implementation that is behind that interface or API. And in the same way, if we have really good verifiers, our verifiers are kind of describing what we expect the agent to do, the side effects we expect it to produce in the environment. And they're doing so in a way that is decoupled from the implementation.
- 12:09
And that's a really nice feature to have for a set of tests because it allows you to refactor under green, for example. It allows you to hold your tasks constant while you, like, completely change the implementation of your agent, and then it allows you to, like, fire this pipeline really quickly to figure out if everything still works. That's kind of what we're talking about when we're talking about iterating quickly, um, and shipping confidently. So before I go any farther,
- 12:39
let me just pause right here for questions or comments or, um... Yeah, let me just pause before I go any farther. Yeah, yeah. You just... Yeah. Again, it's just a Docker container. Yeah. So you just pop whatever you want in there, right? It's a primitive that you're, you're proposing. Yeah, yeah, yeah. Yeah, yeah, yeah. Yeah, yeah, yeah. So it, you know, it could be, it could be any framework, right? Uh, it could be any language. It could be any piece of code, any executable. Concretely, like, you know, I'll give you a little sneak, sneak peek or sneak preview, right? Like, we've had to, like, change tools, like, um, you know,
- 13:09
adjust, adjust prompts, right? Um, I'm going through a process right now where I'm trying to consolidate our skills, for example. So we kinda have, like, uh, several different agent experiences that we try to support. Um, there's a lot of overlap, and then there's, like, certain differences, and we're trying to, like, basically get our skills all kind of, like, reusable, right? Um, and agnostic of the particular, like, direction that the agent is coming from, whether it's MCP or a CLI or via, like, our, our own, like, server-side agent. We're trying to get it
- 13:39
agnostic. And, and so those are the kinds of things where it kinda helps to be able to specify this set of tests that is, like, completely agnostic of the underlying implementation, you know? And we, we kind of... We came to this, like, maybe, um, through the pain of doing it the other way, where we were like, kind of like, had a set of tests that were, um, that were coupled, and it was becoming difficult to maintain because, you know, I would change a tool and the tests would break, right? Even though the, noth- even though
- 14:09
the true functionality of the agent was still intact, right? Things like that. Um, we'll get to more of these, right? We'll get to more of these at the end. Um, any other questions before I go on?
- 14:19
Okay. Okay. So, uh, this, this slide is gonna talk about scale. Um, so as we, as we already talked about, right, each task and each trial, uh, is independent. Runs in its own environment, its own Docker container. Um, and that makes this whole pipeline completely horizontally scalable. And that means that it's very compatible with a sandbox provider such as Daytona or Modal or E2B or any of those. Um, and it means you can kinda rip through these tests,
- 14:49
like, really fast if you want to, which is kind of fun. So in this diagram, arrayed vertically, we have a set of four tasks in a dataset, including the fixed migration task that we were just imagining. Um, we've got a couple of different coding agents that we might wanna test out, Claude Code with Opus-5, Codex with GPT-5, and then we've got, um, a couple of different trials. Let's suppose that we're doing three trials for K equals one, two, three. Again, the point of that is just to kind of handle the, handle the fact that these things are
- 15:19
stochastic and they, they're not perfect. They don't behave the same every time. Um,
- 15:26
so because all of these are independent, um, you can run them all in parallel on these sandbox providers. So we use Daytona, um, and it runs quite fast with, like, 200 concurrent workers. So even if you have a large test suite, you can rip through it quickly. Um, and then from these individual trials, you are able to compute aggregate metrics for the kinds of things that you care about. So for example, you could compute a pass@K metric that just tells you, in this case, did I
- 15:56
pass after three attempts at the, at the task? And so I can see that, oh, for, like, this particular task, the fixed migration task, both of my agents, like, pass@K, right? Pass at three. Uh, for this one, only one of my two agents pass at three. You can actually also compute those kind of aggregate metrics over the entire dataset, right? And so this kind of like gives you this very quick picture of how your change is performing, 'cause again, if you have, like, a, a good dataset that covers the set of functionality that you care about, you can get
- 16:26
these aggregate metrics pretty, pretty rapidly. Yeah, go ahead. Each... Yeah, that's a great question. So this is a really important point, right, is each of these boxes is a totally separate Docker container, identical in the beginning, but then you, you put an agent, a separate agent into each one, and you let it go do its thing, and they do different things, right? Um, so each one is totally independent. They're completely isolated, right? They're independent trials of the same task with the same agent. Um, you're basically ru-
- 16:56
Yeah. You're basically running Claude Code three times and saying, "Hey, try to fix this migration." And then you're saying, "Oh, after three tries, did Claude Code fix the migration?" Okay, so now, now I've, now we've talked about Harbor at a, at a high level. Um, but I wanna switch gears and I wanna talk about how Harbor plugs into Phoenix. So again, I'm not a maintainer of Harbor. Um, I'm just a fan. Uh, but I am a maintainer of, of Phoenix, uh, which is our, which is Arize's open source AI observability tool. And the point of this slide is just
- 17:26
to basically convey that we have analogous concepts for the primitives in Harbor. So we're able to map your datasets to our notion of a dataset. We're able to track your experiments. We're able to, um, record your trials. Uh, we're able to convert your rewards into our notion of reward, which we call an evaluation. Um, and I guess the other thing I, I wanna take away from this slide is also we, we do something
- 17:56
where we convert those trajectories that I talked about into so-called OpenInference format. Um...
- 18:09
So OpenInference is a open standard of semantic conventions, so-called semantic conventions, which is built on top of OpenTelemetry, which is an open source framework for telemetry data such as logs, metrics, and traces. And
- 18:26
these conventions basically define data structures that, uh, your AI application can emit, which then downstream consumers such as Phoenix can ingest, and we can use those to build certain kinds of UI experiences and workflows that wouldn't otherwise be possible. So full disclosure, I am a maintainer of OpenInference. Um, we recently donated this, the spec to Cloud Native, right? Um, and to the OTel working group. So it has some traction outside of Horizon, so that's
- 18:56
maybe a reason you might want to, um, use OpenInference. Um, so this diagram on the left, you can see the ATIF trajectory for that fixed migration task. You can see the individual steps that the agent might take, um, including an LLM call and including a tool call. And over on the right, uh, this is kind of what it winds up looking like in OpenInference.
- 19:21
And in the product,
- 19:24
like I said, OpenInference allows us to build out the kinds of opinionated UIs and workflows that you see here. So we can do things like we can render your trajectories with some opinionated UI, some specialized UI for LLMs and tools. We can version your tasks and your datasets so that at any given time, you know exactly what the task looked like for a particular job in Harbor. We track your experiments, and we help you
- 19:54
visualize metrics and rewards as you iterate. So in this case, the reward, you could imagine it being pass/fail, um, zero or one, and you can run a sequence of experiments, and you can kind of see over time, is my reward moving in the right direction? We also have skills, CLIs, and an MCP server so that Claude and Codex and Cursor can easily get access to this data. And at this point, I do wanna note that Harbor does have its own open source UI, um, that you can run
- 20:24
locally on your machine. It also has a hosted web service. Um, so you, you definitely don't have to use Phoenix to use Harbor. Um, but if you're interested in some of the features I just mentioned, that would be a reason to give Phoenix a try.
- 20:37
Okay, so hopefully at this point I've given you kind of a bird's-eye view of how everything works. Now I wanna talk about some of the ways in which our team has been using Harbor, and hopefully this answers, um, answers your point over here just around getting some concrete examples. Um, so in order to do that, I just wanna give kind of this high level of like what our architecture works with. So again, this is the Phoenix architecture. This is particular to our team, right? This is the... This is what our team is kind of like dealing with on a day-to-day
- 21:07
basis. Um, on the left here, you have an end user's AI application or agent. And again, this application is going to have been instrumented with OpenTelemetry and OpenInference, and it's going to be exporting and emitting traces to the Phoenix server over OTLP, which is the OpenTelemetry Protocol. So it's basically sending data to Phoenix, right? Saying, "Hey, I'm doing this, and this is like what my-- these are what the sequence of steps that I did to accomplish this task."
- 21:38
This could be like a production application. It could be an application that you're testing out. Those traces are ingested by Phoenix, and they are written to one of our SQL backends, either SQLite or Postgres. And once the data is in the system, we have a set of core services for things like traces, spans, sessions, datasets, experiments, and evaluations. And these core services are exposed via various kinds of APIs, REST or GraphQL. But our goal is
- 22:08
not only to, like, make those programmatic interfaces, but it's also to make it easy for our end users and for their coding agents and other kinds of agents to get access to the data and use the data. And so we support several different kinds of agent experiences, um, the first of which are coding agents. So for coding agents, we have a CLI that the coding agent can use, which will invoke REST, or it will invoke GraphQL. We also have an MCP server that you can
- 22:37
configure your coding agent to use, um, which is going to do the same things. And we have a third agent interface that we maintain for the team, which is a server-side agent that we call Pixie, which also has access to the MCP server, and therefore has access to REST and GraphQL. Um, it also has like a Bash emulator, so it's, it's kind of like a coding agent, but running server side. Um, so these are the three surfaces that we, that we want to
- 23:07
support and that we have to test and ensure are working. The coding agent with the CLI, the coding agent with an MCP server, and a server-side agent, which is our own harness.
- 23:21
And when it comes time to actually, like, test all of this out, architecturally, we have a pretty big advantage. We have this really nice feature, which is that the server runs on a single node. It's kinda like a Grafana-like architecture. Um, and that means it's very easy to Dockerize. So you can just pop the whole thing in a Docker container. What we do is we just take a SQLite file, which contains some realistic data that we've tested on, um, we replace the browser with a test client, um, and we just put it in
- 23:51
Docker.
- 23:54
And once it's in Docker,
- 23:58
we can start using it in Harbor, so we can feed the system the kinds of tasks that our particular users care about. So our particular users might ask a coding agent, or they might ask our Pixie server agent Find some hallucinated LLM calls from the last week of my traces and add them to a dataset. That's a realistic query that one of our users might ask. And we can go ahead and let any of these three kinds of
- 24:28
agents go wild inside of this environment, and at the end, we can run our verifiers. And the kinds of verifiers that we can run for us in this particular setup are really quite simple because we have a server running inside of here with a SQLite file. So we could either hit a REST endpoint to verify that the intended side effect has changed, or we could literally, like, query the SQLite file if we wanted to because it's just in the container,
- 24:58
um, and just verify, for example, that the particular tuple for this datasets table has been written in the way that we expect. Yeah. Yeah. No, thanks for stopping me because I think there's a lot going on on this slide, so maybe I should pause and, and kind of, like, make sure it's clear before I go on, right? Um, so I think there were a couple of questions there, and I'll, I'll try and address, address them. Um,
- 25:21
so in this slide, there are three different agents. In any, in any given trial, I would only be testing one of them, if that makes sense. So you could imagine maybe I'm only testing Claude Code with the CLI, maybe I'm only testing Claude Code with MCP, or maybe I'm only testing, you know, our Pixi server, server agent. So just may- let's imagine, like, it's just one at a time. In terms of how does Harbor feed it the prompt, Harbor, Harbor... And this is maybe a nuance that I haven't really talked about, right? Harbor itself, um,
- 25:51
does not-- Harbor is a program. It's a command line tool. Um, and it doesn't actually run in the environment per se, in the Docker environment per se. It runs on a host machine, which could be your laptop if you're running it locally, or it runs in, like, a GitHub Action runner, you know, if you're running it in CI. Um, and you can run the environment in a Docker container locally. That's, like, the default behavior. But if you're trying to do something more scalable, you would be running it on a sandbox provider in all
- 26:21
like-likelihood. Um, so you'd be running it on a Daytona. And so Harbor is this orchestrator, um, which is running on your laptop or running in GitHub Actions, which is, um, kicking off the run. It's basically saying, you know, "Hey, start, start the agent," right? Um, and the way it does that is... And I can show you what this looks like on the file system, but it basically, you know, um, expects a particular script. Harbor expects a particular
- 26:51
script to be written for the task, right? It's all driven via scripts. So you-- Like, when you're, when you're setting up Harbor, you have to kind of make sure that all these scripts are in the right place in the Docker image. Um, so there's gonna be a script to start the task. There's gonna be a script to run the verifiers. There's gonna sc- be a script to run the oracles. It's all driven by scripts. So the scripts are in the Docker container, um, but Doc-- but Harbor itself is a program that lives on a, on a host machine and just kind of orchestrates things and kicks things off. That's right. And the interfa-- Yeah. So
- 27:21
maybe I'll just repeat what you, what you just said because I think it's well put, is like, Harbor is a CLI, um, and it has certain conventions for if you, like, make your Docker container or your Docker image look this way, it will be able to run it. Um, and those conventions are almost all, like, is there a script in the right place? So that's kinda like the contract, right? If there was a, a contract that you had to honor with Harbor so that your, your image would run, it would be like, "Did I put my scripts in the right place?" Right? That's kind of the
- 27:51
contract. Got it. Yeah. Does that, does that clarify? Yes. Okay. Yeah. So again, on this one, on this slide, right, basically, I think the takeaway is, um, the takeaway on this slide is, like, you have a great deal of freedom, um, and flexibility when it comes to customizing your Harbor environment, right? Um, the, the simplest, like, the simplest example that people always get started with is the example of a coding agent operating on a Git repo, but this example is different, right? This example is, like, I
- 28:21
literally put some data from a SQLite file in there. I threw my whole application in there with it, along with its APIs, along with its own server-side agent. It's all-- It's a running server in the environment, in the Docker container. Um, something, you know... There's a CLI in the environment. So what's not even included here is, like, there's actually a set of skills literally in the environment on the file system that the agent has access to. Um, so all that is to say is, like, you have a lot of flexibility in setting up your
- 28:51
environment. Um, you know, it has to fit in a Docker image, but if you can fit it in a Docker image, um, you know, you can, you can make Harbor run it, basically.
- 29:04
All right. And now... Any other, any other questions before I move on? Maybe... 'Cause this is a complex slide, I think. This is, and y- and, you know, this is like, again, this slide is very much, like, particular to h- to our project, right? This is, like, what our project looks like. Your project may not look like this, right, if you, if you're not running, like, a single node server or something. But yeah, I mean, that's such a good question, and I haven't had to deal with that problem, so it would require a lot of thought, I think, right? But basically, the question is like, my system is not a single node,
- 29:34
it's not a single, you know... I can't just, like, throw in a SQL s- single SQLite file. I have, like, a, you know, some distributed system with, like, S3 and all this other stuff that I'm trying to maintain. How would I test Harbor out with that? The answer is, like, that's probably harder. Yeah. That's probably harder. So in that case, in that case, my initial thought is, like, you know, you have to perhaps carve out your agent a little more, right? So here we've had this advantage where, like, it's simple for us to throw the whole damn thing in there, in the Docker
- 30:04
container, and so we did. If that wasn't simple to do, if you can modularize your agent somehow into some kind of, like, interface, hide i- hide the implementation behind an interface Um, and if you can like, if there's some, like, stable API boundary or interface that is relatively stable, right? Where you know, um, I'm feeding, like, this agent data in this format, right? Whatever that format may be. Like, eventually, this agent has access to data in this format, right?
- 30:34
Um,
- 30:37
that would be one approach, right? Um, and I realize that that is maybe not a satisfactory answer depending on what your exact setup looks like, because maybe your agent, like, literally queries S3, and then, like, what are you to do, right? Are you gonna mock out all those S3 calls? And then once you start mocking out S3 calls, you're kinda in a world of pain, because now you're in the world of mocking stuff again, right? And, and that's really, like, what we're trying to get away from with Harbor. We're trying to get away from that world. Um, so I don't know that there's like... I actually don't know, 'cause I haven't had to explore this problem
- 31:07
yet. Um, I do know that, you know, I've been kind of, like, eliding this particular feature of Harbor, but Harbor does have the ability to, um, like, run multiple Docker images, right? Spin up multiple things. So, like, you could, you could wonder... I would wonder about that, is like, oh, is there a way where I could somehow encapsulate my application or something? Yeah, go ahead. The question was, like, what scale do you use, right? If you have some massive SQLite file or some, you know, really large set of traces, do you, like, actually
- 31:37
evaluate on the whole thing? We don't currently do that, and I think it would probably be okay for us to do that. Um, right now, we, you know, right now, the kind of format is like, oh, like, we have some data here. I'm gonna have Claude kinda help me clean it up a little bit for this particular set of tasks. And again, the kind of, the nice thing about Harbor is, you know, you, you can, um, use the exact same data for many different tasks, but you don't have to. Um, and so if there's a particular question that you care about, and it only
- 32:07
requires a small subset of, of the data to answer, you don't have to, like, upload all of the data to answer that particular question. Um, but yeah, I, I mean, it, it just depends, too, like, on what you're trying to test, I think, right? If part of what you were trying to test was the performance of your agent when the system is full and the database is, like, quite full, then I think it would totally make sense to, um, put in a lot of data. We, we haven't done that yet. Well, I tried to... Yeah, that's such a good question. I think the question is, like, what is, what, what do you get by using
- 32:37
Phoenix with Harbor? Why not just use Harbor maybe? Um, and the answer is, like, you totally can use just Harbor, right? I'll, I'll give you a little demo, um, in just a second, where hopefully some of, some of what we offer is, is on display, right? 'Cause I, I do think that there are some genuinely unique things around, like, dataset versioning, around the experiments, around the visualizations, around the, the agent experiences that you get, um, the ability to, like, actually inspect and drill down quickly, right? So we've been, we've been solving those kinds of
- 33:06
problems, um, for several years, and, and our UI is, like, really dialed in for that sort of thing. And I'll give you a demo in just a sec. Yeah. And then you can ask me again, see if I answered the question. All right. Uh,
- 33:21
all right. I'm gonna go to the next slide, right? So this particular example that we're gonna think about is, um, this problem that we encountered recently. Uh, the problem was, you know, in this picture that I've shown you, I talked about REST, and I talked about GraphQL. So our MCP server, our CLIs, they had access to these APIs. But we found that people were asking questions that are not easily answerable via REST or GraphQL. So they were asking aggregate kinda questions, right? They were asking, you know, "What was the busiest day
- 33:51
that I had in the last few months?" Or something, right? Um, "How many traces did I get on that day?" Or they're asking questions like, "How much am I spending on, uh, each of these models?" Right? Like, which model is actually costing me the most out of all my LLM calls, right? Where am I spending the most? Um, and so what the agents would try to do is they would, uh, you know, when possible, they would, uh, look for the, the REST route or the, uh, the GraphQL resolver. Um, but these were not aggregate resolvers. So they
- 34:21
would try to, like, basically paginate through stuff and, and do the aggregation themselves. And as you can imagine, that results in a problem, because if there's enough data, you wind up, uh, you wind up doing a lot of, a lot of paginations, right? And a lot of tool calls. Um, and so part of what we did was we actually added this, this line right here. So this line, uh, is meant to represent a tool that we added to our MCP server, um, which basically
- 34:51
allows the agent to write a read-only SQL query and execute it against the database. Um,
- 34:59
and a lot went into making that tool. I won't really go into the implementation details. But the thing that we noticed even after we made that tool was that, oh, sometimes, sometimes the agent isn't calling the right tool, because now the agent has this other problem, right? Now the agent is here, and it's like, "Well, I got REST, I got GraphQL, and I got SQL. Which one, which one should I be using?" The agent doesn't really know. And so we realized, oh yeah, well, naturally, we have to, like, somehow update a prompt. We gotta tell it what to do, right? Um, but even after you tell it what to do, how do you
- 35:29
verify that it's actually doing it, right? Um, you know, it's one thing to change the prompt, but again, we're trying to, like, ship based on some evidence, right? And not just on vibes. Um, so this is, this is the change that we, we wound up testing out. It's like a small little tweak to a, to a prompt, right? So before, it, it only mentioned, uh, GraphQL. But after it says, "Hey, if you're dealing with, like a... Some kinda aggregate, like a count or a group, um, why don't you use our SQL tool rather than reaching for GraphQL first," right?
- 36:00
Um, and of course, the most important, the most important thing that we need in order to answer this question is, first of all, some, like, realistic data. So we throw in a, you know, semi-realistic, um, SQLite file that we've been kind of just, like, collecting as we develop ourselves, as we work ourselves, and, like, dug through the product ourselves. Um, but then here's the really... the part that I found quite cool or novel about Harbor, which is like, um, I literally just asked Cl- Claude, like, "Can you look at like
- 36:30
the domains of my application, you know, traces, spans, um, data sets, experiments? Can you look at all these domains? Can you brainstorm a list of user queries, some of which would be aggregate, some of which would not be aggregate, right? And then can you write an Oracle solution to those queries?" Right? Um, the Oracle solution literally just being a REST call or perhaps a database query, and the coding agent will just do that. So we generated just this data
- 37:00
set of synthetic questions. Um, I spot-checked the questions, right, just to make sure they sort of make sense. Um, and I have a certain degree of confidence in the actual verifiers, which again, are just scripts that are, that are running in the environment. But because those verifiers, um, are passing against an Oracle, again, those verifiers are just like, you know, kind of passing against this Oracle, I have a certain amount of confidence in them, right? Some of them were LLM-as-a-judge, some of them were SQL queries or, or REST calls
- 37:30
themselves. But, you know, just the ability to kind of work out those kinks before the agent ever gets to the system, it gives you a certain level of confidence. Um, and then, you know, I was able to test out this particular change, and I was able to see like, oh, you know, as you might expect, making a change like this, it causes the system to use SQL more frequently and more aggressively, which is what I wanted it to do, right? I want it to overuse SQL rather than underuse it, 'cause I don't want it to like start trying to paginate through trying to do like aggregate queries. That would be bad.
- 38:01
And then the other interesting thing is just by changing this one prompt, I can also see the number of tool calls in each, in each, uh, trajectory, the average number of tool calls dropping. Which again, is like also, you know, the reason that we did this in the first place, is I wanted to reduce the number of tool calls. And this dataset, it wasn't even like a big data. It wasn't a big dataset, so it didn't have to do a lot of pagination. I think it-- th-these numbers, like these modest drops, they kind of understate the issue because the data was not large. And so let me, let me show
- 38:31
you now what this looks like inside of Phoenix. So the first thing that you can see here, um, are the examples in our dataset, the tasks in our dataset. And again, these are versioned, so we actually have a whole version history here every time there's a change. Um, and we can inspect what some of these look like. So again, some of these are aggregate queries, some of them are not. So here's an example of an aggregate query that I might care about. What are the three most expensive models by total cost?
- 39:01
How much does each cost? So this question is more easily answered with a SQL than, than by paginating through like a REST API. And down here, you can see the answer. And again, this answer, um, it comes from an Oracle. Um, and so that's part of why I have a certain level of faith in it, is because it's, it's coming from a SQL query run against a concrete set of data. It's not something that I feel, um, a strong need to check actually for this particular example, because
- 39:31
the answer is deterministic, and I'm, I'm sort of allow-- I'm tr-- I'm placing a certain amount of trust in Claude, I guess, to like, write a good SQL query. Um, so we got all our examples here.
- 39:44
And then we got our, um, we got our experiments and, uh, our notion of an experiment, that's, that's what I've been calling a job in Harbor. So just imagine this to be all the jobs that we've run. I've run a lot of jobs in preparation for this talk. Um, these charts show you the kinds of metrics that you might care about, whether those are cost or run latency. Uh, the reward, as you can see for this particular task, is quite high. Um, and that's because I haven't really done anything that's gonna like, hopefully break the system. It's not like something was truly, genuinely broken. The system was getting the right
- 40:14
answer in the beginning, so the reward was always high. Um, it's just that it was just taking too many tool calls, right? So this was more of an optimization rather than a correctness fix. You can see tool call count. You can see, um, whether or not we use SQL. How, how do you calculate the reward? So the reward, if we go back in the slide, the reward is just coming from the verifier, and the verifier is something that you, the user, have in your control, right?
- 40:44
So when you're defining your task, for each task, you get to write a script that says what constitutes success or failure, and that script is going to emit a zero or a one, a pass or fail. You get to name the reward, right? So the reward could have a different name other than pass, fail. Um, that's all a reward is, though. You as the developer, you as the maintainer, you think about, for this particular task, what, what does it mean to succeed and what does it mean to fail?
- 41:15
How could I figure out just by looking at the side effects in the environment, looking at the file system or database file, for example, how could I figure out whether the agent succeeded? And that's what the reward is. And so that's what we're tracking over time there, are the aggregate rewards for the entire job. That's what we're tracking in this view. Yeah, go ahead. I mean, this is such a good question, right? I kinda get... it kinda like gets to one of these big, big quick, big picture questions about like, what do you use Harbor for, actually? The question was around like, well, hey, you said that, you know, typically
- 41:45
your verifiers are agnostic with respect to the, the trajectory or the execution path of the agent. But hey, like look, some of your verifiers actually like kinda... it looks like they're peeking in to see whether the agent used SQL. Isn't that cheating? Um, the answer is yes. So it's not-- I think the times when you would do what I said in the purest form of like your verifiers are truly agnostic, and they really don't look at the trajectory, and they really just look at the file system, you know, or the, or the side effects. The situation for that is really like, um, when you care about correctness.
- 42:14
Yeah. Right? So may-- you know, one of the other things that I'm not showing here, which is a different example that we've done, is like we've worked on some regression tests, and those tests are really like- Does it get to the right answer in the end? And for those tests, I think our verifiers are quite pure. They don't really look at the trajectory. They just look at like, "Oh, did it get the right answer?" Right? For this one, for this particular problem, I was concerned with something maybe subtly different. Like, I wasn't really just trying to get the right answer. I was trying to optimize a metric, right? I was trying to optimize tool
- 42:44
calling in particular, tool calls. Harbor's one of these things where you can use it for many things. You could use it for, uh, you know, I, I talked about integration testing. You can use it for like kind of end-to-end testing, um, for assuring correctness, um, for running in CI, right? Or you could also use it because you're like, "Oh, right now I'm trying to solve a really particular problem," which is like, "Oh, I kinda noticed that it sucks on these aggregate queries. Like, how can I make it better? How could I, in a quick and dirty way, kind of convince myself that it's actually doing what I hope it's doing, right?" Um, so like at least
- 43:15
for, for us, like what I've found so far is it just depends on what you're trying to do. Um, sometimes, sometimes it, it is worth peeking inside the trajectory, I think, you know? Yeah, even though, even though it makes the test less robust to change perhaps. That, that's a really good question. My... You know, my team is so small, I don't know that we can, I can really speak from personal experience. Um, that being said, like one of the reasons that you, you might use a tool like Phoenix is because it allows, you know, it has like the ability to share data, right?
- 43:45
To curate data from different sources, to add it, you know, to, to like add traces to a dataset. Um, it has the ability to like store data as a, you know, in an external file and like upsert it to the database and then run it. It has those kinds of abilities, so like that might help you. In terms of the, the, uh, logistics of like, you know, actually like dealing with this in a larger team, I, I don't, I don't know, 'cause really it is just me and a handful of people working on this right now. All right, I'm gonna show you one last little demo, and then I'll just take questions. I think we got only a handful of minutes here left.
- 44:15
So, you know, back to, back to your question around like what are some of the things that you might get out of Phoenix. I think this is like kind of the, the, one of the bigger ones, right? Is like the ability to compare individual jobs at the task level, right? So right here I have two, two particular jobs selected. I have Opus 5 before and after making that single prompt change. This column is Opus 5 before. This column is Opus 5 after. These, these metrics at the top, they're telling me in aggregate how did these like metrics that I care about, how did
- 44:45
these rewards change after I made this tweak to the prompt, right? And I can see that like GraphQL tool calls went down, so it's calling GraphQL in aggre- in, uh, it's calling GraphQL less than it was. Um, the reward is staying the same, which means everything is still passing, right? SQL calls are going up. Tool counts are going down. The number of turns the agent is taking is going down because it's encapsulating a lot of that, a lot of those turns that would've been multi-turn steps into a single turn because
- 45:15
it's using SQL. Uh, it's using more SQL. It's using less GraphQL, right? So you get those kinda like top-level metrics, which are the ones that in my mind verify, "Okay, this is a decent change." Um, and then I'm able to see in this table, like four particular tasks, right? Here's the, here's that one we looked at, like what are the three most expensive models by, by total cost. I'm able to see what I expected the answer to be, right? I'm able to see the actual answer in each job. I'm able to see the metrics and how they changed. So for
- 45:45
this one, um, yeah, for this one the number of tools went down. The number of turns went down. Interestingly enough, the number of SQL calls actually went down too, which is kind of unexpected. And I can do things like I can, I can filter by these, right? So I can... Here's a fun little feature that Mikio built recently, um, which is I can say, "Show me the runs where the second experiment had, um,
- 46:14
where the second experiment had lower
- 46:20
tool call count," and I'll use the exact, the exact metric to help it out, "lower tool count than the first experiment." We'll see if this works. So what this is doing is it's actually running a, a local model in the browser to convert that piece of natural language into a query DSL. I think I messed that up, though.
- 46:46
We gotta, we gotta use a more powerful one. The prompt API is a little bit- I'll just, I'll just copy in what I know to be the correct, uh,
- 46:57
correct expression here.
- 47:02
So I'm able to filter down. So now I'm just looking at the, um, now I'm looking at the rows where it improved basically. Like I'm looking at the ones where it didn't use SQL before where now it's using SQL. Um, and you can see here this one used SQL. It went up by one. And if we actually like look at the, uh,
- 47:22
look at the trace, we can actually see that, right? So those ATIF trajectories that g- get converted into OpenInference traces, here you can see it. You can see it was using Bash before. Um, and when it's using Bash, under the hood it is actually using GraphQL. It's calling, has a Bash emulator which is calling GraphQL underneath the hood, right? So I'm able to see that right here. I'm able to see in the second job the same trace or the analogous trace, and I'm able to see, oh, instead of, instead of calling Bash
- 47:53
and using GraphQL here, it actually issued a SQL query directly to the database. So I'm able to like visually inspect and say, "Oh, okay. In this particular example, it did the right thing." Um, and I can even, I can even ask, you know, the agent itself. So this is kinda like what we do. Um, you know, I can literally ask the agent itself like, "Hey, look at a particular, um, set of experiments," you know, "Look at this particular task," and it will tell me exactly what happened. And so this is kinda like how we get some of our data in the first place,
- 48:23
is we're kind of like using, using the agent, um, recording its traces, uh, getting its data, and using the agent to like iterate on itself. Yeah, I'd say with respect to the regression stuff, we're still really early days. I mean, we're trying to get like happy paths going, I would say. Um, and I think like the way that, the way that we're structuring it is, is around skills because I think in my mind, when you're doing those regression tests, they are sort of analogous to like Playwright in a certain way, where it's like an end-to-end thing, and it just includes
- 48:53
like an agent in the loop. It includes like LLM calls in the loop. Um, and so I think skills are really good, like, thing to write those kinds of tests for because at least in our case, like the skills are describing workflows. They're saying, "Oh, if this happens, do this. If that happens, do this," right? It's describing a workflow that you wanna have happen. And, and so right now, we're just kinda in the process of trying to nail down happy paths on our most important skills, um, and ensure that the happy path passes across
- 49:23
like the, the broad array of like models and providers that we're trying to support. Right, right, right. Yeah, so this, this particular comment is around like the ability of a... You could make the gr- the greatest skill in the world, but like, if somebody else makes another skill that kind of like where the front matter of the skill kind of treads on your feet a little bit, it's gonna-- it may not fire anymore, right? And then it's no good if it never fires. Um, and for that sort of thing,
- 49:51
it's an interesting question, right? It's like there's multiple ways of approaching it, right? Like one thing that you could do would you, you could just say, "Well, you know, I don't care which one fired as long as I have a good test, and I know that like, oh, well, like if this skill didn't fire, it's probably not gonna pass this test," right? Um, that would be one approach. Another thing you can do, and maybe this is the appropriate thing, right, is you can always, as we said, reach into the trajectory with your verifiers. You can always look in. You can always peek. You know, you
- 50:21
can cheat a little bit, and you can say, "Oh, well, like given this question, did it call this particular skill?" And we have had tests like that. Um, I think the challenge is maintainability, right? It's always around maintainability. Um, if you change the skill name or something, the test is gonna flake. Um, but we've had tests where the thing we were trying to test was skill firing, right? And those tests are not, they're not the hardest to write, right? I mean, don't even... 'Cause if, if you're only testing one turn,
- 50:51
and all you wanna know is like, did the right skill fire, um, you don't even really need great data for that. You just ask it a question and, and see did the skill fire. Um, and like you can totally, you know, maintain a suite of tests that, that do that, right? And, and, you know, then it becomes less of a end-to-end test. It becomes more of like a,
- 51:13
almost like a, i- it's like almost like, you know, on the spectrum between, you know, fully decoupled from the implementation and coupled to the implementation. It's a little bit more coupled, um, and yet it could still be useful to you, right? Um, so there's no reason to not write a test like that. Um, there's no reason to not write a test like that, I think. So it really just depends on like, you know, getting, getting, getting a broad level of coverage, um, on the things that you care about. We are collecting traces
- 51:43
in production, so when you deploy Phoenix in production, this is the picture. The traces are coming into the system from an instrumented application or agent. We're collecting those traces, and we're writing them to the database. When we're running things in Harbor, you can see that this, this thing disappears, right? The OTLP ingest, the AI agent, which is the user's agent, those disappear, and we just seed the database with data. So when you're running an... When we evaluate in Harbor,
- 52:13
um, we don't, we don't pretend or emulate an end user's application. Um, we assume that the end user's application exists and has resulted in the data in the, in the SQLite file. Yeah, so the question is around, um,
- 52:29
Harbor is canonically used for evaluating coding agents.
- 52:32
Right.
- 52:32
That's the canonical use case. And we're doing something
- 52:38
similar on the one hand, but the unit that we're trying to evaluation, to, to evaluate here is not the coding agent in isolation because we don't work on Claude Code, right? We don't, we don't tweak Claude Code. Uh, we don't tweak Codex, right? The thing that we're trying to optimize for is this hybrid system, which is Claude Code plus our particular bespoke MCP server, or Claude Code plus our particular bespoke set of skills that we maintain, or Claude Code plus
- 53:08
our, um, Claude Code plus, uh, our CLI, or Claude Code, um,
- 53:18
interacting with our APIs. And that is a different problem, right? And the fact that that is the thing that we're trying to optimize, this hybrid entity, which includes Claude Code, it includes the coding agent, but it's more than just the coding agent. It's all of the, um, supporting infrastructure around the coding agent which we maintain, which helps Claude Code be better at interacting with our product. Um, that's the reason that we've shoved all of this stuff into an environment together is
- 53:48
because all of that stuff has to change, um, and be evaluated together, um, as a unit. I think it took a second to kind of conceptually land on and understand Harbor well enough to land on this picture. Um, and then it took a second. It, it didn't take super long, and again, like I think part of that is, part of that is due to our advantage, which I mentioned, which is that Phoenix runs on a single node. Um, and that makes it easy to Dockerize, right? And so as, as some of these other
- 54:18
folks suggested, it's like, you know, if you're working on a distributed system, you know, it's, it's tougher. Your, your life is gonna be a little tougher, right? And so we had a certain advantage that made it easier for us to set this picture up. Um, but I would say like the coding agents are, um... Harbor's tough in the sense that like there's no-- because the contract that Harbor tries to honor is all based on like scripts in certain directories. You know, I need to put my, my task s- my task
- 54:48
initiation script here. I need to put my verifier script here. Um, there's no like linters that the coding agents can rely on to make sure that that works. Um, the best that you can do, uh, is you can download Harbor's skills, um, and those will help you set up your project correctly. Yeah, so the question is, uh, I have a SQLite file here. Where, where do I put it, right? Um, cloud storage, yeah. So basically, um,
- 55:16
it's kind of a one-off process right now. It's like we have a collection of data that we generally use, and if that thing works for the task at hand, I tend to reuse that file. And if it, if I don't need the whole file, I'll ha- say, "Claude, can you clean this up? Uh, can you, can you cut out the stuff I don't need, um, and push it to cloud storage for me?" Um, that data though, uh, the data's the hard part, right? I think the data, getting the data is the hard part, um, for, for any of these tasks, and the more realistic your data, the better the system is going to run,
- 55:47
uh, the better it's gonna like uncover realistic issues for you. So it's always the hard part, getting data.