Temporal's 2026 developer survey surfaces a striking pattern. The fraction of engineers using AI coding agents daily or more frequently jumped from 47.3% to 80.8% in a single year — a 70.8% relative increase across 544 respondents spanning the US and UK.
Yet the same survey reveals a troubling second trend. When asked why they don't use agents more, 35.7% cited an inability to track task state — knowing what the agent has already done, what system state it left behind, and how to resume after failure. Trailing close behind: difficulty debugging agent behavior (the agent's output is syntactically correct but semantically wrong, invisible to standard monitoring) and unpredictable cost per task.
The picture is clear: adoption and reliability are diverging. Teams that solved state tracking, observability, and cost control are pulling ahead. Teams that bolted agents into existing processes without these primitives hit a wall.
State Tracking: Why Failures Always Happen Mid-Task
A multi-step agent task succeeds through step two, hits an unexpected condition at step three. Restart the agent? Sure. But what if the failure is at step seven? Step fifteen?
The naive approach: restart from the beginning. This means re-paying API costs and risking duplicate operations against live systems.
Our recommended approach at webhani: checkpoint the agent's state after each step completes. On resume, skip completed steps and start from the failed one.
// Save checkpoint after each agent step
interface TaskCheckpoint {
stepId: string;
status: "pending" | "in_progress" | "completed" | "failed";
result?: unknown;
timestamp: number;
agentContext?: Record<string, unknown>;
}
async function executeTaskWithCheckpoint(
taskId: string,
steps: Array<{ id: string; action: () => Promise<unknown> }>
) {
const checkpointFile = `./checkpoints/${taskId}.json`;
let checkpoint = loadCheckpoint(checkpointFile) || { taskId, steps: [] };
for (const step of steps) {
const existing = checkpoint.steps.find((s) => s.stepId === step.id);
if (existing?.status === "completed") {
console.log(`Step ${step.id} already done, skipping.`);
continue;
}
try {
checkpoint.steps.push({
stepId: step.id,
status: "in_progress",
timestamp: Date.now(),
});
saveCheckpoint(checkpointFile, checkpoint);
const result = await step.action();
checkpoint.steps = checkpoint.steps.map((s) =>
s.stepId === step.id
? {
...s,
status: "completed",
result,
timestamp: Date.now(),
}
: s
);
saveCheckpoint(checkpointFile, checkpoint);
} catch (err) {
checkpoint.steps = checkpoint.steps.map((s) =>
s.stepId === step.id ? { ...s, status: "failed" } : s
);
saveCheckpoint(checkpointFile, checkpoint);
throw err;
}
}
return checkpoint;
}Completed steps are skipped on resume. Failed steps retry. Idempotency at the step level becomes tractable because you control which operations run.
Debugging Agents: Why Standard Logging Fails
Agent failures don't look like traditional errors. The output is valid JSON, the HTTP status is 200, the tool call succeeded. But the agent made the wrong semantic decision, and you don't know until hours later when the task completes with garbage output.
Detect this by logging the agent's reasoning at each step and comparing expected vs. actual outcomes immediately.
// Structured log of every agent decision
interface AgentDecisionLog {
taskId: string;
stepNumber: number;
decision: string;
reasoning: string;
toolSelected: string;
toolArguments: Record<string, unknown>;
toolResult: unknown;
expectedOutcome: string;
actualOutcome: string;
match: boolean;
timestamp: number;
}
async function logAgentDecision(
taskId: string,
stepNumber: number,
decision: string,
reasoning: string,
tool: { name: string; args: Record<string, unknown> },
result: unknown,
expectedOutcome: string
) {
const actual = String(result);
// Simple substring check; in production use semantic similarity or LLM-as-judge
const match = actual.toLowerCase().includes(expectedOutcome.substring(0, 20).toLowerCase());
const log: AgentDecisionLog = {
taskId,
stepNumber,
decision,
reasoning,
toolSelected: tool.name,
toolArguments: tool.args,
toolResult: result,
expectedOutcome,
actualOutcome: actual,
match,
timestamp: Date.now(),
};
// Append-only; never deleted by the agent
fs.appendFileSync(
`./agent-logs/${taskId}.jsonl`,
JSON.stringify(log) + "\n"
);
// Alert immediately on mismatch
if (!match) {
await notifySlack(
`⚠️ Agent decision mismatch in task ${taskId} step ${stepNumber}: ` +
`expected "${expectedOutcome}" but got "${actual}"`
);
}
}Each agent decision is logged with its reasoning and the actual outcome. Mismatches surface immediately instead of at task end. Post-mortems become possible because the entire decision chain is preserved outside the agent's memory.
Cost Control: Preventing Runaway Bills
Agents running tool loops can consume 10x the tokens of a single LLM call. Retry loops spin longer than expected. A single "innocuous" decision spawns dozens of background API calls. Month-end invoice arrives and the bill is $10,000 instead of $1,000.
Our approach: set a budget before the agent runs and abort if it's exceeded.
// Hard budget guard on agent spend
interface CostBudget {
taskId: string;
maxSpendUSD: number;
currentSpendUSD: number;
tokensUsed: { input: number; output: number };
}
const COST_PER_INPUT_TOKEN = 0.000003; // Claude Haiku pricing example
const COST_PER_OUTPUT_TOKEN = 0.000015;
async function executeAgentWithCostGuard(
taskId: string,
maxBudgetUSD: number,
agentFn: () => Promise<unknown>
) {
const budget: CostBudget = {
taskId,
maxSpendUSD: maxBudgetUSD,
currentSpendUSD: 0,
tokensUsed: { input: 0, output: 0 },
};
const originalSpend = async (usage: { input_tokens: number; output_tokens: number }) => {
budget.tokensUsed.input += usage.input_tokens;
budget.tokensUsed.output += usage.output_tokens;
const spend =
budget.tokensUsed.input * COST_PER_INPUT_TOKEN +
budget.tokensUsed.output * COST_PER_OUTPUT_TOKEN;
budget.currentSpendUSD = spend;
if (spend > budget.maxSpendUSD) {
throw new Error(
`Cost exceeded: $${spend.toFixed(4)} > $${maxBudgetUSD}`
);
}
};
try {
// Wire cost tracking into LLM SDK before calling agent
const result = await agentFn();
return result;
} catch (err) {
console.error(`Agent ${taskId} aborted:`, err.message);
await recordCostExceeded(budget);
throw err;
}
}
async function recordCostExceeded(budget: CostBudget) {
fs.appendFileSync(
"./cost-overruns.log",
`${new Date().toISOString()} - ${budget.taskId}: ` +
`$${budget.currentSpendUSD.toFixed(4)} (limit: $${budget.maxSpendUSD})\n`
);
}Agent spend is tracked continuously. Budget exceeded? The agent stops immediately. No more surprises.
Why The Gap Widens
Temporal's data points to a transition: agents are moving from "experiment" to "infrastructure." When you're testing an agent, state management feels optional. Debugging can wait until post-mortems. Cost overruns happen once.
But when agents run in your CI pipeline, serve multiple teams, or touch production systems, these gaps become gaps in your operational readiness.
The reliability gap isn't about model quality. It's about whether you've built the operational layer agents need to be trustworthy.
Checkpointing state, structured decision logging, and cost budgets are simple to implement but transformative in practice. They're the difference between "we tried agents" and "agents are our infrastructure now."
At 80%+ adoption, the question isn't whether to use agents. It's whether you're ready to run them safely.