PromptisePromptise
Docs
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure, and operate autonomous AI systems at scale with Promptise Foundry.

Foundry

  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering

Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths

Company

  • About
  • Imprint
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Back to Guides/Guide

Program-of-Thought & PAL (compute offload)

Hands-on guide to Program-of-Thought (PoT) and PAL. Learn how to generate Python programs per task, run them safely in a sandbox, and return clean results with compact traces. Covers rerun guards, Decimal/itertools usage, single-shot vs. self-consistency, sandboxing, cost vs. latency trade-offs, and a 5-minute mini lab.

September 6, 2025
75 min read
Promptise Team
Advanced
Prompt EngineeringProgram-of-ThoughtTool UseCompute OffloadVerificationGuardrailsReliability

Promise: You’ll learn to offload math and logic to a Python scratchpad the model writes, then execute for correctness—safely. You’ll leave with a ready-to-use “safe runner” prompt that returns only results + a compact trace, and a “disagree? rerun” guard that catches subtle errors before they reach users.


Why this works (and why now)

Chain-of-Thought (CoT) has been a workhorse for reasoning, but models still make arithmetic and logic slips. Two families pushed things forward:

  • PAL — Program-Aided LMs. The model produces a short program; a Python runtime does the exact math. PAL showed big gains across math and symbolic tasks; in GSM8K it beat a much larger CoT system by a wide margin when using Codex + Python execution. (arXiv, Proceedings of Machine Learning Research)

  • PoT — Program-of-Thoughts. Similar idea, but framed as “reason in code, compute outside.” PoT reported ~12-point average improvements over CoT on math/finance benchmarks, and further boosts with self-consistency voting. (arXiv, OpenReview)

The core move is simple: let the model handle problem understanding and decomposition; let Python handle calculation and exact logic. When you add self-consistency (sample a few programs, execute all, vote), robustness improves further. (arXiv, OpenReview)


The move

Think of your system in two parts:

  1. Planner (LLM) → emits a small, pure Python snippet that solves this instance.

  2. Runner (sandbox) → executes it, captures result and a terse trace, and returns structured output. No internal “thoughts,” only code + outputs.

This separation buys you correctness, auditability (you can re-run code), and cost control (code is short; you don’t pay for verbose reasoning).


A compact demonstration

Here’s the pattern on a classic word-problem micro-scenario.

Task: “A train goes 2 hours at 60 km/h and 1.5 hours at 80 km/h. What’s the average speed over the trip?”

LLM emits code (PAL/PoT style):

json

# inputs encoded in code for this instance t1, v1 = 2, 60 t2, v2 = 1.5, 80 d_total = t1*v1 + t2*v2 t_total = t1 + t2 avg = d_total / t_total trace = {"d_total": d_total, "t_total": t_total} result = round(avg, 2)

Runner returns JSON (no chain-of-thought):

json

{"result": 69.14, "trace": {"d_total": 270.0, "t_total": 3.5}}

That’s the entire surface: code in, result + trace out. If your UI shows anything else, you’re leaking tokens for no reason.


Build the safe runner (drop-in)

Below is a copy-paste scaffold for a Python sandbox that executes model-generated snippets safely and predictably. It forbids file/network, limits imports to a safe list, whitelists AST nodes, enforces timeouts, and extracts only result and trace.

Use this when correctness matters (math, tabular logic, unit conversions), or when a short program beats long free-form reasoning.

json

