Skip to main content
Glama
imam-db

reimburse-buddy

by imam-db

reimburse-buddy

An office agent for finance teams, built for the Tencent Cloud Hackathon 2026 (Office Agent track). It audits business-travel expense claims and carries out office requests (send minutes, schedule a follow-up) without ever acting on its own: a deterministic rule engine decides every amount, the language model only investigates and drafts, and every write waits for a human approval that is recorded in an append-only audit log.

This repository holds two versions that share one rule engine:

  • Version B (Challenge B submission): the web app in web/ plus the LangGraph agent in agent/. Finance reviews claims, starts agent investigations, turns requests into plans and approves each action on a confirmation page. Employees submit claims and can ask about their own claims.

  • Version A: the WorkBuddy skill (skills/claim-auditor/SKILL.md) and the MCP server in src/reimburse_buddy/, described under Plug into WorkBuddy.

All data in this repository is synthetic. Company, people, hotels and emails are fictional. No real email or message is ever sent.

Architecture (version B)

Source: docs/architecture_diagram.py (regenerates the SVG; the PNG is rendered from it).

  • Web app (web/reimburse_web/, FastAPI, server-rendered HTML, Indonesian labels in labels.py): employee portal, finance triage and claim pages, request intake, plan confirmation, employee Q&A.

  • Claim agent (agent/reimburse_agent/graph.py, LangGraph, DeepSeek through any OpenAI-compatible API): runs as a subprocess of the web app (agent/investigate_worker.py), calls the MCP tools and the rule engine, and drafts the decision note.

  • Plan graph (agent/reimburse_agent/plan_graph.py, no LLM): turns a request into staged actions, asks which person is meant when a name is ambiguous, takes recipients only from the directory (EMP- IDs) and times only from the operating-rules tool. See docs/request-intake.md.

  • Approval gate and executor are separate graph nodes, because LangGraph re-runs an interrupted node from the top. The gate logs the reviewer's answer; the executor acts only when the audit log holds an approval for the same action_id and args_hash, re-runs check_claim for claim writes, and otherwise refuses.

  • Audit log (agent/reimburse_agent/audit.py, migrations in agent/migrations/): append-only, unique event_key per event, Postgres in production (see Deployment notes).

  • Prompt-injection detection (agent/reimburse_agent/injection.py): rule-based HIGH/LOW detector, withheld spans blocked at staging, an echo check, a needs_review flag and an allowlist for where action arguments may come from.

  • Employee Q&A (flag EMPLOYEE_QA): read-only, identity from the signed session. See docs/employee-qa.md.

Related MCP server: policy-mcp

How the claim agent works

Source: docs/agent-how-it-works.excalidraw (open it at excalidraw.com).

  • The LangGraph agent (agent/reimburse_agent/graph.py) reads the claim and the policy, then calls check_claim. Every amount and gap comes from that deterministic rule engine, never from the LLM.

  • For each open gap it searches the mailbox with search_for_gap: 2 searches before asking the employee and 1 after. If a document is still missing, it asks the employee with the fixed-template request_missing_evidence and checks the claim again.

  • Read-only tools run freely. Every write or message stops at the approval gate, evidence requests included. A run may ask for at most 10 approvals; after that, gated calls are declined without asking.

  • Decisions that need judgment (approve_full, approve_capped, reject) come only from the finance reviewer, through the web question form or a reply. The graph refuses decisions the model tries to set itself.

  • Once the check is clean, or every gap has been searched and asked about, the graph drafts the decision note. Finance takes the final action.

  • Version B also offers find_messages(employee_id, query, date_from, date_to) (agent only, not a WorkBuddy tool). It collects the employee's messages through the same mailbox search, ranks them with BM25 plus a small local multilingual embedding model (fused with reciprocal rank fusion), and returns the top 5. Each call counts as one search against the gap's budget. If the model cannot be loaded, ranking falls back to BM25 and the result says so.

What the evaluation shows so far (synthetic data, small n, no LLM runs in this round)

Retrieval was the bottleneck. With the whole mailbox in hand the no-LLM baseline passes all 19 hard cases (0 unsafe, 0 false approvals) and 5/5 on each large set; with BM25 ranking only it passes 17/19 (cases 11 and 12 need the paraphrase) and 2/5 and 3/5. What the LLM adds is so far untested and is measured on the reasoning track.

suite (baseline, $0)

ranked (BM25+embedding), 5 runs

BM25 only, 1 run

whole mailbox, unranked (control), 1 run

hard, 19 cases

93/95 (18 cases 5/5, case 13 3/5)

17/19 (11, 12 fail)

19/19

large set A (tuning), 5

14/25 (A-slang_reply 4/5, A-reschedule_later and A-quoted 0/5)

2/5

5/5

large set B (held out), 5

10/25 (B-slang_reply, B-reschedule_later, B-quoted 0/5)

3/5

5/5

reasoning track, 11

50/55 (R1x 0/5, one false approval per run)

10/11 (R1c)

10/11 (R1x, 1 false approval)

