<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mehmet Kerem Turhan</title>
    <description>The latest articles on DEV Community by Mehmet Kerem Turhan (@keremturhan).</description>
    <link>https://dev.to/keremturhan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4062974%2F293ab613-27dc-4fd5-8ac8-c52acf7fd817.png</url>
      <title>DEV Community: Mehmet Kerem Turhan</title>
      <link>https://dev.to/keremturhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keremturhan"/>
    <language>en</language>
    <item>
      <title>When a Failed Agent Step Looks Finished</title>
      <dc:creator>Mehmet Kerem Turhan</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:34:17 +0000</pubDate>
      <link>https://dev.to/keremturhan/when-a-failed-agent-step-looks-finished-1g8b</link>
      <guid>https://dev.to/keremturhan/when-a-failed-agent-step-looks-finished-1g8b</guid>
      <description>&lt;h2&gt;
  
  
  When a Failed Agent Step Looks Finished
&lt;/h2&gt;

&lt;p&gt;A multi-agent research workflow can fail in a quiet way: a step returns nothing useful, the orchestration keeps going, and the run ends with a polished report. The logs still know something went wrong. The surfaces people actually read — the summary, the terminal output, the exit path — do not.&lt;/p&gt;

&lt;p&gt;I evaluated OpenAI's official financial-research example in &lt;a href="https://github.com/openai/openai-agents-js" rel="noopener noreferrer"&gt;&lt;code&gt;openai/openai-agents-js&lt;/code&gt;&lt;/a&gt;, frozen at commit &lt;code&gt;710cccfd8fd26b395f8e3470419852d76de80967&lt;/code&gt;. A six-case harness caught two fail-open paths. A one-file patch closed them. Upstream fixed the example about ten hours after the report. This is a case study of that path — not a critique of the SDK, and not a claim about how often the pattern shows up in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was measured
&lt;/h2&gt;

&lt;p&gt;The target slice is &lt;code&gt;examples/financial-research-agent&lt;/code&gt;. The example plans web searches, runs search agents concurrently, drops failed results, writes a structured report, asks a verifier for &lt;code&gt;{ verified, issues }&lt;/code&gt;, revises at most twice, and then emits terminal output.&lt;/p&gt;

&lt;p&gt;Every result here is labeled &lt;code&gt;synthetic-orchestration&lt;/code&gt;: the real &lt;code&gt;FinancialResearchManager&lt;/code&gt; control flow runs, but the model and web-search boundaries are deterministic fakes. No model API key is required. That keeps the evidence cheap and repeatable. It also means this is not a model-quality benchmark, a financial-accuracy study, a security finding, or evidence about the Agents SDK as a whole.&lt;/p&gt;

&lt;p&gt;The six cases cover search aggregation, partial search failure, complete search failure, an accepted first draft, a report that passes on the final retry, and retry exhaustion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baseline: 4/6
&lt;/h2&gt;

&lt;p&gt;Canonical baseline run: &lt;strong&gt;4 pass, 2 fail&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The two failures share one control-flow pattern. A negative prerequisite exists in state, but no guard connects that state to the terminal report path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complete search failure.&lt;/strong&gt; Both searches fail. &lt;code&gt;search()&lt;/code&gt; turns exceptions into &lt;code&gt;null&lt;/code&gt;; &lt;code&gt;performSearches()&lt;/code&gt; filters them out; &lt;code&gt;run()&lt;/code&gt; still calls the writer. The manager reports success and emits a report with no usable sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification exhaustion.&lt;/strong&gt; The verifier rejects the initial report and both permitted revisions. The revision loop stops correctly after two attempts, then output proceeds anyway. The manager ships the still-rejected report.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two of the four baseline passes reproduce the example's own shipped tests (accepted first draft; pass on final retry). Across the four cases this teardown designed, the baseline was therefore &lt;strong&gt;2 pass / 2 fail&lt;/strong&gt;. The headline 4/6 includes the two replicated cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  After a one-file patch: 6/6
&lt;/h2&gt;

&lt;p&gt;The patch changes only &lt;code&gt;examples/financial-research-agent/manager.ts&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;throw before the writer when zero usable search summaries remain;&lt;/li&gt;
&lt;li&gt;throw before terminal output when verification is still negative after the bounded revision loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same corpus, same fakes, same control flow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Patched&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus pass&lt;/td&gt;
&lt;td&gt;4/6&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus fail&lt;/td&gt;
&lt;td&gt;2/6&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream files changed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Behaviorally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FR-003 (all searches fail):&lt;/strong&gt; baseline called the writer and emitted a report; patched stops with a controlled error before the writer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FR-006 (retry budget exhausted):&lt;/strong&gt; baseline kept three negative verifications and two revisions, then still emitted the report; patched keeps the same loop shape and stops with a controlled error instead of emitting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other four cases stayed green. The example's own two focused tests still pass under the patched run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upstream closed the path
&lt;/h2&gt;

