I build and evaluate AI agents.

I build the interfaces that let agents act, then study what happens when they do.

8+ years in cloud and data engineering. MS in Computer Science at UT Austin, expected 2027. São Paulo, Brazil.

Open to AI and research engineering roles: ntokuda@utexas.edu

An LLM’s flank in Total War: WARHAMMER III, from the recording (replayed below): gpt-6-sol’s cavalry , the halberd screen and general , and the guns behind them. Dashed: fleeing.

01 / Game environments and evaluation

Agents in real games

Language models play Total War and Slay the Spire through game interfaces and evaluation tools I built. Every decision is recorded, so I can replay a battle or a run and check what the model saw, what it ordered and what the game actually did.

The LLM’s cavalry (in blue) rides around the halberd screen and into the guns behind it, while its horse archers pull a halberd unit away. Its orders act on a view of the battle that is typically 5 s old.

Lord, gpt-6-sol’s army: health 68%Knights of the Realm 1, gpt-6-sol’s army: 43 of 45 soldiers, in meleeKnights of the Realm 2, gpt-6-sol’s army: 42 of 45 soldiers, in meleeQuesting Knights, gpt-6-sol’s army: 45 of 45 soldiers, in meleeGrail Knights, gpt-6-sol’s army: 32 of 36 soldiersMounted Yeomen Archers, gpt-6-sol’s army: 45 of 45 soldiersGeneral of the Empire, game AI: health 85%Halberdiers 1, game AI: 89 of 90 soldiers, in meleeHalberdiers 2, game AI: 83 of 90 soldiersHalberdiers 3, game AI: 90 of 90 soldiersHalberdiers 4, game AI: 90 of 90 soldiers, in meleeHandgunners 1, game AI: 68 of 68 soldiersHandgunners 2, game AI: 24 of 68 soldiers, fleeing, in meleeGreat Cannons, game AI: 24 of 33 soldiers, in meleeLordKR1KR2QKGKMYAGenHB1HB2HB3HB4HG1HG2 fleeingGC
  • Lord, Lord (gpt-6-sol’s army): health 68%
  • KR1, Knights of the Realm 1 (gpt-6-sol’s army): 43 of 45 soldiers, in melee
  • KR2, Knights of the Realm 2 (gpt-6-sol’s army): 42 of 45 soldiers, in melee
  • QK, Questing Knights (gpt-6-sol’s army): 45 of 45 soldiers, in melee
  • GK, Grail Knights (gpt-6-sol’s army): 32 of 36 soldiers
  • MYA, Mounted Yeomen Archers (gpt-6-sol’s army): 45 of 45 soldiers
  • Gen, General of the Empire (game AI): health 85%
  • HB1, Halberdiers 1 (game AI): 89 of 90 soldiers, in melee
  • HB2, Halberdiers 2 (game AI): 83 of 90 soldiers
  • HB3, Halberdiers 3 (game AI): 90 of 90 soldiers
  • HB4, Halberdiers 4 (game AI): 90 of 90 soldiers, in melee
  • HG1, Handgunners 1 (game AI): 68 of 68 soldiers
  • HG2, Handgunners 2 (game AI): 24 of 68 soldiers, fleeing, in melee
  • GC, Great Cannons (game AI): 24 of 33 soldiers, in melee

Outlines: where units stood 7.2 s ago, in the view behind the latest reply.

gpt-6-sol low reasoning, rules-only prompt

Won in 8:073:06 / 8:07

Waiting 2.7 s for the model’s next reply.

Orders applied at 3:03reply 28 of 84

The model’s stated reason

“Support the cavalry fighting around the guns while the yeomen continue to wear down the southern halberdiers.”

Orders

  • Lord — use Stand Your Ground
  • Mounted Yeomen Archers — shoot at Halberdiers 2

Acted on a view 4.5 s old. The battle kept running between the observation the model was given (2:59.0) and the game applying its reply (3:03.5).

Analyst note (an interpretation written after the battle)

The guns break first

Following its stated plan to go “around the south side of the central obstruction to approach the enemy flank”, the cavalry reached the gun line and fought there: Handgunners 2 began fleeing at 3:06, the Great Cannons at 3:20 and Handgunners 1 at 3:56.