Run 2026-10-28 on this branch (agent/eval/results/*-20261028T0*.json). The ranked column is five runs per case, not one: case 13 passes 3 times in 5, so a single run reports 19/19 or 18/19 depending on the draw, and the same case is what makes test_ranked_baseline_on_heuristic_hard_cases[13-True] fail now and then. The BM25 and whole-mailbox columns are single runs.

Against the 25 Oct run the hard set goes from 17/19 to 19/19 in whole-mailbox mode (two scoring bugs, docs/regression-hard-eval.md), and large A, large B and the reasoning track each had one case that only failed because the scripted reviewer declined the evidence request that delivers the case's receipt. Those four cases now get a reviewer that grants a document request, and in whole-mailbox mode the two large sets are back to 5/5 and the reasoning track to 10/11. In ranked mode the receipts in those cases are not retrievable (the reply is "wkwk maap telat": no shared term with the item), so they depend on the graph's post-ask hold, which fires 4 times in 5 on set A and never on set B. That is a retrieval/hold limit, not a gate failure, and it is not fixed here.

  • The whole-mailbox control passes everything because every synthetic receipt and schedule change carries structured fields (claim_id, item_id, meeting_update) and the rule engine keeps only what matches. On these fixtures the loss comes from the top-5 cut, not from understanding. Real mail has no such tags.

  • Large sets (agent/eval/large_fixtures.py): 46-68 messages per employee over Aug-Oct 2026, hand-written paraphrases combined by a seeded RNG; set A and set B share no paraphrase. Needed message recall@5 / MRR (baseline query): set A lexical 0.2/0.2, ranked 0.2/0.12; set B lexical 0.4/0.12, ranked 0.2/0.15 (agent/eval/retrieval_metrics.py). Every large mailbox is flagged possibly_incomplete (the 36 probes reach 33-47 of 46-68 messages).

  • Reasoning track (agent/eval/reasoning_fixtures.py, labels in fixtures/reasoning/labels.json, to be reviewed by Imam): the PR #30 fix (meeting updates applied by message date) cleared R1, so the false approvals drop from two to one. The one left is R1x (moved, then cancelled in free text): the cancellation is prose only, meeting_update cannot express it, so the tag-based engine still sees a live move to 15:00-16:30 and approves the 16:00 checkout. Under BM25 R1x passes, but for the wrong reason — the cancellation message is not retrieved at all, so the agent routes it to the reviewer; that is luck, not understanding. R1c fails only under BM25 (its paraphrase is not lexically retrievable).

  • Results: agent/eval/results/{hard,large_a,large_b,reasoning}-baseline*.json and retrieval-*.json; each records a SHA-256 of the fixtures.

Prepared, not yet run: model screening and an optional judge

  • agent/eval/screen_models.py screens SumoPod models on the 30 hard, large and reasoning cases. It is a dry run by default. --live needs --max-usd (hard cap; --strict-cap stops mid-model). It runs a preflight that forces each tool once, freezes the conditions (temperature 0, no parallel tool calls, the same token cap), retries infrastructure errors and drops a model after two in a row, reads the cost from the gateway header, ranks by unsafe actions, then false approvals, correct, protocol, cost and latency, adds the $0 baseline as a reference row, and runs a gateway control (deepseek-flash direct vs. via SumoPod). The dry-run estimate is about $0.81 (worst case $1.10), using third-party prices that have not been verified.

  • agent/reimburse_agent/judge.py is an optional Jev (jev-1.13.0) judge. It is off by default; set RB_JUDGE=jev. It only escalates: a yes or unsettled answer adds a review flag. It never closes a gap, picks evidence or changes an amount. Each request sends one candidate message. Code computes dates and weekdays. Questions are Nouls, never Choices. A failed call is recorded as an error, never as "no". All wording and thresholds are in judge_questions.py. Today the judge is wired only into the contradiction scan of a calendar-only late checkout, and it sees the 160-character snippet, not the full body.

  • agent/eval/judge_set.jsonl has 62 hand-written labelled pairs (formal Indonesian, slang Indonesian, English; 27 positives; hard negatives and injection attempts). agent/eval/judge_score.py compares Jev with a flash model using structured output. It reports AUROC, Brier, miss rate at the review threshold, false escalations and repeatability, overall and per register. It is a dry run by default; the estimate for 3 repeats is about $0.017.

find_messages: setup and limits

  • Embedding model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2, loaded by fastembed 0.9.1 (pinned) from Qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q at commit faf4aa42 (pinned in retrieval.py). Install with uv sync --extra embed. The first run downloads about 240 MB into RB_EMBED_CACHE, else FASTEMBED_CACHE_PATH, else ~/.cache/fastembed.

  • RB_NO_EMBED=1 skips the model: BM25 only, and the result's ranker says so.

  • Scope: only the dedicated reimburse@ mailbox, limited to messages the employee sent or forwarded. Personal inboxes are never read.

  • Limitation: the server offers only a substring search capped at 5 results, so find_messages builds its pool with 36 one-character probes (a-z, 0-9). Any probe that returns 5 may have hidden more, so the pool is flagged possibly_incomplete; that happens for any mailbox with 5 or more messages. A contradiction scan on an incomplete pool counts as unavailable and sends the item to review. A server-side list or paged search would remove the workaround.

  • Guards: a late checkout resolved by the calendar alone gets a graph-side scan (event title and date over the trip window; a hit at similarity >= 0.45, tuned on set A, or a schedule-change word sends it to review). After an evidence request, a reply that arrived since the pre-request mailbox snapshot and that the latest check_claim did not consider holds the draft once per mailbox version; a repeat draft goes through, flagged "new evidence found but not used". The same check runs when the model ends in plain text.

Tools

Tool

Purpose

list_claims(status)

Claims waiting for review

get_claim(claim_id)

Claim, trip, items and employee

get_policy()

Travel policy: city tiers, hotel caps, per diem, receipt rules

search_reimburse_mailbox(employee_id, query)

Search the shared reimburse@ mailbox that employees forward evidence to (boarding passes, receipts, schedule changes). Personal inboxes are never read.

approve_cleared_batch(batch_id, claim_ids, reviewer_confirmed)

(Mock write) Bulk-approve the claims triage cleared, only with explicit reviewer confirmation and the exact cleared list

get_calendar(employee_id, date_from, date_to)

Mock calendar

find_approval_memo(employee_id, city, date)

Hotel-cap exception memos

check_claim(claim_id, evidence_ids, human_decisions)

Deterministic rule check

triage_claims(batch)

Split a batch into auto-cleared claims and exceptions, with a time-saved estimate

draft_decision(..., language)

Reviewer-ready decision note: id, en or both (default, Bahasa Indonesia first)

send_message(to, body, subject, claim_id)

(Mock write) Email an employee; goes to the outbox

request_approval(manager_id, claim_id, item_id, reason)

(Mock write) Ask the claimant's manager; returns a pending request

update_claim_status(claim_id, status, note)

(Mock write) on_hold, approved, partially_approved, rejected

get_outbox()

Everything the agent sent or changed this session

reset_demo()

(Mock write) Restore all in-memory state before a new take (replies, outbox, approvals, statuses)

request_missing_evidence(claim_id, item_id, gap_code)

(Version B) Ask the claim's owner for a missing document with a fixed template; refuses a repeat while the last request is unanswered

The ten tools without a mark are always registered. The five (Mock write) tools are registered only when REIMBURSE_ENABLE_WRITE_TOOLS=1, and request_missing_evidence only when REIMBURSE_ENABLE_EVIDENCE_TOOL=1. The WorkBuddy config (.workbuddy/mcp.json) sets neither, so in WorkBuddy the agent can only read and draft; version B's agent sets both (16 tools) and gates every write, evidence requests included, behind a human approval.

Write tools are mocks: state lives in memory for one server process. send_message is a closed loop: scripted replies in data/replies.json are the employee's answer and land in the reimburse@ mailbox right away, so the agent can search_reimburse_mailbox and pass the reply's ID to check_claim.

Rules in check_claim:

  • hotel nightly cap by city tier (or a signed memo), and hotel nights inside the trip

  • per diem per trip day

  • receipt required; a receipt forwarded to the reimburse mailbox counts

  • boarding pass must match flight number, date and passenger name

  • expense dates within the trip

  • duplicates keyed on receipt number (no distinct receipt numbers means duplicate_suspected, not auto-reject)

  • late check-out justified by a business meeting in the same city that ends after the standard check-out time, with the requested check-out at most 2 hours after the meeting (fee capped at 50% of the nightly cap)

  • claim-level warning when an outbound flight has no return flight

Unresolved problems come back as gap codes (receipt_missing, boarding_pass_missing, hotel_over_cap, hotel_nights_mismatch, late_checkout_unjustified, duplicate_suspected, return_flight_missing). The skill maps each code to a tool. human_decisions (approve_full / approve_capped / reject) can settle any item, in any category. ignored_evidence lists IDs that did not count and why (unknown_id, belongs_to_other_employee, memo_unsigned, no_matching_item). Wrong argument shapes return an error with a correct example, so the model can retry.

search_reimburse_mailbox is a ranked OR search (case-insensitive, QZ7520 = QZ 7520, identifiers weigh more) that returns the top 5 summaries. Scripted replies react to English and Indonesian requests (receipt, struk, kuitansi, nota, bukti, taksi). The decision note's Indonesian section is fully localised and lists ignored evidence as an audit trail.

Employee Q&A (flag)

EMPLOYEE_QA=1 adds a read-only Q&A box on the employee claim page. It answers policy questions, says what an employee's own claim is missing and shows its status, cites a source for every answer and keeps a full audit trail. It uses a synthetic rulebook. See docs/employee-qa.md.

Demo scenarios

#

Claim

What happens

Triage

batch 2026-W43

triage_claims checks 10 claims: 7 clean ones are auto-cleared, 3 exceptions are investigated. Time-saved estimate: ~105 min (15 min per auto-cleared claim, a stated synthetic assumption).

Hero

CLM-2026-0110

Four problems in one claim. (a) Searching "boarding pass" only finds another trip; the airline email says "Check-in successful: QZ 7520", so the agent retries with the flight number. (b) The only hotel memo, MEMO-2026-038, is unsigned, so check_claim ignores it (memo_unsigned) and the agent asks the reviewer. (c) The calendar says the meeting ended 11:30, but an email moved it to 13:00-15:00; together they justify the 16:30 check-out. (d) The taxi receipt is missing; the agent emails Rina and her reply carries the receipt.

1

CLM-2026-0101

Flight has no boarding pass. Agent searches the reimburse mailbox, finds MSG-5001, claim approved.

2a

CLM-2026-0102

Jakarta hotel over cap. Agent finds signed memo MEMO-2026-041, approved.

2b

CLM-2026-0103

Medan hotel over cap, no memo. Agent asks the reviewer; capped amount approved.

3

CLM-2026-0104

Late check-out fee. Calendar shows a contract signing until 15:30, so the fee is justified. A post-trip dinner is rejected and a suspected duplicate taxi goes to the reviewer.

Self-approval

batch 2026-W44

One claim, CLM-2026-0120, owned by EMP-101 (Makassar hotel over cap). EMP-101 is a manager by default and cannot open finance pages, so this scene needs EMP-101 seeded as finance (seed_users --employee EMP-101 --role finance); then, signed in as EMP-101 the approve controls are hidden and the server refuses; signed in as EMP-102 they work. A separate batch, so the 2026-W43 triage numbers above stay 10 / 7 / 3.

Run them without WorkBuddy:

uv run python -m reimburse_buddy.demo boarding-pass   # or triage, hero, hotel-memo, hotel-ask, late-checkout, all

Setup

Requires Python 3.11+ and uv.

git clone https://github.com/imam-db/reimburse-buddy.git
cd reimburse-buddy
uv sync
uv run pytest            # rule tests + a real MCP stdio round-trip
uv run --no-dev reimburse-buddy-mcp   # starts the server on stdio (WorkBuddy normally does this)
(cd agent && uv sync && uv run pytest -q)   # agent, plan graph, audit log, injection, Q&A

CI runs both suites on every pull request and on pushes to main, with Tesseract and RapidOCR installed. The Postgres-marked tests stay skipped in CI because no database is available there.

Run the version B web app

uv sync && (cd agent && uv sync)
export DEEPSEEK_API_KEY=...   # only for "Selidiki dengan agent"; never commit it
export REIMBURSE_AUTH_DATABASE_URL=...   # app_users table (migration 007); unset outside production = login off
EMPLOYEE_QA=1 uv run uvicorn --app-dir web reimburse_web.app:app --port 8000
# open http://127.0.0.1:8000/ (sign in at /login)

Without a model key everything except the agent investigation works: triage, claim pages, request intake and plan confirmation, and employee Q&A (offline router) need no LLM.

Login (email + password)

Accounts live in the Postgres table app_users (agent/migrations/007_app_users.sql): employee_id (matches data/employees.json), lowercase email, a scrypt password_hash (scrypt$n$r$p$salt$hash, stdlib hashlib.scrypt), role (employee, reviewer, finance), is_active. The app role can only SELECT and UPDATE (last_login_at). With the gate on, every route except /login, /logout and /healthz needs the signed rb_auth cookie: a browser navigation is redirected to the form, anything else gets 401. Three roles (ADR 0001): employee (Karyawan) opens /employee/* and lands on /employee; reviewer (shown as Manager) opens /manager/* and lands on /manager; finance opens / and /admin/* and lands on /admin. A wrong-role navigation is redirected to the role's landing page, any other method gets 403. POST /admin/reset is refused (403) when APP_ENV is prod/production.

Manager approvals (ADR 0003): on a finance claim page, "Minta persetujuan atasan" sends one item to the claimant's own manager (one open request per item). The manager answers once on /manager (Setujui / Tolak plus a note); another manager gets 404 and the owner of the claim is refused. The answer is logged as approval / rejection (run approval_request:<id>, actor human:<manager>) before the request changes, and an audit failure changes nothing (503). Finance sees the answer next to the item. It is input only: the rule engine, the claim status and the item decisions stay with finance.

  • Session: issued_at|employee_id|role|nonce plus an HMAC, 12 hours, httponly, samesite=lax, secure in production, checked on every request. auth.reviewer_of returns the employee_id, which is what makes a self-approval refusable.

  • One message for every failure (unknown email, wrong password, inactive account): "Email atau kata sandi salah." An unknown email still runs a hash check against a dummy hash, so timing does not leak.

  • Five failures per IP per minute; after that the form refuses even the right password. Failures are written to the audit log as access_denied (run_id=auth, actor system, payload.reason=bad_credentials), one event per IP and minute, without the email, password or IP.

  • next= only accepts a local path, so the form cannot be used as an open redirect.

  • GET /healthz answers ok without touching the audit log, the store or the LLM.

  • Outside production the gate is on only when a user DSN is set (REIMBURSE_AUTH_DATABASE_URL, else REIMBURSE_AUDIT_DATABASE_URL); otherwise it is off, so local runs and the test suite behave as before. In production a missing DSN, an unreachable database or an unset REIMBURSE_DEMO_SESSION_KEY stops startup with auth.AuthConfigError.

Create the table and the accounts (owner role, never from the app; the passwords are prompted with getpass, never passed as arguments):

psql "$OWNER_DATABASE_URL" -f agent/migrations/007_app_users.sql
uv --directory web run python -m reimburse_web.seed_users --dsn "$OWNER_DATABASE_URL" --employee EMP-100 --role reviewer

To create every account in data/employees.json in one run, use --all. The role comes from each row: a manager in Finance becomes finance, any other manager reviewer, everyone else employee. --same-password (with --all) asks for one password and uses it for every account, which is a demo convenience only.

uv --directory web run python -m reimburse_web.seed_users --dsn "$OWNER_DATABASE_URL" --all

--dry-run prints the accounts and their roles without asking for a password or connecting to the database, so it needs no --dsn:

uv --directory web run python -m reimburse_web.seed_users --all --dry-run

Who may approve

The finance user signed in is the actor on every decision. Nobody may approve their own work. The scenes below need the demo people seeded as finance (for example seed_users --employee EMP-101 --role finance and the same for EMP-100); by default EMP-100 and EMP-101 are managers and EMP-102 is finance:

  • A plan whose requester is the reviewer cannot be approved (POST /admin/plans/<run_id>/decide); the approve radio is disabled on the confirmation page.

  • Neither can a plan the reviewer submitted: the intake records submitted_by (the signed-in reviewer), so picking a colleague as the requester does not make the plan approvable by its author. The page shows both lines: peminta / requester and Diajukan oleh / submitted by.

  • A claim owned by the reviewer cannot be approved: item decisions (approve_full, approve_capped), a status change off on_hold, Accept evidence, an agent write (/resume) or an agent question answered with an approve decision (/reply).

  • POST /admin/batch/approve refuses the whole batch and names the claim, rather than dropping it silently.

Rejecting, adding a note and putting your own claim on_hold stay available: they cannot approve anything. A refusal changes no state and is written to the audit log as access_denied with actor human:<reviewer>, payload.reason = "self_approval" and payload.blocked naming the rule that fired (requester or submitted_by). The intake form defaults the requester to EMP-101, so the demo reviewer is EMP-100 (the login form's default) and the third reviewer is EMP-102 (Finance): the demo shows the refusal as EMP-100, then the approval as EMP-102.

The server checks are the guard; the hidden/disabled buttons are only there so the page does not offer an action that will be refused. The plan gate in plan_graph.py is unchanged: this is a web-layer refusal, so an external driver calling the graph directly (for example agent/plan_demo.py) is not covered.

An agent write approved from the investigation page is attributed the same way: the web layer sends the reviewer ID on the worker's stdin answer, the worker puts it on the graph's resume payload as answer["reviewer"], and graph.decide() (unchanged) logs human:<reviewer>.

Daily LLM budget

One cap for the whole deployment, not one per claim, on a ledger of its own (web/reimburse_web/spend.py; agent/migrations/005_llm_spend.sql). The day boundary is Asia/Jakarta (UTC+07:00 all year, no DST).

  • Before a worker starts, the run reserves its worst case: REIMBURSE_AGENT_MAX_TOKENS tokens all billed at the output price. If committed + worst case > LLM_DAILY_BUDGET_USD, the run is refused with "Batas biaya harian demo tercapai. Coba lagi besok." and a limit audit event (payload.kind = daily_llm_budget). Check and insert happen under one lock (a file lock for the JSONL ledger, pg_advisory_xact_lock for Postgres), so two concurrent starts cannot both take the last slot.

  • When the worker reports tokens (final, or error/token_limit), the run settles the real cost from the input hit/miss and output split (the worker accumulates graph.message_usage(), so cache-hit input is billed at the hit price). A run that ends without a token report (cancel, deadline, crash) keeps its whole reservation: the ledger fails safe instead of assuming the money was not spent.

  • The Jakarta day is taken once, when the run is reserved: a run that crosses midnight WIB settles on the day it was reserved, so its reservation is always replaced and never leaks into the next day's cap.

  • If Postgres dropped the connection (a Neon endpoint suspends when idle), the ledger reconnects and retries once; if it is still unreachable the run is refused with "Ledger biaya LLM tidak bisa dihubungi, jadi tidak ada run yang dijalankan." instead of a 500, and nothing is spawned.

  • Committed for a day = every settle, plus the reservation of each run that never settled.

  • The remaining amount is shown in the nav ("Sisa anggaran LLM hari ini: $x.xx") and the ledger is append-only: the app role has INSERT + SELECT on llm_spend, never UPDATE or DELETE.

  • In production a missing budget or price stops startup with spend.SpendConfigError (and refuses LLM runs as a second line of defence). Outside production an unconfigured ledger simply means no cap.

  • Not metered yet: the optional LLMQAModel (Q&A with a real key) and the Jev judge. Both are off by default and the demo runs the offline Q&A router and no judge, so the demo cannot spend there.

Plan state after a restart

A request that is waiting — on a "who" question, or on the reviewer's approval — has to survive a deploy. Two things hold it: the plan store (request, plan versions, decisions, mock office rows) and the plan graph's checkpointer (the open interrupt). REIMBURSE_PLAN_BACKEND chooses where they live.

  • file (default outside production): REIMBURSE_PLAN_STORE (JSON files) and langgraph's MemorySaver. A restart loses every open question and every pending approval; that is what the demo has always done.

  • postgres (default in production): agent/migrations/006_plan_state.sql plus the checkpoint tables langgraph creates. One pool serves the store, the checkpointer and the mock office, so a suspended Neon endpoint costs one reconnect: check=ConnectionPool.check_connection replaces a dead socket when it is taken from the pool, and prepare_threshold=None keeps the connection safe behind a pooler.

Both backends implement one PlanStore protocol (agent/reimburse_agent/office/store.py), and both mock offices are idempotent per action_id (INSERT ... ON CONFLICT DO NOTHING in Postgres), so a resumed run that replays an approved action finds the row it already wrote instead of sending a second email.

/decide and /answer never fail silently. When the run cannot be resumed — the intake started it but the checkpointer has no thread for it, which is what a restart on the file backend looks like — the page says "Rencana ini tidak bisa dilanjutkan setelah server dimulai ulang. Gunakan Jalankan ulang." with a Jalankan ulang button, and nothing is executed. A decision for a plan an external driver owns (agent/plan_demo.py) is still only stored, as before.

Two limits of this step: the expiry sweep still treats store().runs() as live (see plans.startup_sweep), so a run whose plan survived but whose thread did not is not expired — it shows the message above instead; and the checkpoint tables belong to langgraph, so checkpoint_setup has to run once per database (below).

Backup: the plan tables are now the only copy of a pending request. Back them up with the rest of the database (plan_requests, plan_versions, plan_decisions, mock_office_rows and the checkpoint* tables, or Neon's point-in-time restore). The audit log remains the record of what was approved and executed; the plan tables are what lets a half-finished request be finished.

Environment variables

Secrets come from the environment (or a git-ignored .env.local) and are never committed.

Variable

Default

Meaning

APP_ENV

unset

prod / production requires a Postgres audit log, a user database, a session key and the LLM budget with its prices; refuses JSONL or memory

REIMBURSE_AUTH_DATABASE_URL

audit DSN

Postgres DSN for app_users (secret; app role). Falls back to REIMBURSE_AUDIT_DATABASE_URL. Unset outside production means the login gate is off; unset in production aborts startup

REIMBURSE_DEMO_SESSION_KEY

random per process

HMAC key for the login session cookie (rb_auth); required in production, set it in any shared deploy (secret)

REIMBURSE_AUDIT_DATABASE_URL

unset

Postgres DSN for the audit log (secret; app role, Neon direct endpoint)

REIMBURSE_AUDIT_LOG

unset

JSONL audit log path outside production; unset means in memory

DEEPSEEK_API_KEY or REIMBURSE_AGENT_API_KEY

unset

Model key for the claim agent (secret)

REIMBURSE_AGENT_MODEL / REIMBURSE_AGENT_BASE_URL

deepseek-flash / https://api.deepseek.com

Any OpenAI-compatible endpoint

REIMBURSE_AGENT_MAX_TOKENS

250000

Model tokens per claim run

REIMBURSE_AGENT_RUN_DEADLINE

600

Seconds of agent work per run (paused while an approval is pending)

REIMBURSE_AGENT_APPROVAL_DEADLINE

900

Seconds a pending approval waits before it is declined

REIMBURSE_INVESTIGATE_MAX_PER_MIN

3

Agent runs started per minute per web process (one live run at a time)

LLM_DAILY_BUDGET_USD

unset

Global LLM budget per Asia/Jakarta day, in USD. Unset outside production means no cap; unset in production aborts startup

LLM_PRICE_INPUT_MISS_PER_MTOK / LLM_PRICE_INPUT_HIT_PER_MTOK / LLM_PRICE_OUTPUT_PER_MTOK

unset

Token prices in USD per million tokens, read from the provider's pricing page at deploy time (never hard-coded)

REIMBURSE_LLM_SPEND_LEDGER

<plan store>/llm_spend.jsonl

Dev/test spend ledger; production uses the Postgres table from migration 005

REIMBURSE_APPROVAL_DEADLINE_MIN

30

Minutes before an unanswered staged action of a dead run is expired

EMPLOYEE_QA

off

Employee Q&A box and POST /employee/qa (see docs/employee-qa.md)

REIMBURSE_PLAN_STORE

agent/.plans

Where plans, requests and per-run mock office state are stored (file backend)

REIMBURSE_PLAN_BACKEND

postgres in production, file otherwise

Where plan state and the graph checkpointer live: file (JSON + MemorySaver) or postgres (migration 006 + the checkpoint tables)

REIMBURSE_STATE_DATABASE_URL

REIMBURSE_AUDIT_DATABASE_URL

Postgres DSN for plan state and checkpoints (secret; app role). One database can serve audit, spend and plan state

REIMBURSE_OFFICE_CONTACTS / REIMBURSE_OFFICE_RULES

bundled synthetic files

Contact directory and operating rules for request intake

REIMBURSE_EXTRACTOR

ocr

Upload extraction: ocr, dual, rapidocr or vision (see File extraction)

REIMBURSE_WEB_UPLOADS

web/uploads

Where uploaded files are kept

REIMBURSE_BUDDY_DATA

data/

Synthetic dataset used by the rule engine and the MCP server

Plug into WorkBuddy (version A)

  1. Copy .workbuddy/mcp.json into your project's .workbuddy/ folder (or merge it into ~/.workbuddy/mcp.json) and replace /ABSOLUTE/PATH/TO/reimburse-buddy with your clone path. On Windows use a path like C:\\Users\\you\\reimburse-buddy.

  2. Restart WorkBuddy and confirm the reimburse-buddy server shows its 10 tools.

  3. Add the skill in skills/claim-auditor/SKILL.md to WorkBuddy's skills. It covers the gap-code-to-tool mapping, the fallback search strategy, how to explain ignored_evidence, batching questions to the reviewer, and post-decision actions.

  4. Try: "Triage batch 2026-W43." or "Review claim CLM-2026-0110."

To point the server at another dataset, set REIMBURSE_BUDDY_DATA=/path/to/data in the env block.

Deployment notes: audit log

Production refuses JSONL and in-memory audit logs. When APP_ENV is prod or production (case-insensitive), the audit log must be Postgres:

export APP_ENV=prod
export REIMBURSE_AUDIT_DATABASE_URL='postgresql://user:pass@db.example:5432/reimburse'
export REIMBURSE_AUTH_DATABASE_URL='...'    # optional: defaults to the audit DSN (secret)
export REIMBURSE_DEMO_SESSION_KEY='...'    # stable HMAC key (secret)
export LLM_DAILY_BUDGET_USD=2.00           # cap for the whole deployment, per Asia/Jakarta day
export LLM_PRICE_INPUT_MISS_PER_MTOK=...   # from the provider's pricing page on the day you deploy
export LLM_PRICE_INPUT_HIT_PER_MTOK=...
export LLM_PRICE_OUTPUT_PER_MTOK=...
  • REIMBURSE_AUDIT_DATABASE_URL is required and must start with postgres:// or postgresql://; the connection must open at startup.

  • If it is missing, is not a Postgres URL, or the connection fails, the process exits with audit.AuditConfigError (a subclass of audit.AuditError). There is no fallback to REIMBURSE_AUDIT_LOG (JSONL) or the in-memory log.

  • The web app checks this in its FastAPI startup hook, so it fails before serving the first request. agent/investigate_worker.py checks it before running, and audit.default_log() enforces it too.

  • Production startup also refuses a missing or unreachable user database (app_users), a missing REIMBURSE_DEMO_SESSION_KEY and a missing LLM_DAILY_BUDGET_USD or token price (auth.AuthConfigError / spend.SpendConfigError), so a deploy cannot come up with every route open or able to spend without a cap.

  • The plan state is checked last, right before the expiry sweep: with REIMBURSE_PLAN_BACKEND=postgres (the production default) a missing DSN, an unreachable database or a missing table aborts startup with store.StoreConfigError naming what to run. There is no fallback to the file store, which would lose exactly the state this backend keeps.

  • Apply migrations 001-006 before the first request: 005_llm_spend.sql adds the spend ledger the daily cap reads and writes, 006_plan_state.sql the plan state (plan_requests, plan_versions, plan_decisions, mock_office_rows; reimburse_app gets SELECT, INSERT and UPDATE, never DELETE). Apply them in numeric order: 002-004 each rewrite the audit_events.event_type constraint, so a later one applied first would silently narrow it.

  • Then create the plan-graph checkpoint tables once per database, as the owner role (langgraph owns their shape, so this is not a migration). It prints PASS/FAIL per step, masks the DSN, and grants reimburse_app SELECT, INSERT, UPDATE and DELETE on the checkpoint* tables only — the audit log and the spend ledger stay append-only:

    cd agent
    NEON_OWNER_DSN='postgresql://owner:...@ep-...neon.tech/neondb' \
      uv run python -m reimburse_agent.office.checkpoint_setup

    checkpoint_setup is safe to re-run: PostgresSaver.setup() skips the migrations it already applied.

  • Outside production (APP_ENV unset or any other value), behaviour is unchanged: JSONL via REIMBURSE_AUDIT_LOG, otherwise a fresh in-memory log.

After deploy: audit log self-test

Use Neon's direct (unpooled) endpoint for REIMBURSE_AUDIT_DATABASE_URL and connect with the application role (INSERT + SELECT only), never the owner role. psycopg connects with prepare_threshold=None, so it never creates server-side prepared statements even if a pooled/PgBouncer endpoint is used by mistake.

Install the optional extra in the agent image and run the self-test as a Cloud Run job (same environment as the web/agent service):

cd agent
uv sync --extra postgres
uv run --extra postgres python -m reimburse_agent.audit_selftest

It prints one PASS/FAIL line per check — append + list roundtrip, duplicate event_key, UPDATE/DELETE/ TRUNCATE denied on audit_events, no unmatched executions, and prepare_threshold=None — and exits non-zero if any check fails. The DSN and its password are masked in the output. There is intentionally no HTTP route for this: the web simulation has no admin authentication, so it is CLI/job only.

Project layout

web/reimburse_web/      version B web app (FastAPI): app.py, auth.py + users.py (login), intake.py, plans.py,
                        plan_state.py (plan store + checkpointer backend), qa.py, upload_flow.py,
                        agent_bridge.py (starts the agent worker), spend.py (daily LLM budget),
                        labels.py (Indonesian UI labels)
agent/
  reimburse_agent/      graph.py (claim agent), plan_graph.py, office/ (plan, directory, rules, mock tools,
                        store.py (file + Postgres plan store), checkpoint_setup.py), audit.py, injection.py,
                        qa/, retrieval.py, audit_selftest.py
  investigate_worker.py agent subprocess used by the web app
  migrations/           Postgres schemas: audit log (001-004), LLM spend ledger (005), plan state (006)
  eval/                 synthetic eval suites and results
docs/                   architecture and flow diagrams, request intake, employee Q&A, UI notes
src/reimburse_buddy/
  server.py      MCP server (FastMCP, stdio)
  rules.py       deterministic rule engine
  decision.py    decision note renderer
  data_store.py  JSON data access + mock tools
  models.py      result types, Decimal helpers
  triage.py      batch triage
  demo.py        scripted scenarios
data/            synthetic employees, policy, claims, reimburse mailbox (inbox.json), calendar, memos
skills/claim-auditor/SKILL.md
.workbuddy/mcp.json
tests/

Human in the loop

The agent proposes, the reviewer confirms. triage_claims is read-only; cleared claims change status only through approve_cleared_batch(..., reviewer_confirmed=true) with the exact cleared list. Final status changes, rejections, above-cap approvals, escalations and decision notices always wait for the reviewer. Gathering evidence happens automatically; asking an employee for a missing document also waits for the reviewer's approval.

In a real deployment the evidence source is a dedicated reimburse@ mailbox that employees forward documents to, plus connectors for calendar, HR and ERP. The mock MCP tools here stand in for those.

License

MIT

Web app details (web/)

Pages

web/ is the version B app: a two-sided FastAPI app that imports the same core (data_store, rules, decision, triage). The rule engine computes every amount. The reviewer can investigate by hand or start the claim agent ("Selidiki dengan agent"), which runs agent/investigate_worker.py as a subprocess and calls the tools over MCP; only that button needs a model key.

  • Employee portal (/employee), drop-first: enter the trip, drop all receipts and boarding passes at once (/employee/new), then check one draft card per item. Each card field shows where it came from: extracted (ocr, pdf_text or vision, with a confidence; low confidence is highlighted) or self_declared (typed or changed by the employee). Cards can be edited, merged, split or deleted, and files moved between them. One submit creates the claim and forwards each file to the reimburse@ mailbox, linked to its item (files are kept locally in web/uploads/). The claim page lists evidence per item and has an "Add file" button for gaps.

  • Finance admin (/admin): read-only triage, bulk approval only with an explicit confirmation, and a claim page with findings, gap codes, ignored evidence, mailbox search, calendar, memos, item decisions, a bilingual note preview, and a status change that needs the reviewer's tick. Each item shows provenance badges for its fields. Each uploaded file is cross-checked against its item; any failed rule marks it needs_review and keeps it out of the rule engine. Rules: unreadable_file, self_declared_only (the file does not show the doc type or amount), mismatch_with_file (item amount, date or flight number differs from the file), file_date_outside_trip, doc_type_mismatch, implausible_amount (per-category caps, override with REIMBURSE_PLAUSIBLE_MAX), boarding_pass_mismatch (passenger or flight on the pass) and duplicate_file (same file hash on another item or claim). Extraction confidence is only a display hint. Boarding pass metadata is taken from the file, never from the item. The item stays out of the engine until the reviewer clicks "Accept evidence" or "Reject evidence" (logged). Until then the item shows "pending review", its claim cannot be bulk-approved or moved past on_hold, and the employee sees "Menunggu verifikasi finance / Awaiting finance review". "Reset demo data" restores the seed data.

uv sync
uv run uvicorn --app-dir web reimburse_web.app:app --port 8000
# open http://127.0.0.1:8000/admin

File extraction

Extraction is pluggable (web/reimburse_web/extract.py). It only drafts fields; the rule engine still decides every amount. The default reads files locally with Tesseract plus regex rules; PDFs use pdftotext, falling back to rasterising with pdf2image. Photos are read with two page layouts and the better-supported parse wins. The amount is chosen from labelled lines (SUBTOTAL, PPN, SERVICE, DISKON, TOTAL, TUNAI, KEMBALI): cash, change, discount and tax lines are never the total, and a TOTAL that matches subtotal + tax + service - discount or cash - change is preferred; no consistent TOTAL means low confidence. Dates with a 2-digit year (03/10/26) are read. An unreadable file becomes an empty card for manual input. Only PDF, PNG and JPEG are accepted.

sudo apt-get install tesseract-ocr tesseract-ocr-ind poppler-utils   # Debian/Ubuntu
brew install tesseract tesseract-lang poppler                       # macOS

A second OCR engine, RapidOCR (PaddleOCR models on onnxruntime, CPU only, Apache-2.0), is optional. REIMBURSE_EXTRACTOR picks the extractor:

Value

What runs

ocr (default)

Tesseract only

dual

Tesseract and RapidOCR on every photo. If they read a field differently (doc type, date, amount, merchant, flight number, nights, passenger), or only one of them reads it, the Tesseract value is kept but shown as low confidence, and the file goes to needs_review with reason ocr_engine_disagreement

rapidocr

RapidOCR only

vision

the vision extractor below

PDFs with a text layer are read by pdftotext in every mode. If RapidOCR is not installed, dual and rapidocr fall back to Tesseract. RapidOCR rebuilds lines from its word boxes and splits a date glued to a time (03-10-202609:12).

uv sync --group rapidocr                 # adds rapidocr + onnxruntime (models ship with the package)
REIMBURSE_EXTRACTOR=dual uv run uvicorn --app-dir web reimburse_web.app:app --port 8000

RapidOCR is not in the default dev group, so uv sync and the MCP server never install it. Install it with uv sync --group rapidocr; without it the RapidOCR test skips and dual / rapidocr fall back to Tesseract.

On 10 clean synthetic images we generated (38 fields), Tesseract read 37/38 fields and RapidOCR 38/38; dual mode flagged the one Tesseract error and no other file. This shows the pipeline works end to end. It says nothing about accuracy on real receipts.

Reproduce with uv run python web/sample_uploads/measure_accuracy.py ocr rapidocr dual. Timings are mean seconds per file after a warm-up file, measured on a cloud VM with an Intel Xeon CPU (no GPU), and vary by machine:

Extractor

Tuning set (7 files, 26 fields)

Held-out set (3 files, 12 fields)

Files flagged for review

Tesseract

25/26, 1.22 s/file

12/12, 0.67 s/file

n/a

RapidOCR

26/26, 0.48 s/file

12/12, 0.41 s/file

n/a

Dual

25/26, 1.58 s/file

12/12, 1.08 s/file

1 (tuning: the blurry taxi merchant, the one Tesseract miss)

An optional vision extractor calls any OpenAI-compatible chat completions API. It is off by default and the key is read from the environment only:

export REIMBURSE_EXTRACTOR=vision VISION_API_BASE=https://api.example.com/v1 VISION_MODEL=your-model
export VISION_API_KEY=...   # never commit this

Synthetic sample uploads live in web/sample_uploads/ (make_samples.py regenerates them with Pillow, truth.json holds the expected fields). web/sample_uploads/heldout/ is a separate set of tricky layouts (TOTAL (3 item), cash larger than the total, 2-digit year, TOTAL DISKON last) not used to tune the parser. uv run python web/sample_uploads/measure_accuracy.py reports both (see the table above). Both sets are synthetic and small.

Known limits before any public deploy: shared in-memory state (claims, drafts, reviewer decisions and agent runs live in one process, so a restart loses them and a second instance sees a different demo); one shared demo password rather than accounts, so the app cannot tell two reviewers apart beyond the ID chosen at login and there is no way to revoke one person's access; local uploads (basic 5 MB / type limits only); and a self-declared field is only flagged, not verified. The daily LLM cap covers the investigation worker only: the optional Q&A model and the Jev judge are not metered (both are off by default and the demo runs neither). Dual mode detects OCR misreads, not edited receipts; PDFs, single-engine reads and near-duplicate files are not cross-checked yet (planned after the hackathon).

Known limitation: the cross-checks compare the item with what OCR read from the same file, so a convincingly forged or edited image still passes; OCR heuristics are tuned on synthetic receipts and need real, anonymised receipts (and a vision fallback) before production use.

Available Tools

14 tools
check_claimA

Run the deterministic policy rules on a claim. ALWAYS use this for any money figure.

evidence_ids: IDs you found (inbox MSG-..., calendar EVT-..., memo MEMO-...). human_decisions: {item_id: approve_full | approve_capped | reject}; works for any item category. Items with status needs_evidence list gap codes (e.g. boarding_pass_missing, hotel_over_cap, late_checkout_unjustified, duplicate_suspected). ignored_evidence explains IDs that did not count.

NameRequiredDescriptionDefault
claim_idYes
evidence_idsNo
human_decisionsNo

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations at all, the description carries the full burden and does a fair job: it discloses that items can come back with status needs_evidence, enumerates real gap codes (boarding_pass_missing, hotel_over_cap, late_checkout_unjustified, duplicate_suspected), and explains that ignored_evidence reports IDs that did not count. The remaining gap is whether calling it mutates claim state or is a pure evaluation, which matters a lot for a tool accepting human_decisions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The mandate is front-loaded in the first two sentences, then the per-parameter notes are compact and each carries non-obvious information. No filler sentences and nothing repeated from the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-param tool with no annotations and no output schema, the description supplies parameter formats and describes key result fields (gap codes, ignored_evidence, needs_evidence status), which is close to sufficient. It still leaves open whether the call persists decisions and how to interpret an item that is neither approved nor flagged.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must compensate, and it largely does: evidence_ids is given concrete ID formats per source (MSG-, EVT-, MEMO-), and human_decisions is given its allowed vocabulary (approve_full | approve_capped | reject) plus the note that it works for any item category. claim_id itself is left entirely unexplained, which is the one uncovered parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("Run the deterministic policy rules on a claim") and implicitly distinguishes itself from get_policy (which supplies the rules) and draft_decision (which drafts output). The word "deterministic" also tells the agent the result is reproducible, not a model opinion.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"ALWAYS use this for any money figure" is an explicit, unambiguous triggering condition that an agent can act on directly. It stops short of naming which sibling to avoid in a specific scenario (e.g. draft_decision vs check_claim), so it is clear context without full alternative routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

draft_decisionB

Draft the final decision note for a reviewer. Re-runs check_claim, so numbers stay consistent. language: "id" (Bahasa Indonesia), "en" or "both" (default: Indonesian, then English).

NameRequiredDescriptionDefault
noteNo
claim_idYes
languageNoboth
evidence_idsNo
human_decisionsNo

Output Schema

NameRequiredDescription
resultYes

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It usefully discloses that the tool re-runs check_claim so numbers stay consistent, and it explains the language fallback behavior. However, it never states that this is a write/draft-creation operation, whether it overwrites an existing note, whether it requires human_decisions, or what permissions are needed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences and front-loads the core action before adding behavioral detail and the language note. The language specification is useful but jammed in as a trailing inline fragment, which slightly hurts scannability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained, but this is a multi-parameter drafting operation with no annotations and zero schema description coverage. The description covers purpose, a check_claim rerun, and only one parameter, leaving the roles of claim_id, note, evidence_ids, and human_decisions, plus side effects of drafting, incomplete for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for five undocumented parameters. It only explains one: the language parameter, adding valuable meanings for 'id', 'en', and 'both' with the fallback default. It says nothing about claim_id, note, evidence_ids, or human_decisions, leaving most parameters unexplained.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Draft the final decision note for a reviewer.' That clearly distinguishes it from read-oriented siblings like get_claim or list_claims, and the mention of re-running check_claim further separates it from plain validation. It loses a point because it never explicitly says whether this persists a decision versus producing a draft artifact.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: 'for a reviewer' suggests this is invoked at decision time, and 'Re-runs check_claim' hints at a relationship with that sibling. There is no explicit when-to-use rule, no prerequisites (e.g., must the claim be triaged first), and no guidance on when to use check_claim or update_claim_status instead.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

find_approval_memoA

Find approval memos for an employee, optionally filtered by city and a date they must cover. Only memos with signed=true count as approval.

NameRequiredDescriptionDefault
cityNo
dateNo
employee_idYes

Output Schema

NameRequiredDescription
resultYes

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It usefully discloses that only memos with signed=true count as approval, but it does not state read-only behavior, permission requirements, missing-result behavior, or matching rules.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two tight sentences with no wasted wording. The core purpose and the signed=true approval criterion are front-loaded and easy to scan.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is reasonably complete for a simple lookup tool and does not need to explain return values because an output schema exists. Still, with no annotations and no parameter descriptions, the missing date format and matching semantics leave meaningful gaps for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It maps all three parameters conceptually: employee_id identifies the employee, city is an optional filter, and date means the memo must cover that date; however, it gives no date format, city matching rules, or employee ID format.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific verb and resource: find approval memos for an employee. It also adds the important signed=true criterion, but it does not explicitly distinguish this tool from approval-related siblings such as request_approval.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the first sentence: use this to retrieve approval memos for an employee, optionally narrowed by city and date. However, there is no explicit when-to-use/when-not-to-use guidance or comparison to alternative tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_calendarA

Get an employee's calendar events between two ISO dates (inclusive).

NameRequiredDescriptionDefault
date_toYes
date_fromYes
employee_idYes

Output Schema

NameRequiredDescription
resultYes

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It does add genuine behavioral detail beyond the schema — ISO date format and an explicit inclusive boundary — which prevents off-by-one range errors. However it is silent on permission requirements, timezone handling, pagination, and what an empty range returns, all of which matter for a calendar read with zero annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence with no filler, front-loading the verb and resource before the range constraint. Every clause (ISO, inclusive) carries information the agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a three-parameter read tool with an output schema already documenting return shape, the description covers all inputs and the key range semantics. Remaining gaps are permission/prerequisite context and timezone assumptions, which are minor for this complexity level.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it largely does: 'an employee's calendar events' maps to employee_id, 'between two ISO dates' maps to date_from/date_to, and 'exclusive/inclusive' clarifies the boundary semantics that the schema leaves undefined. Only the expected string format (e.g. YYYY-MM-DD) is still left implicit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb ('Get'), a specific resource ('an employee's calendar events'), and the scope ('between two ISO dates, inclusive'). An agent knows exactly what the tool returns. It does not distinguish itself from any sibling, though no sibling covers calendars, so ambiguity risk is low.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the resource — an agent needing an employee's schedule would select it — but the description gives no explicit when-to-use condition, no exclusions, and no mention of alternatives among the claim/policy/inbox siblings it might be confused with during a workflow.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_claimA

Get one claim with its trip, items and employee details.

NameRequiredDescriptionDefault
claim_idYes

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full disclosure burden. It does usefully reveal that the response is an expanded record (trip, items, employee), but says nothing about permissions, error behavior for missing/inaccessible claims, or response size.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with zero filler; the payload contents are appended efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

A simple one-parameter read with no output schema, and the description compensates by naming the returned sub-resources. Only the claim_id format and failure behavior are missing, which is minor for this tool shape.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter and schema description coverage is 0%, so the schema adds no semantics for claim_id. The description confirms the parameter identifies a claim but gives no format, source, or example beyond that.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Get one claim') and clarifies the payload includes trip, items and employee details. 'One claim' implicitly separates it from the sibling list_claims, but no sibling is named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: the single required claim_id signals a lookup by identifier. The description never says when to prefer this over list_claims or check_claim, nor what happens if the claim_id is unknown.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_outboxA

(Mock) Show sent messages, approval requests and status changes made in this session.

NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It does add real context — the '(Mock)' marker signals demo data and 'in this session' bounds the scope — but it never states the operation is read-only, how results are ordered, or what a message entry contains.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single efficient sentence with the mock and session-scope qualifiers front-loaded and no wasted wording.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a no-argument read tool with no output schema, stating what categories of items appear (messages, approvals, status changes) is close to sufficient. Return shape and ordering are the only gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Zero parameters, so the baseline is 4. Nothing in the schema needs interpretation and the description correctly implies the tool takes no arguments.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb ('Show') plus a concrete resource set: sent messages, approval requests, and status changes. The 'sent' framing implicitly separates it from search_inbox, though no sibling is named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'made in this session' clause implies when this is useful (reviewing this session's outbound activity), but there is no explicit when-to-use guidance, no mention of alternatives like search_inbox, and no stated prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_policyA

Get the company travel policy (caps, per diem, receipt rules).

NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. 'Get' implies a read operation, and it lists returned categories, but it omits explicit statements about safety, permissions, or caching. Some behavioral context is conveyed, but not enough for a higher score.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence that efficiently conveys the core details without waste. Every part earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-param getter with no output schema, the description sufficiently indicates the return content (caps, per diem, receipt rules). It could mention format (e.g., text vs. structured), but it is largely complete for its simplicity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the description does not need to explain any. The baseline for zero-param tools is 4, and no additional parameter information is required.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Get') and resource ('company travel policy') with parenthetical detail on contents, making the tool's function clear. It does not explicitly differentiate from siblings, but the distinct resource makes selection straightforward.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the tool name and resource, but the description provides no explicit when-to-use guidance or alternatives. An agent can infer this is for retrieving policy information, but no conditions or exclusions are stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_claimsB

List expense claims with the given status (default: submitted).

NameRequiredDescriptionDefault
statusNosubmitted

Output Schema

NameRequiredDescription
resultYes

TDQS

B3.1/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It implies a read-only listing operation but does not disclose pagination, ordering, permissions, or whether all claims are returned when status is omitted beyond the stated default.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single compact sentence with the purpose and default front-loaded. There is no wasted text.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained. Still, the missing allowed status values and lack of sibling routing guidance leave meaningful gaps for a list tool with a filter parameter.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single status parameter has 0% schema description coverage. The description repeats the default and calls it a status filter, but does not list allowed status values, which is essential for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'List expense claims.' The status filter and default are clear, so an agent can distinguish this from singular retrieval tools like get_claim, though it does not explicitly contrast with siblings such as triage_claims.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the name and the status filter, and the default behavior is stated. However, there is no explicit guidance on when to use this instead of get_claim, check_claim, or triage_claims.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

request_approvalC

(Mock) Ask the claimant's manager to approve one item. Returns a pending request.

NameRequiredDescriptionDefault
reasonNo
item_idYes
claim_idYes
manager_idYes

TDQS

C2.7/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it only discloses that the result is a pending request. It doesn't say whether a notification is sent to the manager, whether calling twice creates duplicate requests, whether the request can be rescinded, or what permissions are needed, all of which matter for a mutating approval tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with the mock caveat and action front-loaded, followed by the return value. Nothing is wasted, though the brevity comes at the cost of the parameter detail noted above.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

A four-parameter mutation tool with no annotations, no output schema, and 0% schema coverage demands far more than two sentences. Missing side-effect disclosure, parameter meanings, and sequencing relative to sibling approval tools leave the definition well short of what an agent needs to invoke it reliably.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain the four parameters, but it only loosely implies manager_id ('the claimant's manager') and item_id ('one item'). claim_id is never mentioned, and reason is completely unaddressed, leaving half the inputs opaque.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: asking the claimant's manager to approve one item. It clearly distinguishes this from read-only siblings like get_claim or find_approval_memo, though it doesn't explicitly contrast with the closest sibling draft_decision. The '(Mock)' marker also signals this is a demo-environment operation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to call this versus draft_decision or find_approval_memo, and no stated preconditions (must a claim exist, must a memo be found first?). The only implied context is that an approval flow exists, which an agent has to infer from the sibling names.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

reset_demoB

Restore all demo state: removes delivered replies, clears outbox, approval requests and status changes.

NameRequiredDescriptionDefault

No parameters

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It helpfully discloses what is destroyed (delivered replies, outbox, approval requests, status changes), but it does not say whether the reset is irreversible, what permissions are required, or whether any other demo state is affected.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The definition is a single efficient sentence with the action and affected state front-loaded. Every listed item earns its place by clarifying the reset scope.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter reset tool with no output schema, the description covers the main effects but omits important behavioral context such as irreversibility and required permissions. With no annotations supplied, it should do more to warn an agent before a destructive reset.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so there is no parameter semantics to explain; the baseline for zero-parameter tools is 4. The description appropriately does not waste space on nonexistent inputs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses the specific verb "Restore" with a clear scope, "all demo state," and enumerates the affected artifacts: delivered replies, outbox, approval requests, and status changes. It is easy to distinguish from sibling read/list tools, though it does not explicitly contrast itself with other mutation tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description does not state when to use this reset versus alternatives, nor does it give prerequisites or exclusions. The destructive nature is implied by "removes" and "clears," but there is no explicit guidance about when this should be invoked.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_inboxA

Search an employee's mailbox (mail to or from them). Ranked OR search over subject, body, sender and attachment names; returns the top 5 as summaries (id, subject, snippet, attachments). Pass promising IDs to check_claim; it reports IDs that match no item as no_matching_item.

NameRequiredDescriptionDefault
queryNo
employee_idYes

Output Schema

NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses ranked OR matching, the fields searched, a hard top-5 cap, and the returned summary fields. It omits permission/scope caveats and what an empty default query returns.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences, front-loaded with scope, then behavior, then next step. No filler and every clause carries information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values did not need spelling out, yet the description still gives the useful shape of results and the no_matching_item linkage. The main missing piece is the empty/optional query behavior, which matters for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must compensate. It defines what the query string matches against (subject, body, sender, attachment names) and the OR/ranking semantics, but says nothing about employee_id format or the fact that query is optional with an empty default — a real gap for a 0%-coverage schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Search an employee's mailbox') and scopes it ('mail to or from them'), which implicitly separates it from get_outbox. It does not name a sibling explicitly, so the differentiation is inferred rather than stated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear workflow: search here, then pass promising IDs to check_claim. It does not say when to prefer this over get_outbox or what to do when zero results come back, so alternatives are only partly covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_messageA

(Mock) Email an employee, e.g. to ask for a missing receipt. Goes to the outbox. Scripted replies arrive in that employee's mailbox; check with search_inbox.

NameRequiredDescriptionDefault
toYes
bodyYes
subjectNo
claim_idNo

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations at all, the description carries the full load and does disclose genuinely non-obvious behavior: it is a mock, messages route to the outbox rather than the recipient, and replies are scripted later. It stops short of stating permissions, whether resending duplicates a message, or how claim_id affects delivery, which are gaps for a write tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, zero waste, and the leading parenthetical "(Mock)" plus the routing statement are front-loaded before the follow-up hint. Every sentence contributes distinct information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a four-parameter write tool with no annotations, no output schema, and 0% schema coverage, the description adequately covers delivery semantics and the reply loop but leaves the meaning of subject and claim_id, error conditions, and return values entirely unaddressed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain all four parameters, and it does not. "Email an employee" maps only loosely to `to`, and `subject` and especially `claim_id` (which implies linking the message to a claim) are never explained anywhere.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ("Email an employee") and gives a concrete use case (asking for a missing receipt), which tells an agent exactly what the tool does. It also names where the message lands (outbox) and points to search_inbox for the follow-up, so it is distinguishable from sibling read tools without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides a clear usage context ("e.g. to ask for a missing receipt") and an explicit follow-up path: replies arrive in the employee's mailbox, so check with search_inbox. It lacks an explicit when-not clause or a statement about alternatives such as draft_decision, but the operational guidance is concrete.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

triage_claimsA

Check every submitted claim in a batch (e.g. "2026-W43"). Returns auto-cleared claims, exceptions with their gap codes, and a time-saved estimate. Investigate only the exceptions.

NameRequiredDescriptionDefault
batchNo

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It usefully describes the return shape (auto-cleared claims, exceptions with gap codes, time-saved estimate) and the follow-up workflow, but it is ambiguous about whether 'auto-cleared' means claims are mutated in the system, and it says nothing about permissions or side effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, all front-loaded with the core action first, then the return value, then the workflow instruction. No sentence is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a batch tool with no output schema and no annotations, the description covers the essentials: what it operates on, what comes back, and what the agent should do next. The main remaining gap is clarity around whether the operation mutates claim state.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 0% description coverage for the single 'batch' parameter, so the description must compensate. It does so partially by providing an example format ('2026-W43'), but it does not explain what the empty/default value means or how batches are identified beyond that one example.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('Check every submitted claim in a batch') and distinguishes itself implicitly from single-claim siblings like check_claim and get_claim by operating at batch level. It stops short of naming an alternative tool explicitly, which keeps it from a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Investigate only the exceptions' gives a clear follow-up rule after calling the tool, which implies the triage use case. However, it never states when to choose this over check_claim or list_claims, and there are no explicit exclusions or prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

update_claim_statusC

(Mock) Set claim status: submitted, on_hold, approved, partially_approved or rejected.

NameRequiredDescriptionDefault
noteNo
statusYes
claim_idYes

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. The '(Mock)' prefix is honest about the tool being a demo stub, but nothing is said about whether status transitions are validated, whether the change is reversible, what permissions are needed, or what note does — major gaps for a mutation tool with zero annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler; the enumeration of statuses is the most useful information and it is placed immediately after the verb.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no annotations, no output schema, and 0% schema description coverage, the description should explain side effects, the role of the note field, and what is returned on success or invalid input. Only the status value set is covered, leaving the definition substantially incomplete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it partially does by enumerating the five valid status values that the schema does not constrain with an enum. However, claim_id (required) and note (optional, default empty) get no explanation at all, leaving two of three parameters undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb+resource (set claim status) and enumerates the exact accepted values, so an agent knows precisely what the tool does. It doesn't explicitly differentiate from siblings like check_claim, triage_claims, or draft_decision, which prevents a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no statement of when to use this versus alternatives such as check_claim or triage_claims, and no preconditions (e.g. whether a claim must exist, whether a decision must be drafted first). The agent must infer usage entirely from the tool name.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 14 tool updatesv0.1.0
    • First observedcheck_claim
    • First observeddraft_decision
    • First observedfind_approval_memo
    • First observedget_calendar
    • First observedget_claim
    • First observedget_outbox
    • First observedget_policy
    • First observedlist_claims
    • First observedrequest_approval
    • First observedreset_demo
    • First observedsearch_inbox
    • First observedsend_message
    • First observedtriage_claims
    • First observedupdate_claim_status

TDQS

A3.5/5.0

Scored across 14 tools

Disambiguation4/5

Each tool targets a distinct resource or action: listing/reading claims, reading policy/mail/calendar/memos, running rules, triaging, drafting, and mutating status. The only mild overlap is check_claim vs triage_claims vs draft_decision (single claim, batch, and note drafting that re-runs rules), but the descriptions clearly delineate when to use each.

Naming Consistency4/5

All names are snake_case verb_noun compounds, giving a predictable pattern. Verb choice varies a bit (get_/list_/search_/find_/check_), but each verb carries a real distinction rather than being random.

Tool Count5/5

14 tools sits comfortably in the well-scoped range, and every tool maps to a step in the reimbursement review workflow (evidence gathering, rule checking, approval, decision, demo reset). No tool feels redundant or filler.

Completeness4/5

The surface covers the review lifecycle end to end: policy, evidence (inbox, calendar, memos, outbox), rule checking, triage, drafting, approvals, status updates, and reset. The main gap is claim intake/editing — there is no create_claim or item-update tool, so the agent can only review pre-existing claims.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables searching fictional policy documents and assessing expense claims against deterministic rules via MCP, returning structured assessments and citations without an approval tool.
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables natural-language-driven purchase-to-pay workflows against mock procurement and payment APIs, including invoice matching, exception handling, human approvals, and idempotent payment scheduling with a tamper-evident audit log.
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables agents to review local travel expenses against fictional policy rules, preview and build immutable XLSX/PDF/DOCX/ZIP claim packages, and track submission, approval, and reimbursement status. Runs entirely offline with SQLite storage, receipt imports, exports, and portable backups.
    6
    MIT