&lt;p&gt;The behavior was reported as &lt;a href="https://github.com/openai/openai-agents-js/issues/1544" rel="noopener noreferrer"&gt;openai/openai-agents-js#1544&lt;/a&gt;. Maintainer pull request &lt;a href="https://github.com/openai/openai-agents-js/pull/1546" rel="noopener noreferrer"&gt;#1546&lt;/a&gt; ("fail closed without financial research sources") merged into &lt;code&gt;main&lt;/code&gt; as &lt;code&gt;6483fef&lt;/code&gt; about ten hours after the report and closed the issue as completed.&lt;/p&gt;

&lt;p&gt;The reproduction in the teardown repo stays pinned to &lt;code&gt;710cccf&lt;/code&gt; on purpose — a commit that predates the fix — so the recorded baseline remains checkable after the code moved on. The path described here is closed in current &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same class — a failed step treated as a finished one — also turned up in a scheduled maintenance agent in &lt;a href="https://github.com/google/adk-python" rel="noopener noreferrer"&gt;&lt;code&gt;google/adk-python&lt;/code&gt;&lt;/a&gt;. A collaborator reproduced it; a fix pull request is still under review. That is a separate frozen path, not evidence that anything about ADK is already resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce without a key
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prerequisites: Git, Node.js 22+, Corepack, and network access for the initial clone and locked install. No model API key, paid API, or database. On a fresh clone the full gate was measured at &lt;strong&gt;184 seconds&lt;/strong&gt;, which includes cloning the frozen target; with that clone already cached, three later runs measured 90, 117 and 119 seconds. Timings move with machine load, so treat these as observations rather than guarantees.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm run verify&lt;/code&gt; sets up the detached frozen target, reproduces the canonical baseline (4/6), applies the patch for the remediation run (6/6 plus upstream compatibility checks), reverses the patch, and validates hashes and claims. Full constructive analysis lives in the teardown repository's &lt;code&gt;TEARDOWN.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe your own agent in ten minutes
&lt;/h2&gt;

&lt;p&gt;You do not need this harness. The method is one sentence: &lt;strong&gt;break a step on purpose with a deterministic fake, and see whether anything downstream turns red.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Work through these against one real code path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make the tool boundary raise.&lt;/strong&gt; Replace one client or wrapper with a stand-in that raises immediately. If the summary still counts the item as processed, or a final artifact is still produced, the path is fail-open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the exit status, not the log.&lt;/strong&gt; Run the entry point the way your scheduler does and check the status. Status zero after logged errors usually means a catch-and-fall-through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exhaust the retry budget.&lt;/strong&gt; Force verification or retry to stay negative on every attempt. If the loop ends and the next stage still runs, exhaustion was treated as permission to continue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return empty instead of raising.&lt;/strong&gt; Many pipelines only handle exceptions. An empty list or null flowing into a report is the same defect with quieter symptoms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run zero units of work.&lt;/strong&gt; Point the runner at an empty input set. A green pass rate with nothing measured is not a pass — it is an unmeasurable run reported as success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail one item out of several.&lt;/strong&gt; Partial failure is where "attempts" and "successes" diverge. If the summary counts the whole batch, the accounting is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind a guard.&lt;/strong&gt; Delete the file a check reads, revoke a credential, or hand it malformed input. A guard that still passes with nothing to inspect was decoration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate over nothing.&lt;/strong&gt; Ask for a metric over an empty result set. A number where the honest answer is "not measured" — especially when lower-is-better metrics report an ideal empty value — is fail-open one level up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any probe stays green where it should turn red, you already have a deterministic reproduction: the fake you just wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;p&gt;These results do not measure model quality, financial correctness, security, production frequency, or the Agents SDK beyond one frozen example. Fake boundaries make every figure synthetic-orchestration evidence. That limitation is deliberate: the failure under study is in the accounting of failed steps, not in model intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;A system that cannot tell a failed step from a finished one fails open. The most persuasive form of that failure is not a crash. It is a fluent report written after the evidence is gone.&lt;/p&gt;

&lt;p&gt;Define the minimum usable evidence before synthesis. Treat retry exhaustion as a terminal state. Assert side effects — downstream calls, emission, exit status — not only returned objects. And when you break a step on purpose, believe the surface that says "done" only if every other surface agrees.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kerem Turhan. Reproduction and write-up: &lt;a href="https://github.com/kerem-turhan/agent-reliability-teardown-openai-agents-js" rel="noopener noreferrer"&gt;agent-reliability-teardown-openai-agents-js&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