# Safe PAL/PoT runner — minimal, production-oriented scaffold import ast, builtins, math, statistics, decimal, fractions, itertools, time, types, sys, signal SAFE_MODULES = { "math": math, "statistics": statistics, "decimal": decimal, "fractions": fractions, "itertools": itertools, } # Allowed AST nodes (tighten/expand as needed) ALLOWED_NODES = { ast.Module, ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Expr, ast.Call, ast.Name, ast.Load, ast.Store, ast.BinOp, ast.UnaryOp, ast.Num, ast.Constant, ast.Dict, ast.List, ast.Tuple, ast.Compare, ast.If, ast.IfExp, ast.For, ast.While, ast.Break, ast.Continue, ast.Return, ast.Attribute, ast.Subscript, ast.Slice, ast.Index, ast.BoolOp, ast.And, ast.Or, ast.Not, ast.Pass, ast.Lambda, ast.FunctionDef, ast.arguments, ast.arg, ast.With, ast.Raise, ast.Try, ast.ExceptHandler, } # Remove dangerous builtins SAFE_BUILTINS = {k: getattr(builtins, k) for k in ("abs","min","max","sum","range","len","enumerate","zip","round","sorted","map","filter","any","all")} SAFE_GLOBALS = {"__builtins__": SAFE_BUILTINS, **SAFE_MODULES} class Timeout(Exception): pass def _timeout_handler(signum, frame): raise Timeout("Execution timed out") def ast_is_safe(tree): for node in ast.walk(tree): if type(node) not in ALLOWED_NODES: return False, f"Disallowed AST node: {type(node).__name__}" if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): if node.func.id in {"__import__", "eval", "exec", "open", "input", "compile"}: return False, f"Disallowed call: {node.func.id}" return True, None def run_model_code(src: str, time_limit_sec=1.0): # Parse & validate tree = ast.parse(src, mode="exec") ok, why = ast_is_safe(tree) if not ok: return {"ok": False, "error": why} # Isolate namespace local_env = {} # Enforce wall-clock timeout signal.signal(signal.SIGALRM, _timeout_handler) signal.alarm(int(time_limit_sec)) try: exec(compile(tree, filename="<model>", mode="exec"), SAFE_GLOBALS, local_env) signal.alarm(0) except Timeout as e: return {"ok": False, "error": str(e)} except Exception as e: return {"ok": False, "error": f"{type(e).__name__}: {e}"} finally: signal.alarm(0) result = local_env.get("result", None) trace = local_env.get("trace", None) if result is None: return {"ok": False, "error": "No `result` produced"} # Make trace optional but structured if not isinstance(trace, (dict, type(None))): trace = {"note": "non-dict trace was coerced to string", "value": str(trace)} return {"ok": True, "result": result, "trace": trace or {}}

How to use it. Feed src with the model’s emitted Python code. You’ll get {"ok": bool, "result": ..., "trace": {...} or {}}. The sandbox forbids imports beyond the safe list, blocks I/O and eval/exec, and times out long runs. In production, put this behind a container boundary as well.


A

The prompt below instructs the model to output a tiny Python solution and nothing else. It also builds in precision guidance and structured output.

Paste into your “code-writer” system prompt. Adjust the allowed modules list to match your sandbox.

json

You are a Python scratchpad that solves the user’s question by writing a **small, pure** Python program. Rules: - Use only these modules if needed: math, statistics, decimal, fractions, itertools. - No I/O, files, network, randomness, or imports beyond the list. - Put the final numeric/string answer in a variable named `result`. - Optionally create `trace` (a dict of key intermediate values). - Favor exact arithmetic (fractions/decimal) if rounding matters. - Do not print. Do not explain. Output **only** the Python code. Example shape to follow (pattern, not a constraint): from decimal import Decimal, getcontext getcontext(prec=50) # ... compute ... result = <final_value> trace = {"key": value} Now write code for: {{QUESTION}}

This is intentionally minimal: no prose, no chain-of-thought. Just code.


The “disagree? rerun” guard (catch silent errors)

Even with PAL/PoT, errors happen: off-by-ones, unit slips, integer division, rounding. You can catch many with a cheap controller that asks for a quick mental estimate, runs the code, and reruns if the two disagree beyond a tolerance.

Controller sketch (one call + one optional retry):

  1. Ask model for a “sanity window.” A fast, language-only estimate: range [low, high] plus notes like “must be integer”, “in USD”.

  2. Generate program (with the safe runner prompt), execute.

  3. Compare. If result ∉ window or violates type/format expectations, ask the model to self-debug with the runner’s error or mismatch summary. Regenerate code once and re-execute.

  4. If still out of bounds, escalate: return both results + a short inconsistency note (or route to a stronger model).

Controller prompts (drop-in):

Sanity-window prompt (fast check):

json

Give a quick sanity window for the answer to this question. Return JSON with keys: - "low": numeric lower bound - "high": numeric upper bound - "constraints": short list (e.g., ["integer", "USD", "nonnegative"]) Do not solve exactly; give a conservative window. Question: {{QUESTION}}

Self-debug prompt (only on mismatch/error):

json

The previous Python run failed this check: - Estimated window: [{{LOW}}, {{HIGH}}], constraints: {{CONSTRAINTS}} - Runner output: {{ERROR_OR_RESULT}} Write a corrected small Python program that satisfies the constraints while solving the same question. Follow the same rules: safe modules only, no I/O, put the final value in `result` and optional `trace`. Output code only.

