Symbolic Constraint Agents: Mining Formal Tool-Call Constraints to Prevent Agent Errors
Organizations: University of Illinois Urbana-Champaign · Microsoft Research
Abstract
Symbolic constraints (programmatic, deterministic checks for tool calls) enable deterministic verification of LLM agents' tool calls, but existing approaches primarily enforce predefined safety policies rather than tool dependencies in complex tool environments. We explore whether symbolic constraints can be mined from environments and observations beyond predefined policies, and whether they can improve end-to-end task performance. We present Symbolic Constraint Agents (SCA), a framework that mines symbolic constraints from API documentation, refines them against execution traces, and checks the agent's tool calls with an SMT solver at runtime. A violation triggers a natural-language warning or, when the valid replacement is uniquely determined, an automatic repair. On AppWorld, refinement raises error-detection F1 from 37.0 to 58.3 on GPT-5.5 traces and from 39.3 to 61.9 on Qwen3.5-27B traces. At runtime, repair raises GPT-5.4-mini's task success by 5.4 points under a 40-call budget and 9.8 points when environment errors are hidden, while removing 59--87% of failed calls across models, and transfers to -bench with gains of up to 5.6 points. These findings show that automatically acquired symbolic constraints can improve tool-calling reliability and task completion, particularly when failures are costly or difficult to diagnose.
Figures & tables
| Precision (%) | Recall (%) | F1 (%) | |
| GPT-5.5 traces | |||
| Initialize | 61.5 | 26.4 | 37.0 |
| + Refine | 90.4 | 43.0 | 58.3 |
| Qwen3.5-27B traces | |||
| Initialize | 58.8 | 29.6 | 39.3 |
| + Refine | 83.9 | 49.1 | 61.9 |
| Task success (%) | Failed calls per task | |||||
| Benchmark (Setting) | Agent model | No constraints | + Warning | + Repair | + Warning | + Repair |
| AppWorld (Standard) | Qwen3.5-9B | 18.1 2.2 | 17.6 1.2 | 20.4 2.0 | 4.6 1.1 ( ) | 4.6 0.6 ( ) |
| AppWorld (Standard) | GPT-5.4-mini | 73.5 1.5 | 71.2 2.0 | 75.1 3.7 | 6.6 5.1 ( 24) | 6.6 2.7 ( ) |
| AppWorld (Standard) | Sonnet-5 | 77.3 2.9 | 75.8 1.7 | 78.1 0.5 | 1.0 0.9 ( 5) | 1.0 0.8 ( 14) |
| AppWorld (Standard) | GPT-5.5 | 90.8 0.8 | 91.1 1.0 | 91.4 1.0 | 0.95 0.97 (+3) | 0.95 0.74 ( 22) |
| AppWorld (40-call budget) | Qwen3.5-9B | 12.5 0.0 | 11.1 0.6 | 12.8 0.1 | 1.1 0.61 ( 43) | 1.1 0.56 ( 48) |
| Per task | Standard | 40-call | Silent |
|---|---|---|---|
| Qwen3.5-9B | |||
| Tool calls | 32 29 ( 9) | 22 22 ( 2) | 46 38 ( 17) |
| LLM calls | 45 43 ( 3) | 30 30 (0) | 68 62 ( 10) |
| Prompt (k) | 954 869 ( 9) | 514 519 (+1) | 1428 1291 ( 10) |
| Compl. (k) | 23.8 21.4 ( 10) | 11.9 13.7 (+15) | 16.1 13.8 ( 14) |
| GPT-5.4-mini | |||
Appendix figures & tables8 assets
Supplementary material from the paper’s appendix.
Appendix
| Benchmark | Catalog | Rules | Arg. | Hist. | Outc. | Tools |
|---|---|---|---|---|---|---|
| AppWorld | refined | 295 | 239 | 47 | 9 | 457 |
| airline | policy, initial | 51 | 36 | 15 | 0 | 13 |
| policy, refined | 51 | 37 | 14 | 0 | 13 | |
| no policy, initial | 44 | 41 | 3 | 0 | 13 | |
| no policy, refined | 44 | 41 | 3 | 0 | 13 | |
| retail | policy, initial | 69 | 36 | 33 | 0 | 16 |
| AppWorld, argument rule applies to amazon__add_product_to_cart (+4 more) If the quantity parameter is provided, it must be an integer greater than or equal to 1. ForAll([i1, c1, v1], Implies(And(call(i1, c1, TARGET), params(c1, quantity_f, v1)), And(Value.is_v_int(v1), Value.v_int_get(v1) >= 1))) |
|---|
| AppWorld, history rule applies to gmail__create_draft (+41 more) The access_token parameter must be the access_token returned by a prior gmail login call. ForAll([i1, c1, v1], Implies(And(call(i1, c1, TARGET), params(c1, access_token_f, v1)), Exists([i2, c2], And(i2 < i1, And(call(i2, c2, appworld_gmail__login), rfield(c2, access_token_f, v1)))))) |
| AppWorld, outcome rule applies to amazon__place_order An Amazon place_order call must not be attempted while the cart has an invalid promo code applied; it must not return the invalid-promo-code failure message. ForAll([i1, c1, v1], Implies(And(call(i1, c1, TARGET), rfield(c1, message_f, v1), Value.is_v_str(v1)), Value.v_str_get(v1) != StringVal("The promo code applied to your cart is no longer valid."))) |
| retail, policy, history rule applies to calculate Before any calculate tool call, the user must already have been authenticated by a prior lookup using email or name plus zip code. ForAll([i1, c1], Implies(call(i1, c1, calculate), Exists([i2, c2], And(i2 < i1, Or(call(i2, c2, find_user_id_by_email), call(i2, c2, find_user_id_by_name_zip)))))) |
| airline, no policy, history rule applies to update_reservation_passengers Before updating passengers for a reservation, the same reservation_id must have been looked up earlier with get_reservation_details. ForAll([i1, c1, v1], Implies(And(call(i1, c1, update_reservation_passengers), params(c1, reservation_id_f, v1), Value.is_v_str(v1)), Exists([i2, c2], And(i2 < i1, call(i2, c2, get_reservation_details), params(c2, reservation_id_f, v1))))) |
| Catalog | Rules # | Faithful% | Validity% |
| AppWorld | 295 | 94 | 89 |
| airline, policy | 51 | 84 | 96 |
| airline, no policy | 44 | 91 | 100 |
| retail, policy | 63 | 81 | 95 |
| retail, no policy | 47 | 100 | 94 |
| Argument rules | 384 | 93 | 99 |
| Unfaithful (3/3 judges), correct (3/3) AppWorld, argument rule, applies to amazon__signup (+15 more) Every signup call must provide a first_name parameter that is a non-empty string. ForAll([i1, c1, v1], Implies(And(call(i1, c1, TARGET), params(c1, first_name_f, v1)), And(Value.is_v_str(v1), Length(Value.v_str_get(v1)) >= 1))) Judge (Opus): “The formula only checks first_name when it is supplied and never requires it to be present, while the rule says every signup call must provide it.” |
|---|
| Faithful (3/3), too strict (3/3) AppWorld, history rule, applies to amazon__place_order (+2 more) The address_id parameter must be an address_id returned by a prior Amazon add_address or show_addresses call. ForAll([i1, c1, v1], Implies(And(call(i1, c1, TARGET), params(c1, address_id_f, v1)), Exists([i2, c2], And(i2 < i1, Or(call(i2, c2, appworld_amazon__add_address), call(i2, c2, appworld_amazon__show_addresses)), rfield(c2, address_id_f, v1))))) Judge (Opus): “Address IDs could legitimately come from other sources, such as order details or IDs given in the task, so valid calls without a prior add_address or show_addresses would be rejected.” |
| Faithful (3/3), too strict (3/3) retail, no policy, history rule, applies to cancel_pending_order An order may not be cancelled more than once, so no earlier cancel_pending_order call may have used the same order_id. ForAll([i1, c1, v1], Implies(And(call(i1, c1, cancel_pending_order), params(c1, order_id_f, v1)), Not(Exists([i2, c2], And(i2 < i1, call(i2, c2, cancel_pending_order), params(c2, order_id_f, v1)))))) Judge (Opus): “The formula does not check whether the earlier call succeeded, so a legitimate retry after a failed attempt (e.g., one rejected for an invalid reason string) would wrongly be flagged.” |
| Faithful (3/3), correct (2/3) AppWorld, history rule, applies to gmail__create_draft (+41 more); the rule behind most repairs The access_token parameter must be the access_token returned by a prior gmail login call. Judge (Opus, correct): “The documentation says the access_token is ‘obtained from gmail app login’, so a valid call needs a token returned by a prior gmail login.” Judge (Astra, too strict): “A valid Gmail access token obtained before the recorded call history can authenticate a legitimate call without a prior login call in that history.” |
| Task success (%) | Failed calls per task | |||||
|---|---|---|---|---|---|---|
| Benchmark (Setting) | Agent model | No constraints | + Warning | + Repair | + Warning | + Repair |
| AppWorld (Standard) | Qwen3.5-27B | 49.9 | 49.4 | 51.1 | 6.9 6.6 ( 5) | 6.9 5.0 ( 29) |
| AppWorld (40-call budget) | Qwen3.5-27B | 44.6 | 45.1 | 43.4 | 1.0 0.8 ( 18) | 1.0 0.8 ( 22) |
| AppWorld (Silent errors) | Qwen3.5-27B | 48.4 | 50.1 | 52.3 | 2.3 1.3 ( 42) | 2.3 1.8 ( 23) |
| Per task | Standard | 40-call | Silent |
|---|---|---|---|
| Qwen3.5-27B | |||
| Tool calls | 76 64 ( 17) | 30 30 (0) | 34 34 (+1) |
| LLM calls | 66 52 ( 20) | 20 20 (0) | 23 22 ( 4) |
| Prompt (k) | 1670 1399 ( 16) | 440 440 (0) | 553 523 ( 5) |
| Compl. (k) | 16.4 16.3 (0) | 5.1 5.3 (+4) | 6.2 6.2 ( 1) |
| Task success (%) | |||
|---|---|---|---|
| Agent model | Init+Refine | Init only | |
| GPT-5.5 | 89.3 | 85.9 | |
| Qwen3.5-27B | 59.7 | 57.3 | |
| Catalog | Model | Median | p95 | Time- | Share of |
| (ms) | (ms) | outs | ep. time | ||
| AppWorld | GPT-5.4-mini | 351 | 5136 | 4.4% | 3.9% |
| Sonnet-5 | 133 | 1433 | 1.0% | 1.9% | |
| GPT-5.5 | 211 | 5369 | 7.2% | 3.2% | |
| Qwen3.5-9B | 136 | 2814 | 0.7% | 0.2% | |
| Airline | GPT-5.4-mini | 21 | 207 | 0.5% | 1.2% |
LLM Agent Verification
LLM agent verification checks whether agents’ actions, evidence, policies, and completion claims satisfy explicit requirements.
See all 148 papers