Picked for the maneuver (not a typical result): in this scenario gpt-6-sol with low reasoning won 2 of its 9 recorded battles.

Map key
  • gpt-6-sol’s units
  • Game AI units
  • Ring: in melee
  • Faded and dashed, labeled “fleeing”: fleeing
  • Enemy out of sight: where gpt-6-sol’s army last saw it
  • Recorded movement over the last 20 s (fainter: gpt-6-sol’s route since the start)
  • Model’s view: where a unit stood in the observation behind the latest applied reply, joined to where it is now
  • Move ordered in that reply, from where the model saw the unit
  • Attack ordered in that reply, and its target
  • A move the game rejected
  • Ground the game reported as restricted at deployment (coarse 32 m cells)
  • Timeline: a tick for each reply the game applied (taller where an order was rejected), and gold bands mark the analyst notes

Labels

  • Lord Lord
  • KR1 Knights of the Realm 1
  • KR2 Knights of the Realm 2
  • QK Questing Knights
  • GK Grail Knights
  • MYA Mounted Yeomen Archers
  • Gen General of the Empire
  • HB1 Halberdiers 1
  • HB2 Halberdiers 2
  • HB3 Halberdiers 3
  • HB4 Halberdiers 4
  • HG1 Handgunners 1
  • HG2 Handgunners 2
  • GC Great Cannons

Slay the Spire

Turn-based full runs

A model plays complete Ironclad runs. When one goes wrong, I first check whether the model chose badly or the interface misled it.

Furthest run so far: Act 4, floor 54. The Spire Shield and Spire Spear ended it.

One diagnosis With the Gambling Chip relic the game asks which cards to discard, but the prompt read like any other card choice, so the model sometimes discarded cards it meant to play. The latest prompt says the selection is Gambling Chip’s, but it hasn’t had a full run yet.

From PokéAgent to RoboCasa365

  1. Late 2025

    02 / NeurIPS 2025 competition

    PokéAgent Challenge

    • Quarterfinalist · Gen 1 OU
    • Quarterfinalist · Gen 9 OU

    I competed as MetaHorns. My Gen 9 OU entry combined two existing agents: PokéChamp’s minimax search looks one turn ahead, and Metamon, trained with offline RL on human battles, proposes the moves and scores each position. I also packaged Metamon self-play to run on Google Cloud Run.

    Deciding a turn

    • Metamon offline RL agent
    • PokéChamp minimax search
    One turn of my Gen 9 OU entry: PokéChamp’s minimax search, with Metamon’s policy and critic inside it. Schematic, with illustrative scores.

    Generating training data

    Self-play on Cloud Run Jobs: each task runs two Metamon agents on its own Showdown server and writes only trajectories.
  2. Early 2026

    03 / Open source · UT Austin RPL

    Metamon

    From January to March 2026 I worked on Metamon with PhD student Jake Grigsby at UT Austin’s Robot Perception and Learning Lab. My main merged change took its team validation from about a second per team to about a millisecond.

    • Merged #65 · still on main

      Faster team validation

      Before
      A new Showdown process per team file. Each one reloaded the game data, and rebuilt Showdown first because the script ran from the repository root
      After
      One validator process for the whole run, with teams sent one line at a time over a pipe
      Result
      200 Gen 9 OU teams in 0.6 s instead of 3.4 minutes, re-timed in October 2026
      • Usage statistics filtered by Smogon rank
      • Scraper: exponential backoff and a concurrency cap, after timeouts

      Follow-up: #70, a small fix merged the next day.

    • Experiment #111 · not merged

      Grouped encoder rewrite

      In the lab’s private development repository

      • An encoder rewrite that runs under CUDA graphs ~10% faster at inference
      • A profiler that keeps a change only if it passes two gates
      1. A change
      2. Same outputs? else drop
      3. Not slower? else drop
      4. Keep

      Built so a coding agent could try many small optimizations and keep only the ones that pass. The baseline ran in a different compile mode, so the 10% isn’t the rewrite alone.

  3. Jun–Jul 2026

    04 / Kaggle competition

    Pokémon TCG agents

    A Kaggle competition close to Metamon’s problem, but with no archive of human games to learn from. I recorded games between four scripted players, cloned them, then fine-tuned with PPO. A later specialist won 82.6% against one teacher, but when I checked, every win came from the opponent running out of cards.

    1. 4 scripted players as teachers
    2. 286,035 decisions recorded from 2,000 of their games
    3. 79.6% of held-out decisions matched by the clone
    Behavior cloning helped more than the custom policy Pooled win rate, 800 seat-swapped mirror games per entry against four fixed scripted teachers (reported evaluation, July 2026)
    Reported pooled mirror win rate against four fixed scripted teachers (Lucario, Abomasnow, Dragapult ex and Iono decks), 800 seat-swapped games per entry. A perfect copy of the teachers would score about 50%.
    ModelWin rate95% intervalNote
    Earlier stock-policy RL baselineabout 13%11.1–15.8%Approximate, from the 8 July report
    Custom policy, PPO only13.9%11.6–16.4%From the 9 July report
    Behavior cloning20.0%17.4–22.9%From the 9 July report
    Behavior cloning, then tuned PPO24.0%21.1–27.1%From the 9 July report
  4. Sep 2026

    05 / Robot evaluation

    Changing goals during robot execution

    I extended RoboCasa365, a kitchen simulation benchmark from the same UT Austin lab as Metamon, so the goal or the scene can change mid-episode, and recorded when each controller received the change. In 4 goal-change episodes, GR00T N1.5 always completed the original task, but never the new goal. Astra, my code-writing LLM agent with ground-truth state and evaluator feedback, met the new goal in 1 of 5 episodes. The clips are not from the evaluation.

    GR00T N1.5 · The scene changes. The simulator puts the placed croissant back on its plate, and the policy fetches the jam, then puts the croissant back on the board.
    Astra · The goal changes. Mid-task, the instruction becomes: return the croissant, but still bring the jam.

