AI Agents & Tool Use
Level: 5 (Mastery — cross-domain: math + systems + coding) Time limit: 90 minutes Total marks: 60
Instructions: Answer all three questions. Show derivations, justify design choices, and provide runnable pseudocode/code where asked. Partial credit is awarded for correct reasoning.
Question 1 — ReAct Loops, Cost Bounds & Convergence (20 marks)
A ReAct agent solves tasks by alternating Thought → Action → Observation steps until it emits a final answer. Each step consumes input tokens (the full running transcript) and output tokens (fixed). The transcript grows because every step is appended: after step the transcript has length tokens, where is the average tokens added per step and is the system+task prompt.
(a) Assuming the model re-reads the entire transcript each step, derive a closed-form expression for the total input token cost over a run of steps. State its asymptotic complexity in . (5)
(b) A summarizing memory system compresses the transcript to a fixed cap of tokens whenever (cost of summarization ignored). Derive and compare its asymptotic complexity to part (a). At what step does capping first activate? (5)
(c) Model the ReAct loop as a Markov process: at each step the agent reaches the goal with probability (independent), otherwise it continues. Derive the expected number of steps to termination and the probability the run exceeds a budget of steps. Using this, give the expected total input cost under the uncapped model of part (a). (6)
(d) Explain, with reference to your results, why unbounded ReAct loops are dangerous in production and name two guardrail mechanisms (one on token budget, one on action safety) that address distinct failure modes. (4)
Question 2 — Function Calling as Constrained Generation: Build & Prove (22 marks)
You must design a guardrail that forces an LLM to emit only valid tool calls conforming to a JSON schema, by constraining decoding to a formal grammar.
(a) Given the tool signature search(query: string, limit: int in [1,50]), write a context-free grammar (in BNF) that generates exactly the set of valid JSON call objects {"name":"search","arguments":{"query":<str>,"limit":<int 1..50>}}. Enforce the integer range at the grammar level as far as CFGs allow, and state precisely which constraint cannot be expressed by a pure CFG and must be enforced by a validator. (6)
(b) Constrained decoding masks the next-token logits to the set of tokens permitted by the grammar state. Let the vocabulary be and the allowed set at a step be . Prove that renormalizing the softmax over is equivalent to conditioning the model distribution on the event "next token ", i.e. show Then show this masking can only decrease or preserve the probability mass assigned to any valid full sequence relative to the unconstrained model, and state the condition for equality. (8)
(c) Implement a Python function mask_logits(logits, allowed_ids) that applies the mask numerically stably (using for disallowed logits) and returns a valid probability distribution. Then argue why setting disallowed logits to before softmax is numerically preferable to zeroing probabilities after softmax. (5)
(d) Give one concrete failure case where grammar-constrained decoding produces a syntactically valid but semantically wrong tool call, and name the complementary guardrail layer that catches it. (3)
Question 3 — Multi-Agent Planning, Evaluation & Decomposition (18 marks)
A "planner–executor" system decomposes a task into a DAG of subtasks. Edges encode dependencies; independent subtasks may run in parallel across worker agents.
(a) Given a subtask DAG, define the critical path and prove that the minimum makespan (wall-clock time) with unlimited workers equals the critical-path length. For the DAG below with unit-cost nodes and edges , , , , , , , compute the critical-path length and the minimum number of workers needed to achieve it. (7)
(b) Define an evaluation metric for an autonomous agent that separates task success rate from cost efficiency. Propose a single scalar score combining success and cost, and explain why a naive "success ÷ tokens" ratio can be gamed. Give the corrective term. (6)
(c) In a multi-agent debate/critic setup, one agent proposes and another critiques. Argue formally why, under the assumption that the critic's error rate on catching a wrong proposal is , chaining a critic reduces the final error rate — and derive the residual error rate for a proposal with base error . State one assumption that, if violated, makes the critic useless. (5)
Answer keyMark scheme & solutions
Question 1
(a) Input cost at step (steps , define , or index from 0; use ):
- Sum of arithmetic series (2); correct linear + quadratic terms (2).
- Asymptotically (dominated by ). (1)
(b) Capping activates when (first integer exceeding). Before , cost grows as in (a); after, each step costs :
- For the second term dominates: — linear vs quadratic. (2)
- Correct piecewise expression (2); correct (1).
(c) Geometric distribution with success prob :
- (2), tail (2).
- Expected cost: since , and for geometric , :
- Correct use of (2).
(d) Unbounded loops → quadratic cost blow-up (from a & c) and non-termination when small (tail decays only geometrically). Guardrails:
- Token/step budget cap (hard max iterations or token ceiling) — bounds cost & guarantees termination. (2)
- Action-level safety guardrail (allowlist of tools / sandbox / human-in-loop confirmation for destructive actions) — bounds impact of a wrong action. (2) These address distinct failure modes: runaway resource use vs. harmful side effects.
Question 2
(a) BNF (integers 1..50 enumerable, so range is CFG-expressible by enumeration):
<call> ::= '{"name":"search","arguments":{"query":' <str> ',"limit":' <int> '}}'
<str> ::= '"' <chars> '"'
<chars> ::= '' | <char> <chars>
<char> ::= any escaped JSON char
<int> ::= <1-9> | <1-9><0-9> | ... ; enumerate 1..50 explicitly
- Structure fixed by literal tokens (2); integer range via enumeration of
1..50(a finite regular set, hence CFG) (2). - The constraint a pure CFG cannot express: string content restrictions that require matching/counting or semantic validity — e.g. that
querybe non-empty and balanced-escaped is fine, but cross-field constraints or "limit must not exceed number of available results" require a validator. Accept: "arbitrary length numeric ranges with unbounded digits" or "cross-field/context-sensitive constraints." (2)
(b) Conditioning: By definition of conditional probability with event , since if else , and . This is exactly . (3)
Sequence mass: For a full valid sequence where every is allowed given prefix, unconstrained prob ; constrained where . Hence Wait — this increases the valid sequence's probability (mass redistributed toward allowed tokens). Correct statement: masking never decreases the probability of a valid sequence; the mass removed from invalid tokens is redistributed to valid ones. Award marks for correct derivation of the ratio and the conclusion. (4)
- Equality () holds iff the unconstrained model already placed zero probability on disallowed tokens at every step. (1)
(Grading note: full marks for the derivation showing valid sequences are up-weighted with the correct equality condition; the direction of the inequality is the key insight.)
(c)
import numpy as np
def mask_logits(logits, allowed_ids):
masked = np.full_like(logits, -np.inf)
masked[allowed_ids] = logits[allowed_ids]
m = np.max(masked[np.isfinite(masked)]) # stable shift
exp = np.exp(masked - m) # disallowed -> exp(-inf)=0
return exp / exp.sum()- Correct masking + returns normalized distribution (3).
- Why before softmax: (i) guarantees masked tokens get exactly zero probability after normalization with no floating residue; (ii) zeroing after softmax requires re-normalizing (extra pass) and can leave tiny numerical mass; (iii) the pre-softmax shift by max keeps exponents bounded — zeroing post-hoc still computed unstable exponents for huge disallowed logits first. (2)
(d) Example: schema-valid call {"name":"delete_file","arguments":{"path":"/"}} — syntactically perfect but semantically catastrophic; or search(query="", limit=50) valid grammar but empty query. Complementary layer: runtime validator / policy guardrail (semantic argument validation + action allowlist / dry-run). (3)
Question 3
(a) Critical path = longest-weight path from any source to any sink in the DAG; its length lower-bounds makespan because those tasks are strictly sequential (each depends on the previous), so no scheduling can overlap them → makespan CP. With unlimited workers, list-scheduling by "start each task as soon as predecessors finish" achieves start time = longest path into the node, so finish = CP length → makespan CP. (3)
For the DAG (unit costs), longest paths: ; ; . Critical-path length = 4. (2)
Level schedule (ASAP): L0={A}, L1={B,C}, L2={D,E}, L3={F}. Max width = 2 → 2 workers suffice to hit makespan 4. (2)
(b) Let success indicator over tasks, mean success rate ; token cost . Report them separately, then a combined score e.g.
- Separated metrics (2); sensible combined scalar (2).
- Naive is gamed by an agent that fails cheaply: return trivial near-zero-cost answers → tiny denominator inflates ratio even at low success. Corrective term: gate on a minimum success threshold, or use (additive penalty, so failing cheaply gives low score) rather than a ratio. (2)
(c) Proposal wrong with prob . Critic independently catches a wrong proposal with prob (misses with prob ). A wrong proposal survives only if critic misses:
- Derivation (3).
- If the critic also rejects correct proposals (false positives), or catches wrong ones (add: net improvement requires false-positive rate low). Key assumption for usefulness: critic errors must be (approximately) independent of proposer errors — if both share the same blind spot ( exactly on the cases the proposer gets wrong), the critic catches nothing and . (2)
[
{"claim":"E[C_in] for geometric T equals L0/p + s(3-p)/(2 p^2)",
"code":"p,L0,s=symbols('p L0 s',positive=True); T=symbols('T'); ET=1/p; ET2=(2-p)/p**2; expr=L0*ET + Rational(1,2)*s*(ET2+ET); result=simplify(expr-(L0/p + s*(3-p)/(2*p**2)))==0"},
{"claim":"Uncapped input cost sum_{t=1}^{T}(L0+ t s)=T*L0 + s*T*(T+1)/2",
"code":"t,T,L0,s=symbols('t T L0 s'); S=summation(L0+t*s,(t,1,T)); result=simplify(S-(T*L0+s*T*(T+1)/2))==0"},
{"claim":"Critical path length of the DAG is 4",
"code":"import networkx as nx; G=nx.DiGraph(); G.add_edges_from([('A','B'),('A','C'),('B','D'),('C','D'),('C','E'),('D','F'),('E','F')]); L=nx.dag_longest_path_length(G)+1; result=(L==4)"},
{"claim":"Critic reduces error: e*q < e for q<1, e>0",
"code":"e,q=symbols('e q',positive=True); result=simplify((e - e*q))==simplify(e*(1-q)) and bool((e*(1-q)>0).subs({e:Rational(1,2),q:Rational(1,3)}))"}
]