Tolerance tips. If you expect an integer count, use int(result) == result. For floats, use relative tolerance: abs(a-b) <= max(1e-9, 1e-6*abs(b)). For money, snap to Decimal and cents.

Why this helps. You get the speed of a single shot most of the time, but you avoid shipping a wrong number when the code sneaks a subtle bug. Self-consistency (N programs → vote) can be swapped in when stakes are high. (arXiv)


Variations that matter in practice

Tabular reasoning (finance, ops, analytics). Ask the model to embed a tiny table or pre-parsed rows in code, then operate with decimal or fractions to avoid floating-point surprises. PoT reports strong gains on financial QA when the model expresses steps in code and the runner computes exact values. (arXiv)

Unit conversions and dimensional checks. Encourage a trace with units at key steps ({"km": ..., "hours": ...}). Reject if units mix without conversion.

Combinatorics / search. Allow itertools for small enumerations, but keep the time limit tight. If you routinely hit timeouts, add a “design an O(n log n) method” nudge to the code-writer prompt.

Precision policy. If your domain cares about pennies or basis points, require decimal.getcontext(prec=50) and round only at the end.


Failure modes and the fixes

  • Model writes imports you don’t allow. Return the runner error; your self-debug prompt reminds it of the rules.

  • Infinite/long loops. The timeout gets you out. In the self-debug step, add “design an O(n) or O(n log n) solution; avoid brute force.”

  • Rounding drift. Require Decimal in the prompt when money, ratios, or tax are involved.

  • Non-deterministic output. Disallow randomness; strip prints; require result only.

  • Leaky chain-of-thought. Your code-only contract + runner extraction prevents it.

  • Edge cases on empty/zero. Add a tiny assertion convention: the model may set trace["checks"] = ["n>0"]; your controller can reject if violated.


Cost & latency trade-offs

  • Single-shot PAL/PoT is typically cheaper than verbose CoT for math/logic: fewer tokens, deterministic compute.

  • Self-consistency (k programs → vote) costs ~k× more tokens but improves reliability on ambiguous or brittle tasks; PAL/PoT + voting established strong SoTA at modest k in early results. Use it when error cost > compute cost. (arXiv)

  • Controller retry adds one more model call only when needed; in production traces this triggers far less than 20% if your prompts are disciplined.


In practice: copy-ready scaffolds

Controller (pseudo-Python around your model/RPC):

json

def solve_with_pal(question, model, k=1): window = model("SANITY_PROMPT", QUESTION=question) # returns low, high, constraints def gen_and_run(): code = model("CODE_WRITER_PROMPT", QUESTION=question) return run_model_code(code) # the sandbox from above # Option A: single program + possible retry out = gen_and_run() if out["ok"] and window["low"] <= float(out["result"]) <= window["high"]: return out # Retry once with self-debug debug = model("SELF_DEBUG_PROMPT", LOW=window["low"], HIGH=window["high"], CONSTRAINTS=window["constraints"], ERROR_OR_RESULT=out if not out["ok"] else out["result"]) out2 = run_model_code(debug) return out2 if out2["ok"] else {"ok": False, "error": "Escalate"} def solve_with_pal_voted(question, model, k=5): codes = [model("CODE_WRITER_PROMPT", QUESTION=question, seed=i) for i in range(k)] outs = [run_model_code(c) for c in codes if c] goods = [o for o in outs if o["ok"]] if not goods: return {"ok": False, "error": "All runs failed"} # Majority vote on normalized results (stringify decimals consistently) from collections import Counter norm = lambda x: str(x["result"]) winner, _ = Counter(map(norm, goods)).most_common(1)[0] exemplar = next(g for g in goods if norm(g)==winner) return exemplar

Policy knobs you’ll want in config:

  • allowed_modules, time_limit_sec, precision, vote_k, retry_on_disagree, disagree_tolerance, return_trace=True/False.


Mini lab (5 minutes)

Goal: See the full loop—sanity window → code → run → compare → (maybe) rerun—on a small but non-trivial case.

Question: “A store sells widgets with tiered pricing: first 120 units at $3.20 each, next 80 at $3.00, any beyond at $2.75. You bought 245 units. Add 7.5% tax. What’s the total, in dollars, rounded to 2 decimals?”