06 / Distributed systems

Starting an experimental Rust SDK for Apache Beam

In late 2022 I started a Rust SDK for Apache Beam, based on the TypeScript SDK. It was low-level distributed-systems work: speaking Beam’s portability protos, processing bundles on the worker and running pipelines on an async DirectRunner, plus the first coders and an Impulse transform. By December the smallest possible pipeline ran end to end.

In January 2023 Google’s Dataflow team started its own Rust SDK during a hackathon. In the upstream discussion, the suggestion was to move that work over to my branch:

“IMHO, @nivaldoh’s repo is further along, and better structured, so I think it makes sense to start there.”

Robert Bradshaw, apache/beam #21089

Contributors opened pull requests against it, and in February 2023 a community fork picked it up. At Beam Summit 2023 the fork’s maintainer traced its history back to my repository.

use crate::runners::direct_runner::DirectRunner;
use crate::runners::runner::RunnerI;
use crate::transforms::impulse::Impulse;

#[tokio::test]
async fn run_direct_runner() {
    let runner = DirectRunner::new();

    runner.run(|root| root.apply(Impulse::new())).await;
}
The first end-to-end run: a one-transform pipeline executed by the async DirectRunner. primitives_test.rs

Other merged contributions

Background

I’m a software and AI engineer in São Paulo. After eight years building cloud and data systems, I now work on agents in games and robotics while studying for an MS in Computer Science at UT Austin. The agent work began in 2025, when a post by Prof. Yuke Zhu pointed me to the PokéAgent Challenge. Metamon and RoboCasa365 both come from his lab.

Work

  1. AI agents 2025–

    From PokéAgent to RoboCasa365, above

  2. Globant 2024–2026

    Multimodal video search for Paramount/CBS, LLM translation for Warner Bros., data engineering with Google teams

  3. Globo 2022–2024

    Senior data engineer on production data systems

  4. DP6 2017–2021

    Data engineer, and Megalista (a Dataflow pipeline) with a Google team

Education

  1. UT Austin 2024–2027

    MS in Computer Science (online), expected Dec 2027

  2. Mackenzie 2017–2022

    BS in Computer Science, Mackenzie Presbyterian University

Contact

Open to AI and research engineering roles.