I build production AI agents, and the evaluations that show whether they work.
What I've built
A production AI agent in Microsoft Teams that picks one action out of eighty-seven per request, and the scored test suite that tells me when it picks wrong.
A multi-agent engine for long jobs, with a validator that checks the graph before it runs.
138 probes testing what defeats a frontier coding agent. The task it produced fails Opus 5 and GPT-5.6 three times out of three.
Built solo, start to finish
An assistant that lives in Microsoft Teams. You ask it for something in plain language and it works out which actions to take across your mail, calendar and files, checks what comes back, and keeps going until the job is done.
I did the product, the engineering and the compliance. Each customer's data is kept apart by the database itself, not by application code. The agent passed Microsoft's Marketplace certification and its Teams Store validation, seven review rounds.
It has to choose the right action out of eighty-seven. Sending every tool on every message is expensive, so a retrieval step narrows the list first. The risk is that the short list drops the tool a request needs, so I measure that on test requests written blind. The redesigned selector offers every needed tool 98.1% of the time, up from 87.0%.
How a test case is scoredexpected facts, not similarity
{
id: 'quality-15',
category: 'quality',
prompt: "Draft an email to Tom Nakamura saying I'll review
his investor deck by Thursday.",
expectedFacts: ['tom nakamura', 'thursday', 'investor deck'],
forbiddenFacts:['email sent', 'sent successfully', 'i have sent'],
formatReqs: { forbidFiller: true },
difficulty: 'simple',
}
// passed = accuracy >= 0.8 AND quality >= 0.8 AND no forbiddenFacts present
// accuracy = (expectedFacts matched) / (expectedFacts total)
Each case lists the facts a right answer must contain and the claims it must never make. This one asks for a draft. An agent that sends the email and says “email sent” fails, however well it's written. The suite replays 142 cases offline and checks which tools were called. I run it on demand; it isn't in CI.
What happens when it breaksthe loop and its exits
Graph returns 429/5xx → back off and retry ×3, honouring Retry-After Model rate-limits → wait the provider's interval, bounded to 4 and to 90s Request times out → keep the system prompt + last 15 messages, retry once Content filter trips → ask the user to rephrase rather than fail Same tool, same args, 8× → block that call, tell the model to try another way 35 tool calls in a turn → hard cap, abort before the batch fires // a small model by default; a long request, or one step emitting >3 tool calls, // escalates to the large one for the rest of the turn (87% of eval messages never did)
The repeat guard doesn't end the turn. It blocks the looping call and tells the model to try something else. Rate-limit waits have a hard limit because an early version retried forever. On a 142-case eval, the narrowed list called the right tools on 134 of 142 cases on average over three runs, with 49% fewer prompt tokens than sending every tool.
Where the tenant boundary isclick to enlarge
Everything a user triggers runs inside the boundary, where the database returns only that customer's rows. About twenty system paths, such as cron, retention and metering, need to see every customer, so they run outside it as a separate database role, each listed with its reason. The diagram shows them outside on purpose.
It's one agent in a loop, not a planner and executor, and it works synchronously while streaming progress. It asks before a bulk delete or before sending something it drafted, and that rule lives in the prompt, not in a separate approval system. It's published on the Marketplace and the Teams Store. I've paused the hosted service for now, and it has no paying customers.
TypeScript · Azure OpenAI · Microsoft Graph · Azure Postgres (row-level security) · Azure Container Apps · Bicep · Docker to GHCR via GitHub Actions
Built from scratch
A Python engine that turns a plain-English request into a graph of small agents, each with one job. Every workflow is checked before it runs, and any step that acts outside the engine has to sit behind a person's approval.
The agents are one shared library: every workflow uses the same registered agent by name. A learning loop works out a team's unwritten rules from their corrections, with no fine-tuning. It works with MCP both ways: it can call outside MCP tools, and it offers each of its agents as an MCP tool, plus one that builds and runs a workflow from a plain-English request.
Routing a clinic's patient messages to the right queue, with the clinic's rules never written down: accuracy rose from 59% to 97% on 150 test messages as the engine learned from 291 corrections. Its triage step sent all 160 medical messages out of 320 to a person, with no false alarms. On 120 requests, it refused to fake 41 of the 48 jobs that needed a capability it doesn't have.
The validator, before anything runs26% → 93% of seeded faults
[error] edge touches unknown step 'summarise'
[error] step 'rank' is unreachable from any entry
[error] loop edge review->extract has no `when` — it can only stop at the guard
[error] step 'verify' (verify) is missing contract inputs ['threshold']
[warning] step 'classify' has a single guarded out-edge (when 'score > 0.8')
and no other path — the work-item dead-ends whenever that guard is
false; add an else/fail path or make it a deliberate endpoint
The four errors stop the graph before any agent runs. The warning matters most. A step whose only exit has a condition drops every item that fails it, and the run just returns fewer results. That can be on purpose, so the graph still runs, but you're told before it starts. Against 615 faults seeded from a list written by an agent that never saw the code, the validator caught 26%. I added data-flow checks, and it now catches 93%, with no false alarms on the real workflows. The new checks were written after I'd seen that list, so 93% is coverage of known faults, not of unseen ones.
A real workflow, drawn by the engineclick to enlarge
The engine drew this from the workflow's own definition. The check step has two exits, pass and fail, so nothing gets dropped.
The test sets are synthetic. Each was written by one model and checked by a second, both blind to the code, and scored once. But the writers, the checkers and the engine are all Claude models, so they could share a blind spot.
Python · agent orchestration · MCP client and server · graph validation
Research · 138 probes
I set out to build one task a frontier coding agent couldn't solve, and kept track of why almost every attempt failed.
Complexity doesn't stop it.
The agent solved thirty-one of thirty-three probes. In the solve logs I read, it took in the whole system and fixed every planted bug at once, so adding more bugs, or tangling them together, changed nothing. Three things did work.
Both probes that survived had misleading evidence. So I built the final task around it and ran it on the benchmark's own harness, three trials per model.
The finished task, scoredreward 0 = the agent failed
agent model effort trials reward
claude-code anthropic/claude-opus-5 max 3 0.000
codex openai/gpt-5.6-sol xhigh 3 0.000
oracle (reference solution) — 1 1.000
nop (do nothing) — 1 0.000
// no crash, no rate limit, no timeout, read from the harness's own result.json
// 22 of 22 repository static checks pass; verifier runs in a separate container
The reference solution scores 1 and doing nothing scores 0, so a zero means the agent failed, not the grader. All six trials got the numbers right and then blamed the wrong causes, which is the mistake the task was built to provoke.
My own measurements went wrong twice, and both are in the record. A process that died in three seconds produced seventy-one “results” that were really error messages. A verifier that was too strict failed a correct answer and made a probe look like a survivor. The rule I kept: a probe only counts as a failure after its own reference solution passes its own grader.
| Probe | Runs | Verdict | Why |
|---|---|---|---|
| p22-reconciliation | 2 | SURVIVED | misleading evidence |
| p18-webhook-ordering | 4 | SURVIVED | replica lag, unknown |
| p16-rls-isolation | 1 | solved | full source, static |
| p10-partition-order | 1 | false survivor | my verifier was wrong |
| p01-cancellation | 0 | voided | 70-byte log |
Python · Docker · reproducible environments · oracle-verified grading
2023 – 2026
Three years as a full-time parent while retraining into software. I started with data engineering and machine learning, then moved to building agents as the field did. That period left seventeen public repositories, five of them a trading research system. They were for learning, and they're no longer growing.
forecasting · anomaly detection · NLP · cloud ETL · ML serving · a Databricks medallion lakehouse · streaming analytics · a trading backtest system
I'm looking for AI engineering work, remote or in Portland.
projects.ichikawa@gmail.com
linkedin.com/in/mike-ichikawa
github.com/mtichikawa