What you should see:

  • Window might return [600, 1000] with ["USD", "nonnegative", "2dp"].

  • Code computes tiered subtotal using integer arithmetic, then Decimal for tax and rounding.

  • Runner returns something like:

    {"result": "793.56", "trace": {"qty": 245, "subtotal": "738.00", "tax": "55.56"}}

  • If your first run violates “2dp” (e.g., returns a float with many decimals) or lands outside the window due to a tier bug, the self-debug retry should correct it.

If your output wildly differs, tighten the prompt: require Decimal and explicit rounding at the end, and add a constraint “must be a string with exactly two decimals.”


Where PoT vs PAL differ (and why you might prefer one)

Both emit Python and execute it. In practice:

  • PAL papers emphasize the synergy of neural decomposition + symbolic interpreter, reporting strong wins on GSM8K and BIG-Bench-Hard with simple Python snippets. If you mainly do short math/algorithmic tasks, start here. (arXiv)

  • PoT frames “reasoning in code” broadly and showed gains on math and financial QA, especially when combined with self-consistency. If you do tabular/finance Q&A or need richer traces, lean PoT. (arXiv)

In production, the distinction blurs; what matters is your runner contract, sandboxing, and guards.


Security posture (don’t skip)

Treat model-emitted code as hostile until proven otherwise.

  • Sandbox double-wall. Use the AST whitelist and run inside a restricted container/user with no network, no file mounts, tight CPU/mem/time limits.

  • No dynamic imports. Keep __import__, eval, exec, open, input blocked.

  • Audit surface. Log the code hash, inputs, result, and trace for replayability; never log secrets.

  • Determinism. Disallow randomness; if you must allow it, seed explicitly and capture the seed.


Troubleshooting playbook

  • “No result produced.” Add a final result = ... rule to your code-writer prompt; reject runs without it.

  • “Disallowed AST node.” The model reached for a disallowed construct (e.g., list comprehension with walrus). Either expand your whitelist (carefully) or add guidance like “use explicit loops.”

  • Time-limit exceeded. Raise in the self-debug step: “Design O(n log n) approach. Avoid brute force beyond n=5000.”

  • Float jitter. Switch to Decimal and quantize at the end.

  • Voting splits. If k programs produce several different answers, escalate: return both the plurality and the runner logs; or increase k temporarily. Self-consistency literature supports modest k (e.g., 5–10) as a good trade-off. (arXiv)


References & further reading

  • PAL: Program-Aided Language Models — arXiv & code site. Foundational for “LLM writes code, Python executes,” with strong GSM8K results. (arXiv, reasonwithpal.com, Proceedings of Machine Learning Research)

  • Program-of-Thoughts Prompting — arXiv/OpenReview. Improves over CoT on math and finance; highlights gains with self-consistency. (arXiv, OpenReview)

  • Self-Consistency for CoT — general technique for sampling diverse reasoning paths and voting; equally applicable to multiple programs. (arXiv)

  • Chain-of-Thought Prompting — classic baseline to contrast with compute-offloading. (arXiv)


Summary & Conclusion

Compute-offloading reframes reasoning tasks: instead of coaxing perfect arithmetic from a generative model, you ask for a short, auditable program and let Python do the math. PAL and PoT demonstrated that this shift is not only cleaner but often more accurate than long chains of natural-language steps, especially when you add a light self-consistency or a simple “sanity window” guard.

In practice, the quality of your runner contract—code-only output, strict sandbox, precise result/trace—and your controller—estimate, execute, compare, retry—matters more than the label on the paper. With those in place, you ship fewer wrong numbers, debug faster, and keep latency predictable.

Finally, treat code as untrusted, log what matters, and revisit your tolerance and precision policies as your domain evolves. The pattern is simple, the wins are real, and once you wire it in, it becomes the default for any task where numbers or exact logic drive user trust.

Next steps

  • Wire the safe runner scaffold into your stack and test it against 20–50 of your real tickets.

  • Add the “disagree? rerun” controller and measure how often it triggers; tune the tolerance.

  • For high-stakes flows, experiment with k-program voting (k=5) and compare error/cost curves to your current CoT baselines.

Learning Paths

Structured Learning

Follow guided learning paths from beginner to advanced. Master prompt engineering step by step.

Explore Paths

Continue Your Learning Journey

Ready to Master More? Explore our comprehensive guides and take your prompt engineering skills to the next level.

Explore More GuidesBrowse Learning Paths